Components in EK9

Components go beyond Composition by adding more structural constructs that can be injected. Adding components is only really valuable once the scale of the software gets to a 'reasonable' size.

Before showing this example source code, it might be worth taking a look at this 'tag cloud', just to see if you can quickly understand what the example is about.

This site has wandered off from just giving details into EK9 syntax and has started to discuss design. That's because these additional constructs are all about design, architecture and software engineering.

TAG Cloud

Tags

Reason the TAG Cloud was shown first

There is a point to showing the TAG cloud. That is to emphasize the value of strong typing; not just to ensure code reliability and reduce the chance of passing invalid parameters. The value is first and foremost for the human reader. This point tends to be lost in many programming languages. Just being able to write very short, terse and in some cases obfuscated code; might reduce key strokes, but can make it hard to understand the main reason for the code.

While it's true to say you can see 'string', 'private' and 'println'. The main content is that of the problem domain. Meaningful names that should leap out and inform you with a single glimpse that this example is all about business, work, employees and salaries.

This is the real value of creating strong types, traits and well named abstractions. Not just because it makes your code more flexible, safer, extensible and testable. You instantly get a mile high overview of the code that is about to follow.

To demonstrate the use of components it has been necessary to add quite a bit more functionality to the example from composition. It has also been assumed that the software being developed has to be very extensible. The example that follows accomplishes this by enabling many of the classes and components to be replaceable. For a real development, it would probably not be wise to make every single part of a solution replaceable as this can lead to naming 'overload'.

Composition background

It is only when software gets quite a lot larger than this and needs to be much more flexible that components/applications start to become really useful.

There are other aspects to note about this code (check and compare the code from composition).

Other simplifications.

Components

The new items that have been added are components and applications.

Application

A 'DemoApp' has been created and this only focuses on the 'wiring' of what classes and components should be used in combination. If you've used 'Spring' this is similar to that wiring. The intention in EK9 is however to keep that wiring to a minimum and only inject at the highest level (which is why only components can be injected and classes cannot). By constraining and limiting components to being vessels for composition and not as flexible/useful as classes; but not allowing classes to be injected, EK9 reduces the opportunities to 'abuse' Dependency Injection.

Typically; you should inject 'Factories', 'Strategies' and other major large sub systems (like 'HRSystem' for example). Also try to keep that injection as high in the code structure as possible. Low utility functions/classes should really have 'items' passed in at construction or on method calls. Injecting everywhere; will make your code less reusable/testable and harder to refactor.

The Program

Finally (the point of all this), unlike the original program; where it not only had the business logic of what to do, but also decided on what parts would be used to accomplish that business logic. This new program only decides on the use of 'DemoApp' (abstraction), but is unlinked from the implementation details of what will be plugged in where.

The key point here is that the program 'Demonstration' has 'HRSystem' and 'OutputInterface' injected. The program then only needs to focus on the business logic.

This approach is typical when employing the 'Single Responsibility Principle'.

