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.

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:

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:

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:

  1. Function delegates.
  2. Traits: a simple, well-defined public interface that can hold default functionality and abstract method signatures.
  3. Multiple inheritance of traits: a Façade is a new trait that has the traits of every feature it needs.
  4. 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
Inheritance

The inheritance design is complex, and uses multiple inheritance.

Composition
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)

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:

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:

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

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.