Getting Started with EK9 · Chapter 19
Components
You now have three ways to build an aggregate: a record for data, a class for data with behaviour, and a trait for behaviour shared across types. The fourth is for a bigger job. A component is what you use when a whole group of cooperating types — classes, records, functions, traits — should sit behind one simple element that hides them.
Hiding types, not just values
A class practises value hiding: its fields are private, so callers reach its data only through the methods it offers (Classes). A component does the same one level up: type hiding. It presents a simple interface, and behind that interface is whatever set of classes, functions and records the work needs. A caller depends on the interface alone, and never learns which types do the job, or how many there are.
That is the purpose of the arrangement. Because every caller depends on the interface and nothing behind it, the developer is free to change the implementation — swap the types, add new ones, rework the whole calculation — without affecting any caller. Hiding the types is what leaves the implementation free to change.1
Here is the smallest example that shows the shape: a component that quotes a price, built on two helpers.
#!ek9 defines module book.pricing defines class TaxTable surcharge() <- rtn as Money: 2#GBP defines function basePrice() <- rtn as Money: 10#GBP defines component Pricing as abstract totalPrice() as abstract <- rtn as Money? StandardPricing is Pricing override totalPrice() <- rtn as Money? table <- TaxTable() rtn: basePrice() + table.surcharge() defines program Show() stdout <- Stdout() pricing as Pricing: StandardPricing() stdout.println(`total price: ${pricing.totalPrice()}`)
total price: 12.00#GBP
A component is declared much like an abstract class: Pricing as abstract names the interface — one
method, totalPrice — and StandardPricing is Pricing provides a concrete version. What makes it a
component rather than a class is its role: it is the unit you put a whole subsystem behind.
Look at the program. It holds pricing as a Pricing — the interface — and calls totalPrice(). It
creates a StandardPricing once, but never mentions TaxTable and never calls basePrice; those
belong to the implementation. The Pricing interface is all the rest of the program sees.
Why hide the types
That freedom is the reason components exist, so here it is in concrete terms. Today totalPrice adds
a base price and a surcharge from a small TaxTable. Tomorrow it could read live rates from a
database, split the work across five new classes, or replace the calculation entirely — and no line of
the program that calls totalPrice() would change, because none of it depended on how the price was
worked out. A component separates what a subsystem does from how it does it, so the two can change
independently. It is information hiding, applied to a whole subsystem rather than a single
object.2
Components earn their place in larger software, where whole areas of a system — a payment engine, a search index, a report generator — are best kept behind a simple interface so the rest of the code does not depend on their internals. In a real project the internal types would live in their own module, in separate files, out of the caller's sight; this example keeps everything in one module so it fits in one file. Splitting a project across modules, and injecting a component so one implementation can be swapped for another, are topics for the second volume.
Coupling, cohesion, and a compiler that keeps score
Two terms describe designs like this. Coupling is how much one part of a program depends on others — how many other things it reaches out to. Cohesion is how well the pieces inside one thing belong together, all serving a single purpose. Good design wants coupling low, so a change in one place does not spread everywhere, and cohesion high, so each thing has one clear job. Both terms date from 1974.3 A component is a tool for low coupling: by hiding a subsystem's types, it keeps down the number of things the rest of the program depends on.
EK9 does not leave coupling and cohesion as advice. The compiler measures them — how many other constructs each class, record, trait, component and module depends on, and how well each one's parts belong together — and enforces generous but fixed limits. Go past them and the code does not compile: a class that depends on too many others, a module too tangled to be one module, a component whose parts have drifted into unrelated jobs. The limits are deliberately loose, so ordinary code does not meet them; they exist to catch a badly over-connected or unfocused design before it sets. When you cross one, the error gives your figure and the limit. (The EK9 IDE also shows each construct's coupling and cohesion as you write.)
This is the language's usual approach, applied to structure: a quality usually left to reviewers is something the compiler measures and enforces.
Note. Even this small example has to pass the compiler's quality rules. A helper with an unused parameter is refused (
E08091: "parameter is declared but never used"), and so is a literal repeated three times (E11065: "extract to a named constant"). Small examples are checked exactly as large programs are; there is no relaxed mode for sketches.
Try it yourself
- Add a second implementation,
SalePricing is Pricing, whosetotalPrice()takes1#GBPoff the standard price. Switch the program to it by changing only the line that makespricing. Nothing that usespricingchanges. - Try to make a
Pricing()directly, and read the compiler's error. Why can an abstract component not be built? - Give
TaxTablea second method, and call it only fromStandardPricing. Which parts of the program would notice if you changed it later?
Recap
A component is the fourth aggregate, alongside record, class and trait. A class hides its
values; a component hides its types — a set of cooperating classes, functions and records behind
one simple interface — so callers depend on what a subsystem does, never on how. It is declared like
an abstract class (defines component, an abstract interface, a concrete is implementation), and its
role is to draw a boundary around a subsystem. It comes into its own in larger programs, with its
internals in their own module; packaging and dependency injection are in the second volume. The compiler
also measures coupling and cohesion and enforces limits on both. Next, Using Generics: types
written to work with any type.
-
This is the subject of David L. Parnas, "Designing Software for Ease of Extension and Contraction," IEEE Transactions on Software Engineering 5, no. 2 (1979): 128–138, which treats extensibility as "a special case of design for change": structure a system so that a module's implementation can be extended or replaced without disturbing the code that uses it. Hidden internals behind a stable interface are what buy that freedom. ↩
-
The principle is David Parnas's, from "On the Criteria To Be Used in Decomposing Systems into Modules" (Communications of the ACM 15, no. 12, 1972), first met in Classes: each module should hide a design decision likely to change, so the decision can be revised without disturbing the rest of the system. A class applies it to a data structure; a component applies it to a whole subsystem. ↩
-
The terms were coined by Wayne P. Stevens, Glenford J. Myers and Larry L. Constantine in "Structured Design," IBM Systems Journal 13, no. 2 (1974): 115–139 — the paper that gave the field the vocabulary of coupling and cohesion. The specific measures EK9 enforces are efferent coupling (Ce — how many other constructs a construct reaches out to) and LCOM4 (a lack-of-cohesion measure that counts how many disconnected clusters a type's methods and fields fall into; M. Hitz and B. Montazeri, "Measurement of Coupling and Cohesion in Object Oriented Systems," 1995). Cohesion is checked on classes, services and components — records and traits are exempt by design. EK9's thresholds are deliberately generous, set to leave its functional style unhindered while still catching over-connected or unfocused designs. ↩
In the EK9 reference: Components in EK9, Dependency Injection in EK9, Design note: why EK9 limits injection and aspects