Dependency Injection in EK9
Components and Applications introduced dependency injection and inversion of control, with an example that separates 'wiring' components from 'using' them through an abstract base.
The main value of inversion of control is that separation: of 'what' is used from 'when' it is used. Dependency injection is the mechanics of delivering it.
Inversion of Control
The inversion means the function/class/method no longer decides which object it creates to make a call. Either the object is passed to it, or the object is 'injected'. Either way, the function/class/method neither controls the specific type of the object nor creates it. This depends on polymorphism.
In this part of the example (shown in full later), TimerAspect does not decide the concrete type of Clock it uses for timing; the Clock is passed to its constructor.
TimerAspect extends Aspect clock as Clock? default private TimerAspect() TimerAspect() -> clock as Clock this.clock: clock // ... advice default operator ?
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
The application thirdApp has the control, and decides to use SystemClock. In EK9 it is the applications that decide the wiring: which components to use.
thirdApp register FileLogger() as ILogger register Solution3() as BaseConfiguration with aspect of TimerAspect(SystemClock())
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
This is not dependency injection: nothing above is injected. Only the control is inverted.
Why EK9 does it this way. The 'register' syntax for components and aspects is wordier than most EK9 syntax, by design. You should use components and applications far less often than functions, classes, traits and records, so you should not need many of these large, significant constructs. The syntax reflects their size and significance.
Dependency Injection
Instead of passing a component to a function/class/method or another component, you can have it injected automatically, when it has an abstract base component.
Use this sparingly and with care. Too much of it causes 'hidden' coupling, limits refactoring, and makes pure functions and methods very hard to write.
LoggingAspect extends Aspect loggingLevel as String? LoggingAspect() as pure this("WARN") LoggingAspect() as pure -> level as String this.loggingLevel :=? level override beforeAdvice() -> joinPoint as JoinPoint <- rtn as PreparedMetaData: PreparedMetaData(joinPoint) //Will be injected logger as ILogger! logger.log(loggingLevel, `Before ${joinPoint.componentName()} ${joinPoint.methodName()}`) override afterAdvice() -> preparedMetaData as PreparedMetaData joinPoint <- preparedMetaData.joinPoint() //Will be injected logger as ILogger! logger.log(loggingLevel, `After ${joinPoint.componentName()} ${joinPoint.methodName()}`) default operator ?
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
Above, the method beforeAdvice needs to log messages. There were three ways to give it a logger:
- LoggingAspect keeps control, and decides which implementation of ILogger to use.
- An ILogger is passed in when the LoggingAspect is constructed.
- LoggingAspect expects an implementation of ILogger to be injected. This is the one used.
It then calls the log method through the abstract ILogger component. To have a component injected, put ! after the type when you declare the variable.
This part shows how a concrete implementation of ILogger is made available:
firstApp register FileLogger() as ILogger register Solution1() as BaseConfiguration with aspect of TimerAspect(SystemClock()), LoggingAspect("DEBUG")
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
The application firstApp registers a new FileLogger as the ILogger. That tells EK9 that FileLogger can be injected wherever an ILogger is expected, and EK9 sets each such variable to it. The compiler checks that every injection a program can reach has a registration in the program's application (E08210), and that a program using injection has an application at all (E08220).
If you have used 'Spring' in Java, this is a much cut-down version of it, by design. Use it sparingly and with care: it can inhibit reuse.
Limitations
- Only components can be injected (E08160), and the declared type must be an abstract base component (E08150).
- Injection cannot be used in a pure scope (E08140).
Restrict the number of components you use and the amount of injection. In the example above, logger as ILogger! is hidden inside a method. That makes the whole class less portable, harder to refactor and less reusable, and it cannot be pure: there is a hidden component being injected.
Avoid the mechanism where you can, by passing in parameters.
So why add this feature?
- Wiring subsystems in different ways. Sometimes major subsystems and components need to be wired together in different ways, while the bulk of the code stays the same and works through traits or abstract base classes/components. Inversion of control and dependency injection solve that in specific situations.
-
Aspect Oriented Programming, in a limited form, for 'crosscutting concerns' such as:
- Logging
- Timing of calls
- Transactional boundaries
- Permission processing
There are other reasons, but those are the main ones.
A Scenario
A scenario shows why EK9 includes inversion of control, dependency injection and aspect oriented programming. A 'system' has these major components:
- Client Record System
- Management Reporting System
- Product Purchase System
Each is built from several other components. The key point: every access to a method on these components must be logged. During development and in 'staging', you also need metrics on how long each component method call took.
A real solution would also need 'transaction' boundaries, and would limit user access through LDAP or similar. This scenario covers logging and timing.
You need to do the 'same thing' (log, time) on every method call on every component. You do not care which method it is, or the types and values of its parameters; you only need to know the method was called. That is what 'Aspects' are for.
Why EK9 does it this way. Other aspect oriented programming solutions give a little too much detail. That looks useful, but it tempts developers to alter processing in the aspect, which leads to defects that are very hard to find and fix. EK9 gives you the minimum: enough to know a method was called. See Design note: why EK9 limits injection and aspects.
Solutions
Error Prone and not Scalable
Provide a logger and timer in a 'base class/component', and have the developer call them by hand in each new method. This is not viable: it is error-prone and relies on Inheritance.
Too manual, but necessary if details are needed
Use EK9 classes with traits and delegation: the new class logs and times, then calls the delegate for the actual processing. This is reasonable if you want every parameter's details and will write a lot of boilerplate code.
Using Aspects
You only need to know that the calls happened and how long they took, not their parameters, so use 'Aspects' and components. To keep the example short, it uses one component that notionally holds a 'FileStoreName' as configuration. Calls to methods on that configuration component must be logged, and in some cases timed.
Explanation
- There are three BaseConfiguration solutions; in practice they might differ in performance or cost.
- The class ConfigHandler has the 'wired in' BaseConfiguration injected.
- The function checkConfigHandler creates a ConfigHandler and calls 'showConfigDetails()', which calls 'getFileStoreName()' on the wired-in component.
The Applications
There are four applications:
noAspectApp
noAspectApp register Solution1() as BaseConfiguration
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
No logging or timing, and Solution1 as the BaseConfiguration. Program Program0 uses it, to check the functionality works. It prints:
Program0 functionality Will check config handler to see if file store name is available MainStore
firstApp
firstApp register FileLogger() as ILogger register Solution1() as BaseConfiguration with aspect of TimerAspect(SystemClock()), LoggingAspect("DEBUG")
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
Solution1 again, with both the logging and the timing 'Aspects' (described after the full listing). The time a call takes differs from run to run, so the output shows a Millisecond placeholder in its place:
Program1 functionality
Will check config handler to see if file store name is available
DEBUG: Before site.dependencyInjection.componentsAndAspects::Solution1 getFileStoreName
DEBUG: After site.dependencyInjection.componentsAndAspects::Solution1 getFileStoreName
INFO: {{Millisecond}} Milliseconds for site.dependencyInjection.componentsAndAspects::Solution1 getFileStoreName
MainStore
secondApp
secondApp register FileLogger() as ILogger register Solution2() as BaseConfiguration with aspect of LoggingAspect("WARN")
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
Solution2, with only the logging 'Aspect'. The logging level is 'WARN': secondApp took control of that when it constructed the logging 'Aspect'. It prints:
Program2 functionality Will check config handler to see if file store name is available WARN: Before site.dependencyInjection.componentsAndAspects::Solution2 getFileStoreName WARN: After site.dependencyInjection.componentsAndAspects::Solution2 getFileStoreName SecondaryStore
thirdApp
thirdApp register FileLogger() as ILogger register Solution3() as BaseConfiguration with aspect of TimerAspect(SystemClock())
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
Solution3, with only the timer 'Aspect'. It prints:
Program3 functionality
Will check config handler to see if file store name is available
INFO: {{Millisecond}} Milliseconds for site.dependencyInjection.componentsAndAspects::Solution3 getFileStoreName
DefaultStore
The Programs
Linked with four different applications, the four programs could behave differently. With much larger applications, you might have different programs for a range of tasks on the same application.
That is one of the main points of inversion of control and dependency injection: separate the 'wiring' of the application and its components from the program. Split the control.
The full listing
#!ek9 defines module site.dependencyInjection.componentsAndAspects defines component BaseConfiguration abstract getFileStoreName() abstract <- rtn as String? default operator ? Solution1 is BaseConfiguration override getFileStoreName() as <- rtn String: "MainStore" Solution2 extends BaseConfiguration override getFileStoreName() as <- rtn as String: "SecondaryStore" Solution3 extends BaseConfiguration storeName as String: "DefaultStore" override getFileStoreName() as <- rtn as String: storeName default operator ? defines class ConfigHandler //This component will get injected config as BaseConfiguration! showConfigDetails() stdout <- Stdout() stdout.println(config.getFileStoreName()) default operator ? defines function checkConfigHandler() stdout <- Stdout() stdout.println("Will check config handler to see if file store name is available") configHandler <- ConfigHandler() configHandler.showConfigDetails() defines application noAspectApp register Solution1() as BaseConfiguration firstApp register FileLogger() as ILogger register Solution1() as BaseConfiguration with aspect of TimerAspect(SystemClock()), LoggingAspect("DEBUG") secondApp register FileLogger() as ILogger register Solution2() as BaseConfiguration with aspect of LoggingAspect("WARN") thirdApp register FileLogger() as ILogger register Solution3() as BaseConfiguration with aspect of TimerAspect(SystemClock()) defines program Program0 with application of noAspectApp stdout <- Stdout() stdout.println("Program0 functionality") checkConfigHandler() Program1 with application of firstApp stdout <- Stdout() stdout.println("Program1 functionality") checkConfigHandler() Program2 with application of secondApp stdout <- Stdout() stdout.println("Program2 functionality") checkConfigHandler() Program3 with application of thirdApp stdout <- Stdout() stdout.println("Program3 functionality") checkConfigHandler() defines component ILogger as abstract log() as abstract -> level as String content as String default operator ? FileLogger extends ILogger stdout as Stdout: Stdout() override log() as -> level as String content as String //Just use Stdout for logging for this example. stdout.println(`${level}: ${content}`) default operator ? defines class LoggingAspect extends Aspect loggingLevel as String? LoggingAspect() as pure this("WARN") LoggingAspect() as pure -> level as String this.loggingLevel :=? level override beforeAdvice() -> joinPoint as JoinPoint <- rtn as PreparedMetaData: PreparedMetaData(joinPoint) //Will be injected logger as ILogger! logger.log(loggingLevel, `Before ${joinPoint.componentName()} ${joinPoint.methodName()}`) override afterAdvice() -> preparedMetaData as PreparedMetaData joinPoint <- preparedMetaData.joinPoint() //Will be injected logger as ILogger! logger.log(loggingLevel, `After ${joinPoint.componentName()} ${joinPoint.methodName()}`) default operator ? TimerData extends PreparedMetaData beforeOperation as Millisecond? default private TimerData() TimerData() -> millis as Millisecond joinPoint as JoinPoint super(joinPoint) beforeOperation: millis before() <- rtn as Millisecond: beforeOperation default operator ? default operator $ default operator :=: default operator <=> TimerAspect extends Aspect clock as Clock? default private TimerAspect() TimerAspect() -> clock as Clock this.clock: clock // ... advice default operator ?
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
Logging
Normally ILogger, FileLogger and any other logging implementations would go in a separate module and source file, because they are widely reusable; typically they form part of a 'core infrastructure' layer.
Timing/Logging Aspects
TimerAspect depends on ILogger and expects it to be injected. You may not want that: in general, the more decoupled things are, the better.
The alternative is to write several logger class implementations and pass them to a Logger component/'Aspect' or a Timer 'Aspect'. Each application that creates one composes it with the right implementation.
Aspects
'beforeAdvice', 'afterAdvice', 'JoinPoint' and 'PreparedMetaData' are Aspect Oriented Programming jargon, and there is a lot of it.
Advice
The two methods called before and after the method on the component the 'Aspect' wraps.
JoinPoint
Immediately before the component method is called, EK9 creates a 'JoinPoint' holding the component name and the method name, and passes it to 'beforeAdvice'. What you do there is up to you, but you must return an object that is, or extends, 'PreparedMetaData' (TimerData is an example). You can store whatever data you need in it for 'afterAdvice'.
afterAdvice and PreparedMetaData
EK9 passes 'afterAdvice' the 'PreparedMetaData' that 'beforeAdvice' returned. TimerData holds the millisecond at which 'beforeAdvice' was called (and the 'JoinPoint'), so the Timer Aspect can work out how long the call took.
The TimerAspect's 'afterAdvice' takes a TimerData, not a 'PreparedMetaData', so it is not an override: 'afterAdvice' on Aspect is a dispatcher, and this is a new handler for it that EK9 calls when the 'PreparedMetaData' is a TimerData. Writing override on it is an error (E05110); see dispatcher methods.
Benefits
Once you have defined a set of 'Aspects', you can use them with any component. They get only the component name and method name, as text. You cannot filter to some methods and not others (too fragile), and you cannot see the parameters. These limits are deliberate: anything more tends to lead to complexity, confusion and brittle code that fails when refactored.
Separation of Concerns
Functionality is separated from logging and timing. The same 'Aspects' can be used again and again, across many components and all their methods, none of which deal with logging or timing. That gives clarity, wider reuse and reliability.
With one method to log and time, as here, it is trivial. With thousands of components/methods it works well. It is also a form of composition.
Inversion of Control
Inversion of control now covers which 'Aspect' (if any) is applied to a component, as well as which component is used. That is one of the main reasons for the application construct.
In the application you define the high-level components to use, and pass the appropriate classes and functions to them as parameters when you create them. A class or function that needs a component either has it passed in as a parameter, or uses dependency injection.
Why EK9 does it this way. The application construct is for the high-level composition of your solution. Prefer inversion of control by passing instances in at construction; use dependency injection only when you need it.
Coupling
The class ConfigHandler is unchanged, and not linked to any concrete implementation. You can change if, how and where logging is done without touching the code for the functionality in the components.
Transaction Management
EK9 has a built-in Transaction trait for ACID-compliant operations. It extends Closeable, so an implementation can be the resource of a try-with-resources block.
The Transaction trait defines three methods:
- commit() — commit the transaction (the trait's default does nothing)
- rollback() — roll back the transaction (the trait's default does nothing)
- isCommitted() as pure — returns a tri-state Boolean (unset if unknown, true/false otherwise)
Because Transaction extends Closeable, the close operator is called automatically at the end of a try block. It does not roll back: operator close is pure (an override must be declared pure too, E07500), so it cannot call rollback() (E08130), and by default it does nothing. Roll back explicitly, for example in the catch block.
Implementing a transaction:
DatabaseTransaction with trait of Transaction connectionName <- String() committed <- false DatabaseTransaction() -> connectionName as String this.connectionName: connectionName execute() -> statement as String Stdout().println(`${connectionName}: ${statement}`) override commit() execute("COMMIT") committed: true override rollback() execute("ROLLBACK") committed: false override isCommitted() as pure <- rtn as Boolean: Boolean(committed) default operator ?
Full source: dependencyInjection_transaction.ek9 - Download dependencyInjection_transaction.ek9
It is acquired as the resource of a try, so it is always closed. The work is done in an inner try, whose catch can still see the transaction and roll it back:
for amount in [100, -5] try -> txn <- DatabaseTransaction("primary-db") try updateRecords(txn, amount) txn.commit() catch -> ex as Exception txn.rollback() stdout.println(`Transaction failed: ${ex}`)
Full source: dependencyInjection_transaction.ek9 - Download dependencyInjection_transaction.ek9
The first update commits; the second throws, so it is rolled back:
primary-db: UPDATE accounts SET balance = 100 primary-db: COMMIT primary-db: UPDATE accounts SET balance = -5 primary-db: ROLLBACK Transaction failed: Exception: a balance cannot be negative
If an exception occurs before commit() is called, the catch block rolls the transaction back. Nothing rolls it back for you: neither the Transaction trait nor Closeable does.
For more transaction patterns, including decorator, callback, closure-capture and aspect-based approaches, see
the Q&A system: ek9 -q transaction.
Summary
Inversion of control and dependency injection usually become necessary only once software reaches a certain size. 'Aspects' are useful in a limited number of scenarios, but when comprehensive logging, security or other 'crosscutting concerns' matter across many components, they cut a lot of boilerplate code.
Conclusion
If you have used Spring or AspectJ with Java, you may find EK9's support inadequate. That is deliberate: EK9 is designed to curtail the extreme use of inversion of control, dependency injection and aspect oriented programming. If you want that freedom, use Java and Spring. The argument is in Design note: why EK9 limits injection and aspects.
Next Steps
Web services covers the final construct, which dovetails with components/applications and programs.