Composition in EK9
The traits section outlined composition. This section covers it in more detail, through an example, and explains why EK9 puts so much weight on traits and composition.
Composition background
When object-oriented programming took hold, defining data structures ('structs' in C) and manipulating them with functions fell out of use. Classes and deep class hierarchies took over, often with multiple inheritance, and inheritance became the way to reuse functionality, at the cost of coupling and lost encapsulation. Design note: why EK9 favours composition over inheritance sets out that history and the argument.
The 'new OOP' approach was popular because it hid data and limited access to state (and especially its mutation). EK9's pure mechanism lets mutable data structures stay free of mutation. It is pragmatic: it focuses on the processing, not on the data structure itself.
- You can pass nested data structures into a pure function; it can take a reference to part of the structure and use its values, but not mutate it (E08120).
- The same function can return an object that references part of that structure. Non-pure code that receives it (a function or method) can then mutate it if required.
So with EK9 records and functions you can use the old approach again. It is not bad practice; it is a valid approach if you prefer a functional style. pure gives you finer control over state, and removes the need for the endless 'getters' that spread through OOP code bases.
Why EK9 does it this way. Composition is not used more because it has not been easy enough. Take a trait/interface with 20 methods and a class that implements them all. To compose a second class with it, you write 20 methods that only call delegate.{methodName}. EK9 writes those for you with by ... (see the example below). A class can have several traits, and each trait, with all its methods, can be delegated to a concrete class you already have. Override any of those methods yourself and EK9 does not delegate them.
Smaller and well-defined functionality
In general, the smaller, better defined and more immutable a piece of functionality is, the:
- more reliable it is
- easier it can be tested
- more reusable it is
- refactoring and improvement is simplified
Composition makes the SOLID principles easier to follow than inheritance alone does.
With delegation and multiple inheritance of traits, you can apply the 'I' - interface segregation principle easily. For example: define any number of traits, with a few classes that implement them; then create a trait that extends those traits, and a concrete class that delegates all or some of the implementation to the existing classes.
Functions
A function is the smallest and most reusable piece of functionality; in general it has no state of its own. Object-oriented programming discarded it to some degree: less so in C++, more in Java, which partly reintroduced it in Java 8.
In EK9 the function is a critical, reliable and scalable construct. Function delegates let you adjust what a class does without sub-classing; see this example (see Square).
A function delegate has no direct access to the object's state. Some developers see that as an advantage, as it respects encapsulation. If it needs the state, one approach is to hold all the 'class' state in a record and pass the record into a pure function. That gives direct, read-only access to the data, but breaks the class's information hiding; whether that is worth it is a judgement.
Small Classes
Developers tend to create God classes. These have far too much functionality, or manipulate other objects as if they were C structs (going through the motions of object orientation with 'getters'/'setters'). They tend to be very large, link to many other classes, or have very deep hierarchies.
Break them into much smaller classes, functions and records, and compose them behind a class that delegates each call to the smaller classes. Smaller, well-rounded, encapsulated classes reduce defects and complexity, increase reuse, and follow the single responsibility principle more easily.
Façade
Once a large class is broken up, you may still want a unifying 'interface' (don't let it grow very large). In most languages you would declare an interface type repeating most of the classes' methods; the class that implements it must then declare every method again, and call the method on the class that provides it.
EK9 solves this with traits, as the example below shows: 'Employee' has traits that it delegates, with by, to a matching property class.
Inheritance
Inheritance has its own section, and it is still a key technique. Use it to model concepts that are naturally structured, not because data or processing looks similar.
For example, a class ExternalAspect has properties colour and texture. Human, Bird and Chair all have those properties to some degree, but they are not 'an' ExternalAspect. They could have an ExternalAspect property. With real-world examples this is often obvious; with abstract software concepts it gets blurred.
Even with real-world examples, class structures and inheritance should not follow the concepts too closely. Reality and what you need to model in software can conflict, and that is a common source of confusion.
Contradictions
That leaves some contradictions:
- Large class hierarchies lead to complexity and lack of flexibility
- Breaking up classes into smaller implementations, creates many classes to be composed
- Façades can create duplication of method declarations and the interface/implementation duality
The EK9 Approach
You want small, well-defined traits/classes and the benefits of the Façade, without the disadvantages of either (and still without making the Façade too large). EK9's answer has four parts:
- Function delegates.
- Traits: a simple, well-defined public interface that can hold default functionality and abstract method signatures.
- Multiple inheritance of traits: a Façade is a new trait that has the traits of every feature it needs.
- Class/object delegation: EK9 makes every delegated call for you, so a class repeats nothing, and you can still provide a new implementation of any method.
In the example below, see:
Employee with trait of IRole by role, IPayrollCalculator by payroll
Employee implements two traits, IRole/IPayrollCalculator, and delegates their implementation to its role and payroll properties.
Example
Composition is a design pattern you can use in any language. The diagrams below come from a Python example, itself taken from a book on C++ design. The article is long but detailed and interesting; the two diagrams contrast inheritance with composition.
The EK9 implementation keeps the design largely unchanged. It may not be perfect, or what some OO designers would like, but it shows the issues and proposes a solution you could refine.
Inheritance
The inheritance design is complex, and uses multiple inheritance.
Composition
The composition design is simpler, and centres on the employee. Modelling 'Employment', 'Employee' and 'Person' rather than only 'Employee' might have been better, but that is a modelling question; the subject here is composition.
Composition as above in EK9
The example is long so that the benefits of EK9's composition can be seen. It uses the same names for the constructs as the composition diagram, so the mapping is obvious. Method names have changed, and the inheritance structure is slightly different (and more logical). It is under 300 lines, and the implementation is broadly the same as the Python code. EK9 offers alternatives (such as text constructs or components), not used here, to stay close to the Python and to the point: composition.
Composition Example
#!ek9 defines module site.composition.employees defines trait IRole performDuties() -> hours as Integer <- activity as String? IPayrollCalculator trackWork() -> hours as Integer calculatePayroll() <- amount as Money? defines record //Added support for type inference back in for fields/properties //but only is default constructor or a literal Address street <- String() street2 <- String() city <- String() state <- String() zipcode <- String() //Might allow default with matching number type and name of parameters. Address() -> street as String street2 as String city as String state as String zipcode as String this.street :=: street this.street2 :=: street2 this.city :=: city this.state :=: state this.zipcode :=: zipcode override operator ? as pure <- rtn as Boolean: street? and city? and state? and zipcode? operator $ as pure <- rtn as String: String() mainLocation <- `${city} ${state} ${zipcode}` if street2? rtn := `${street}\n${street2}\n${mainLocation}` else rtn := `${street}\n${mainLocation}` defines class Employee with trait of IRole by role, IPayrollCalculator by payroll id <- Integer() name <- String() address <- Address() role as IRole? payroll as IPayrollCalculator? //You can make a default constructor unavailable if you wish default private Employee() //No body //You could just use //default Employee() + the paramaters and the ek9 compiler would do that implementation for you. Employee() -> id as Integer name as String address as Address role as IRole payroll as IPayrollCalculator this.id: id this.name: name this.address: address this.role: role this.payroll: payroll work() -> hours as Integer stdout <- Stdout() duties <- performDuties(hours) stdout.println(`Employee ${this}:`) stdout.println(` - ${duties}\n`) payroll.trackWork(hours) address() <- rtn as Address: address operator $ as pure <- rtn as String: `${$id} - ${name}` default operator ? AddressBook employeeAddresses as Dict of (Integer, Address): { 1: Address(street: "121 Admin Rd.", street2: String(), city: "Concord", state: "NH", zipcode: "03301"), 2: Address(street: "67 Paperwork Ave", street2: String(), city: "Manchester", state: "NH", zipcode: "03101"), 3: Address(street: "15 Rose St", street2: "Apt. B-1", city: "Concord", state: "NH", zipcode: "03301"), 4: Address(street: "39 Sole St.", street2: String(), city: "Concord", state: "NH", zipcode: "03301"), 5: Address(street: "99 Mountain Rd.", street2: String(), city: "Concord", state: "NH", zipcode: "03301") } getEmployeeAddress() -> employeeId as Integer <- rtn as Address: employeeAddresses.getOrDefault(employeeId, Address()) default operator ? ProductivitySystem roles as Dict of (String, IRole): { "manager": ManagerRole(), "secretary": SecretaryRole(), "sales": SalesRole(), "factory": FactoryRole() } getRole() -> roleId as String <- rtn as IRole: roles.getOrDefault(roleId, FactoryRole()) track() -> employees as List of Employee hours as Integer stdout <- Stdout() stdout.println("Tracking Employee Productivity") stdout.println("==============================") for employee in employees employee.work(hours) stdout.print("\n") default operator ? // Employee roles ManagerRole with trait of IRole override performDuties() -> hours as Integer <- activity as String: `screams and yells for ${hours} hours.` default operator ? SecretaryRole with trait of IRole override performDuties() -> hours as Integer <- activity as String: `does paperwork for ${hours} hours.` default operator ? SalesRole with trait of IRole override performDuties() -> hours as Integer <- activity as String: `expends ${hours} hours on the phone.` default operator ? FactoryRole with trait of IRole override performDuties() -> hours as Integer <- activity as String: `manufactures gadgets for ${hours} hours.` default operator ? //Payroll classes PayrollSystem employeePolicies as Dict of (Integer, PayrollPolicy): { 1: SalaryPolicy(3000#USD), 2: SalaryPolicy(1500#USD), 3: CommissionPolicy(1000#USD, 100#USD), 4: HourlyPolicy(15#USD), 5: HourlyPolicy(9#USD) } getPolicy() -> employeeId as Integer <- rtn as PayrollPolicy: employeePolicies.getOrDefault(employeeId, HourlyPolicy(9#USD)) calculatePayroll() -> employees as List of Employee stdout <- Stdout() stdout.println("Calculating Payroll") stdout.println("===================") for employee in employees stdout.println(`Payroll for: ${employee}`) stdout.println(`- Check amount: ${employee.calculatePayroll()}`) if employee.address()? stdout.println(`- Sent to:\n${employee.address()}`) stdout.print("\n") default operator ? PayrollPolicy with trait of IPayrollCalculator as abstract hoursWorked as Integer: 0 override trackWork() -> hours as Integer hoursWorked += hours hoursWorked() <- hours as Integer: hoursWorked default operator ? HourlyPolicy extends PayrollPolicy hourRate as Money: Money() HourlyPolicy() -> hourRate as Money this.hourRate = hourRate override calculatePayroll() <- amount as Money: hourRate * hoursWorked() default operator ? SalaryPolicy extends PayrollPolicy as open weeklySalary as Money: Money() SalaryPolicy() -> weeklySalary as Money this.weeklySalary = weeklySalary override calculatePayroll() <- amount as Money: weeklySalary default operator ? CommissionPolicy extends SalaryPolicy commissionPerSale as Money: Money() CommissionPolicy() -> weeklySalary as Money commissionPerSale as Money super(weeklySalary) this.commissionPerSale = commissionPerSale override calculatePayroll() <- amount as Money: super.calculatePayroll() amount += commission() private commission() <- amount as Money: commissionPerSale/5.0 * hoursWorked() default operator ? // Part of the driver to create the employees EmployeeDatabase productivitySystem as ProductivitySystem: ProductivitySystem() payrollSystem as PayrollSystem: PayrollSystem() addresses as AddressBook: AddressBook() employees() <- rtn as List of Employee: List() //You can use named variable if you prefer rtn += createEmployee(id: 1, name: "Mary Poppins", theRole: "manager") rtn += createEmployee(2, "John Smith", "secretary") rtn += createEmployee(3, "Kevin Bacon", "sales") rtn += createEmployee(4, "Jane Doe", "factory") rtn += createEmployee(5, "Robin Williams", "secretary") private createEmployee() -> id as Integer name as String theRole as String <- //a bit of a mouth full, but you can use named parameters like this if you wish. rtn as Employee: Employee( id: id, name: name, address: addresses.getEmployeeAddress(id), role: productivitySystem.getRole(theRole), payroll: payrollSystem.getPolicy(id) ) default operator ? defines program Demonstration() productivitySystem <- ProductivitySystem() payrollSystem <- PayrollSystem() employeeDatabase <- EmployeeDatabase() employees <- employeeDatabase.employees() productivitySystem.track(employees, 40) payrollSystem.calculatePayroll(employees)
Download composition_employees.ek9
The driver components
The 'Demonstration' program and the 'EmployeeDatabase', 'ProductivitySystem' and 'PayrollSystem' classes are only there to show the solution working. The output is below; it matches the Python example, except that Money shows the currency.
The Output
Tracking Employee Productivity ============================== Employee 1 - Mary Poppins: - screams and yells for 40 hours. Employee 2 - John Smith: - does paperwork for 40 hours. Employee 3 - Kevin Bacon: - expends 40 hours on the phone. Employee 4 - Jane Doe: - manufactures gadgets for 40 hours. Employee 5 - Robin Williams: - does paperwork for 40 hours. Calculating Payroll =================== Payroll for: 1 - Mary Poppins - Check amount: 3000.00#USD - Sent to: 121 Admin Rd. Concord NH 03301 Payroll for: 2 - John Smith - Check amount: 1500.00#USD - Sent to: 67 Paperwork Ave Manchester NH 03101 Payroll for: 3 - Kevin Bacon - Check amount: 1800.00#USD - Sent to: 15 Rose St Apt. B-1 Concord NH 03301 Payroll for: 4 - Jane Doe - Check amount: 600.00#USD - Sent to: 39 Sole St. Concord NH 03301 Payroll for: 5 - Robin Williams - Check amount: 360.00#USD - Sent to: 99 Mountain Rd. Concord NH 03301
Summary
The key points for this example are:
- The trait concept in EK9 facilitates composition
- It is necessary to actually define the traits
- The abstraction and structure has to be explicit in EK9
- Constructs record, trait and class provide different structure constructs
- 'Employee' methods calculatePayroll and performDuties are automatically delegated, so they need no implementation
Why EK9 does it this way. EK9 takes its layout and syntax from Python, and its structure and explicit extension/implementation from Java, C#, Scala and C++. Matching method signatures are not enough: a trait must be declared and used. That is explicit and type safe: you cannot pass the wrong type of object because its method signatures happen to match.
SOLID
The example follows the SOLID principles well:
- Single Responsibility - Each Role is isolated, each PayrollPolicy is isolated
- Open-closed principle - the behaviour in roles and payroll policies can change without modifying their bases
- Liskov substitution principle - Any of the roles and payroll policies can be replaced without altering the correctness of the program
- Interface segregation principle - IRole and IPayrollCalculator are separate interfaces/traits and so are segregated
- Dependency inversion principle - The Employee class is only aware of IRole and IPayrollCalculator; at construction it is given the implementation it is to use.
Advantages
A change to any of these implementations touches a small part of the code, with direct and obvious effects. It is obvious where to change 'manager' pay or 'secretary' activity text, and address changes go in the 'Address Book'.
Issues
- Output is hardwired to 'Stdout'.
- Adding employees, changing an employee's job role or adding a job role is not addressed. Worse, the design does not help you build these: it is not obvious where or how they would go.
- 'EmployeeDatabase' is directly coupled to 'ProductivitySystem', and also to 'PayrollSystem' and 'AddressBook'.
- 'ProductivitySystem' has mixed responsibilities. Is it only for tracking productivity, or also an HR system that defines which roles a notional title has? That should be separated out.
- 'AddressBook' is used only by 'EmployeeDatabase', which is fine in itself, but 'EmployeeDatabase' is tied to this implementation, so it cannot easily be swapped for a database, a file or a remote service.
This is now software design rather than the EK9 language, but later sections use these issues to show other EK9 features. The aim is not to criticise the original example (thank you, Isaac Rodriguez, for taking the time to produce such a good one): most of these issues are in the 'driver' infrastructure, which was not its subject.
Revisiting
The Components section refines this example further, and dependency injection discusses more alterations and alternatives.
Next Steps
The next section covers inheritance. Components will also interest you: they enable composition too, into larger building blocks.