#!ek9
defines module introduction

  defines type

    EmployeeId as Integer constrain as
      > 0

    Name as String constrain as
      matches /^[a-zA-Z -]+$/

    RoleName
      manager
      secretary
      sales
      factory

  defines record

    Address
      street as String: String()
      street2 as String: String()
      city as String: String()
      state as String: String()
      zipcode as String: String()

      operator ?
        <- rtn as Boolean: street? and city? and state? and zipcode?

      operator $
        <- rtn as String: ""
        rtn += street
        if street2?
          rtn += "\n" + street2
        rtn += "\n" + city + " " + state+ " " + zipcode

  defines trait

    Role
      performDuties()
        -> hours as Integer
        <- activity as String

      operator ?
        <- rtn as Boolean: true

    PayrollCalculator
      resetHoursWorked()

      trackWork()
        -> hours as Integer

      calculatePayroll()
        <- amount as Money
      
      operator ?
        <- rtn as Boolean
        
    RoleDB
      findRole()
        -> roleName as RoleName
        <- rtn as Role
    
    AddressDB
      findEmployeeAddress()
        -> employeeId as EmployeeId
        <- rtn as Address
      
      addEmployeeAddress()
        ->
          employeeId as EmployeeId
          address as Address        
          
    PayrollPolicyDB
      findPayrollPolicy()
        -> employeeId as EmployeeId
        <- rtn as PayrollPolicy
      
      addPayrollPolicy()
        ->
          employeeId as EmployeeId
          payrollPolicy as PayrollPolicy
              
    EmployeeDB
      employed()
        <- employedStaff as List of Employment

      changeRole()
        ->
          employeeId as EmployeeId
          newRole as RoleName

      addEmployee()
        ->
          person as Person
          role as RoleName
          payrollPolicy as PayrollPolicy

  defines class

    PayrollPolicy with trait of PayrollCalculator as abstract
      hoursWorked as Integer: 0

      override resetHoursWorked()
        hoursWorked = 0

      override trackWork()
        -> hours as Integer
        hoursWorked += hours

      hoursWorked()
        <- hours as Integer: hoursWorked

      override operator ?
        <- rtn as Boolean: hoursWorked?

    HourlyPolicy extends PayrollPolicy
      hourRate as Money: Money()

      HourlyPolicy()
        -> hourRate as Money
        this.hourRate = hourRate

      override calculatePayroll()
        <- amount as Money: hourRate * hoursWorked()

    SalaryPolicy extends PayrollPolicy as open
      weeklySalary as Money: Money()

      SalaryPolicy()
        -> weeklySalary as Money
        this.weeklySalary = weeklySalary

      override calculatePayroll()
        <- amount as Money: weeklySalary

    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() + commission()

      private commission()
        <- amount as Money: commissionPerSale/5.0 * hoursWorked()

    ManagerRole with trait of Role
      override performDuties()
        -> hours as Integer
        <- activity as String: "screams and yells for " + $hours + " hours."

    SecretaryRole with trait of Role
      override performDuties()
        -> hours as Integer
        <- activity as String: "does paperwork for " + $hours + " hours."

    SalesRole with trait of Role
      override performDuties()
        -> hours as Integer
        <- activity as String: "expends " + $hours + " hours on the phone."

    FactoryRole with trait of Role
      override performDuties()
        -> hours as Integer
        <- activity as String: "manufactures gadgets for " + $hours + " hours."

    Person
      name as Name: Name()
      address as Address: Address()

      Person()
        ->
          name as Name
          address as Address
        this.name: name
        this.address: address

      name()
        <- rtn as Name: name

      address()
        <- rtn as Address: address

      operator ?
        <- rtn as Boolean: name? and address?

      operator $
        <- rtn as String: name

    Employee
      id as EmployeeId: EmployeeId()
      person as Person: Person()

      Employee()
        ->
          id as EmployeeId
          person as Person
        assert id? and person?
        this.id: id
        this.person: person

      id()
        <- rtn as EmployeeId: id
        
      person()
        <- rtn as Person: person

      operator ?
        <- rtn as Boolean: id? and person?
        
      operator $
        <- rtn as String: $id + " - " + $person

    Employment with trait of Role by role, PayrollCalculator by payroll
      employee as Employee: Employee()
      role as Role?
      payroll as PayrollCalculator?

      Employment()
        ->
          employee as Employee
          role as Role
          payroll as PayrollCalculator
        assert employee? and role? and payroll?  
        this.employee: employee
        this.role: role
        this.payroll: payroll

      changeRole()
        -> newRole as Role
        role: newRole

      work()
        -> amountWorked as Duration
        <- dutiesPerformed as String: String()
        //Note that duration is converted to hours from this point on.
        assert amountWorked.hours() >= 0
        
        dutiesPerformed: performDuties(amountWorked.hours())
        payroll.trackWork(amountWorked.hours())

      employeeAddress()
        <- rtn as Address: employee.person().address()
        
      employee()
        <- rtn as Employee: employee
      
      override operator ?
        <- rtn as Boolean: role? and payroll?
          
      operator $
        <- rtn as String: $employee

    InMemoryRoleDB with trait of RoleDB
      roles as Dict of (RoleName, Role): {
        RoleName.manager: ManagerRole(),
        RoleName.secretary: SecretaryRole(),
        RoleName.sales: SalesRole(),
        RoleName.factory: FactoryRole()
        }

      override findRole()
        -> roleName as RoleName
        <- rtn as Role
        theRole <- roles.get(roleName)
        assert theRole?
        rtn: theRole.get()

    InMemoryAddressDB with trait of AddressDB
      employeeAddresses as Dict of (EmployeeId, Address): {
        EmployeeId(1): Address("121 Admin Rd.", String(), "Concord", "NH", "03301"),
        EmployeeId(2): Address("67 Paperwork Ave", String(), "Manchester", "NH", "03101"),
        EmployeeId(3): Address("15 Rose St", "Apt. B-1", "Concord", "NH", "03301"),
        EmployeeId(4): Address("39 Sole St.", String(), "Concord", "NH", "03301"),
        EmployeeId(5): Address("99 Mountain Rd.", String(), "Concord", "NH", "03301")
        }
        
      override findEmployeeAddress()
        -> employeeId as EmployeeId
        <- rtn as Address: Address()
        address <- employeeAddresses.get(employeeId)
        assert address?
        rtn: address.get()
      
      override addEmployeeAddress()
        ->
          employeeId as EmployeeId
          address as Address
        employeeAddresses += DictEntry(employeeId, address)
    
    InMemoryPayrollPolicyDB with trait of PayrollPolicyDB
      employeePayrollPolicies as Dict of (EmployeeId, PayrollPolicy): {
        EmployeeId(1): SalaryPolicy(3000#USD),
        EmployeeId(2): SalaryPolicy(1500#USD),
        EmployeeId(3): CommissionPolicy(1000#USD, 100#USD),
        EmployeeId(4): HourlyPolicy(15#USD),
        EmployeeId(5): HourlyPolicy(9#USD)
        }
        
      override findPayrollPolicy()
        -> employeeId as EmployeeId
        <- rtn as PayrollPolicy
        policy <- employeePayrollPolicies.get(employeeId)
        assert policy?
        rtn: policy.get()
      
      override addPayrollPolicy()
        ->
          employeeId as EmployeeId
          payrollPolicy as PayrollPolicy
        employeePayrollPolicies += DictEntry(employeeId, payrollPolicy)
                                   
    InMemoryEmployeeDB with trait of EmployeeDB, RoleDB by roleDB, AddressDB by addressDB, PayrollPolicyDB by payrollPolicyDB
      roleDB as RoleDB?
      addressDB as AddressDB?
      payrollPolicyDB as PayrollPolicyDB?
      
      employedStaff as Dict of (EmployeeId, Employment): Dict()
      nextEmployeeId as EmployeeId: EmployeeId(6)

      private InMemoryEmployeeDB()        
        assert false  
      
      InMemoryEmployeeDB()
        ->
          roleDB as RoleDB
          addressDB as AddressDB
          payrollPolicyDB as PayrollPolicyDB
        this.roleDB: roleDB
        this.addressDB: addressDB
        this.payrollPolicyDB: payrollPolicyDB        
        populateEmployedStaff()
        
      private populateEmployedStaff()
        createEmployment(EmployeeId(1), Name("Mary Poppins"), RoleName.manager)
        createEmployment(EmployeeId(2), Name("John Smith"), RoleName.secretary)
        createEmployment(EmployeeId(3), Name("Kevin Bacon"), RoleName.sales)
        createEmployment(EmployeeId(4), Name("Jane Doe"), RoleName.factory)
        createEmployment(EmployeeId(5), Name("Robin Williams"), RoleName.secretary)
        
      override employed()
        <- employedStaff as List of Employment: List()
        values <- this.employedStaff.values()
        cat values > employedStaff

      override changeRole()
        ->
          employeeId as EmployeeId
          newRole as RoleName
        employment <- this.employedStaff.get(employeeId)        
        role <- findRole(newRole)        
        assert employment? and role?
        employment.get().changeRole(role)

      override addEmployee()
        ->
          person as Person
          role as RoleName
          payrollPolicy as PayrollPolicy
        newEmployeeId <- nextEmployeeId()        

        addEmployeeAddress(newEmployeeId, person.address())
        addPayrollPolicy(newEmployeeId, payrollPolicy)
        
        employment <- Employment(Employee(newEmployeeId, person), findRole(role), payrollPolicy)
        addEmployment(employment)

      private nextEmployeeId()
        <- id as EmployeeId: EmployeeId()        
        id :=: nextEmployeeId
        nextEmployeeId++
        
      private createEmployment()
        ->
          id as EmployeeId
          name as Name
          theRole as RoleName        
        address <- findEmployeeAddress(id)
        employeeRole <- findRole(theRole)
        payrollPolicy <- findPayrollPolicy(id)
        addEmployment(Employment(Employee(id, Person(name, address)), employeeRole, payrollPolicy))

      private addEmployment()
        -> employment as Employment
        employedStaff += DictEntry(employment.employee().id(), employment)
          
    ProductivitySystem with trait of StringOutput by output
      output as StringOutput?

      private ProductivitySystem()
        assert false
        
      ProductivitySystem()
        -> output as StringOutput
        this.output: output

      track()
        ->
          employed as List of Employment
          amountWorked as Duration
        println("Tracking Employee Productivity")
        println("==============================")
        for employment in employed
          dutiesPerformed <- employment.work(amountWorked)
          println("Employee " + $employment + ": ")
          println(" - " + dutiesPerformed)
          println("")
        println("")

    PayrollSystem with trait of StringOutput by output
      output as StringOutput?

      private PayrollSystem()
        assert false
        
      PayrollSystem()
        -> output as StringOutput
        this.output: output

      calculatePayroll()
        -> employed as List of Employment
        println("Calculating Payroll")
        println("===================")
        for employment in employed
          println("Payroll for: " + $employment)
          println("- Check amount: " + $employment.calculatePayroll())
          if employment.employeeAddress()?
              println("- Sent to:")
              println($employment.employeeAddress())
          println("")

  defines component

    OutputInterface as abstract
      output()
        <- outputTo as StringOutput: Stdout()

    StdoutInterface is OutputInterface

    StderrInterface is OutputInterface
      override output()
        <- outputTo as StringOutput: Stderr()

    HRSystem as abstract
      newWorkingWeek() as abstract

      calculatePayroll() as abstract
        -> outputInterface as OutputInterface

      trackWork() as abstract
        ->
          outputInterface as OutputInterface
          amountWorked as Duration

      changeRole() as abstract
        ->
          employeeId as EmployeeId
          newRole as RoleName

      newStarter() as abstract
        ->
          person as Person
          role as RoleName
          payrollPolicy as PayrollPolicy

    SimpleHRSystem is HRSystem
      employeeDb as EmployeeDB

      SimpleHRSystem()
        assert false
        
      SimpleHRSystem()
        -> employeeDb as EmployeeDB
        this.employeeDb: employeeDb

      override newWorkingWeek()
        employed <- employeeDb.employed()
        for employment in employed
          employment.resetHoursWorked()

      override calculatePayroll()
        -> outputInterface as OutputInterface
        payrollSystem <- PayrollSystem(outputInterface.output())
        payrollSystem.calculatePayroll(employeeDb.employed())

      override trackWork()
        ->
          outputInterface as OutputInterface
          amountWorked as Duration
        productivitySystem <- ProductivitySystem(outputInterface.output())
        productivitySystem.track(employeeDb.employed(), amountWorked)

      override changeRole()
        ->
          employeeId as EmployeeId
          newRole as RoleName
        assert employeeId? and newRole?        
        employeeDb.changeRole(employeeId, newRole)

      override newStarter()
        ->
          person as Person
          role as RoleName
          payrollPolicy as PayrollPolicy
        assert person? and role? and payrollPolicy?
        employeeDb.addEmployee(person, role, payrollPolicy)

  defines application

    //Separation of what components/classes to use rather than what to do with them
    DemoApp    
      employeeDb <- InMemoryEmployeeDB(InMemoryRoleDB(), InMemoryAddressDB(), InMemoryPayrollPolicyDB())

      register SimpleHRSystem(employeeDb) as HRSystem
      register StdoutInterface() as OutputInterface

  defines program
  
    //Separation of what functionality to provide with configuration supplied.
    Demonstration() with application of DemoApp
      hrSystem as HRSystem!
      outputInterface as OutputInterface!

      hrSystem.trackWork(outputInterface, PT40H)
      hrSystem.calculatePayroll(outputInterface)

      hrSystem.newWorkingWeek()
      //Short week.
      hrSystem.trackWork(outputInterface, PT32H)
      hrSystem.calculatePayroll(outputInterface)
//EOF

There is some additional functionality in the example above, but the main point has been to make the whole system very pluggable. Some developers may start to consider this 'over engineering'. This would depend on how large or long this software is intended to last.

You can start to see two parallel tracks going on here, the first is the abstract nature (designing with interfaces/traits); then there are the concrete implementations of those interfaces/traits. The important part in this is that there is decoupling everywhere.

This decoupling is not without its downside, deep inside a method the code/developer will only 'know' that they have some type of object that conforms to an interface/trait. But that's the point! NO you cannot 'cast' it or use instanceof; those concepts to not exist. But you could use a dispatcher method.

Discussion

While some new functionality has been added, there is a significant amount more code over the previous example. This is mainly in defining the traits and also separating out Single Points of Responsibility. The additional strong typing of 'role name' 'employee id' and 'name' also adds to the increased size.

But look at the 'tag cloud' again, there is value here. As a contrast; it is technically possible to hold the 'Address' as just a 'List of String'. But that's it; it is then only a 'List of String', it has not been abstracted into anything meaningful in the problem domain.

EK9 provides the constructs and mechanisms to be able to code in a very abstract manner with lots of decoupling; or in a very basic manner. Depending on your project/product, its lifetime, use, size and your teams experience you'll have to find the right blend of abstraction.

Summary

While components seem a bit limited in comparison to classes and even traits, this is by design. They are designed to act as an enabler for more functionality in terms of Dependency Injection and Aspect Oriented Programming.

These additional topics are getting much more advanced and have very limited and finite roles to play. But when you need to deal with many cross cutting concerns; components can be employed.

Some architects/developers value AOP and others believe it adds complexity and makes code more fragile. EK9 includes a very simple version of AOP that just gives the developer coarse grain knowledge that a specific method on a component was called.

There is no fine grained access to enable the selection specific methods of specific names (that is too fragile and breaks when code is refactored). Use AOP with care. If fine grained detail is needed then classes and traits with composition and delegation is the way to go.

Next Steps

Continuing with components but adding Dependency Injection and Aspect Oriented Programming is the next logical step in terms of architectural macro structure in software.