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
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 keystrokes, 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'.
There is an important point here that should be highlighted. Whilst abstractions do provide value, they also increase cognitive load. The very fact the developer has to dereference types and work in a more abstract manner; increases the load for that developer.
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).
- Employment IDs are now strongly typed
- Amount worked is typed as a Duration and converted to hours when needed
- The type 'Name' is constrained to match a regular expression
- The type of 'role' has been migrated from just String values to an Enumeration and called 'RoleName'
- The 'I' has been dropped from traits
- Each part of the 'data store' has been modelled as a 'DB' trait to enable alternative implementations
- 'Role', 'PayrollPolicy' and 'PayrollPolicy' and subclasses have only had minor changes
- 'Employee' has been pulled apart! Now 'Person', 'Employee' and 'Employment' are used in composition
- The employee data, role data, address data and salary policies have all been pulled into separate 'in memory databases'
- 'EmployeeDatabase' has been replaced by 'InMemoryEmployeeDB'
Other simplifications.
- 'ProductivitySystem' only tracks amounts worked
- 'PayrollSystem' only calculates payroll
- Several large methods have been decomposed
Components
The new items that have been added are components and applications.
- Use of 'Stdout' being hardwired has been replaced by using an 'OutputInterface' component
- There are two concrete implementations of this 'OutputInterface'
- The concept of an 'HRSystem' has been introduced in abstract form
- A 'SimpleHRSystem' concrete implementation has been provided
- The 'EmployeeDB' the 'SimpleHRSystem' has to use is provided at construction (inversion of control)
- Business level type methods have been provided in the 'HRSystem' and the 'OutputInterface' is provided for output
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, and 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 the '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'. The example code is a little longer now, but much clearer in terms of types and what functionality is implemented by which component.
#!ek9
defines module introduction
defines type
EmployeeId as Integer constrain
> 0
Name as String constrain as
matches /^[a-zA-Z -]+$/
RoleName
manager
secretary
sales
factory
defines record
Address
street <- String()
street2 <- String()
city <- String()
state <- String()
zipcode <- String()
operator ? as pure
<- rtn as Boolean: street? and city? and state? and zipcode?
operator $ as pure
<- 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 ? as pure
<- rtn as Boolean: true
PayrollCalculator
resetHoursWorked()
trackWork()
-> hours as Integer
calculatePayroll()
<- amount as Money?
operator ? as pure
<- 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 <- 0
override resetHoursWorked()
hoursWorked = 0
override trackWork()
-> hours as Integer
hoursWorked += hours
hoursWorked()
<- hours as Integer: hoursWorked
override operator ? as pure
<- rtn as Boolean: hoursWorked?
HourlyPolicy extends PayrollPolicy
hourRate <- Money()
HourlyPolicy()
-> hourRate as Money
this.hourRate = hourRate
override calculatePayroll()
<- amount as Money: hourRate * hoursWorked()
SalaryPolicy extends PayrollPolicy as open
weeklySalary <- Money()
SalaryPolicy()
-> weeklySalary as Money
this.weeklySalary = weeklySalary
override calculatePayroll()
<- amount as Money: weeklySalary
CommissionPolicy extends SalaryPolicy
commissionPerSale <- 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 <- Name()
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 ? as pure
<- rtn as Boolean: name? and address?
operator $ as pure
<- rtn as String: name
Employee
id <- EmployeeId()
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 ? as pure
<- rtn as Boolean: id? and person?
operator $ as pure
<- rtn as String: $id + " - " + $person
Employment with trait of Role by role, PayrollCalculator by payroll
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 ? as pure
<- rtn as Boolean: role? and payroll?
operator $ as pure
<- 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 not true
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()
<- staff as List of Employment: List()
values <- this.employedStaff.values()
cat values > staff
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 not true
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 not true
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 not true
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 crosscutting 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 macrostructure in software.