Dependency Injection in EK9
Dependency Injection and Inversion of Control have been touched on in Components and Applications; which includes an example that shows the separation between 'wiring' components and the 'use' of components through an abstract base.
The separation of 'what' it to be used by 'when' it is used, is really the main value being provided by inversion of control. Dependency injection is then the mechanics of how that separation is delivered.
Inversion of Control
The inversion part really means that the function/class/method no longer 'decides' what object it is going to create to make a call; control is inverted by either the object being passed to the function/class/method or it being 'injected'. In both cases the function/class/method does not have 'control' of the specific type of object and also is not responsible for creating it.
Polymorphism is clearly essential for this mechanism to work.
Consider the following snip of code (the full definition is show later in the example); the TimerAspect class does not decide the concrete type of Clock it will be using for timing. In this case the Clock is passed into the constructor method of the TimerAspect.
... defines class TimerAspect extends Aspect clock as Clock? default private TimerAspect() TimerAspect() -> clock as Clock this.clock: clock ... //EOF
It is this application 'thirdApp' that has the control and decides to use SystemClock. The thirdApp has control, in fact in EK9 it is the applications that are responsible for making the decisions on the wiring of which components to use.
... defines application thirdApp register Solution3() as BaseConfiguration with aspect of TimerAspect(SystemClock()) ... //EOF
But note this is separate to dependency injection, nothing in the above example has been 'injected'. Only the control has been inverted.
You may consider the 'register' syntax of components and aspects to be much more 'wordy' than much of the other EK9 syntax see so far. This is by design, the use of components and application should be relatively infrequent in comparison to functions, classes, traits and records. So their should not be a need to create too many of these large and significant constructs.
The syntax has been designed to reflect their size and significance.
Dependency Injection
Rather than pass the component into a specific function/class/method or component; it is possible (when using an abstract base component) to have it automatically injected.
It is important to use this capability sparingly and with care; as excessive use causes 'hidden' coupling and limits refactoring. It is then very hard to write pure functions and methods.
... defines class LoggingAspect extends Aspect loggingLevel as String? LoggingAspect() as pure this("WARN") LoggingAspect() as pure -> level as String 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()}`) ... //EOF
In the snip above the method beforeAdvice needs to be able to log out messages. So one solution here would have been for the LoggingAspect to retain control and decide on which implementation of ILogger to use.
Another alternative would have been to pass an instance of a construct that was an ILogger at the construction of the LoggingAspect.
Rather than use either of those solutions; the LoggingAspect just 'expects' an implementation of ILogger to be 'injected'. It can then go on to use the log method via the ILogger abstract component. The key syntax to trigger injection of a component is to use the ! symbol after the type when declaring the variable.
The following snip shows how a concrete implementation of the ILogger was made available.
... defines application firstApp register FileLogger() as ILogger register Solution1() as BaseConfiguration with aspect of TimerAspect(SystemClock()), LoggingAspect("DEBUG") //EOF
As the application firstApp registers a new instance of FileLogger as the ILogger this makes it known to EK9 that FileLogger can now be injected in places where ILogger is being used.
This is where EK9 will look for those references to ILogger and ensure that the variable declared is set to use the implementation provided (FileLogger in this case).
If you've used 'Spring' before in Java, this is like a very cut down version of that (by design). Use this capability sparingly and with care as it can inhibit reuse.
Limitations
Only components can be used for dependency injection. When expecting injection to take place the variable declared must have a type that is an abstract base component.
Best practice is to restrict the number of components used and the amount of injection employed. As you can see from the example above logger as ILogger! is hidden inside a method. This makes the whole class much less portable, harder to refactor, less reusable. It also means that pure cannot be employed (there is a very hidden component being injected).
Use this mechanism carefully and avoid it by passing in parameters where possible.
So why add this feature?
There are times when major subsystems and components need to be wired together in different ways. The main bulk of code remains the same and access is via traits or abstract bases classes/components. Inversion of Control and Dependency Injection are really useful to solve that problem in specific situations.
The other major reason is to facilitate Aspect Oriented Programming (but in a limited way). There are times when it is necessary to deal with issues in software design via what are called 'crosscutting concerns'. These typically involve:
- Logging
- Timing of calls
- Transactional boundaries
- Permission processing
There are other reasons, but those above are the main ones.
A Scenario
To explain the rationale for including Inversion of Control, Dependency Injection and Aspect Oriented Programming it is necessary to define a scenario.
The scenario is; a 'system' is required that has a number of major components, these are as follows:
- Client Record System
- Management Reporting System
- Product Purchase System
Each of these major components can be constructed from several other components. But (and this is the key point), whenever any type of access to methods on these components takes place; that access must be 'logged'.
Moreover during development and also in the 'staging' phase it is also necessary to gather some metrics on how long each method call took when calling component methods
For a real solution it would probably be necessary to deal with 'transaction' boundaries and also limit user access via LDAP or something like that. For this scenario focus will be on logging and timing.
As you can see - in effect you need to do the 'same thing' in terms of logging/timing on every method call on every component. In general, you are not concerned with the fine details of which methods or the types and values of parameters. This is where 'Aspects' can be used; as you just need to know that the method was called.
In other aspect oriented programming solutions a little too much detail is given. While initially this seems like a good idea; it triggers to temptation be developers to alter processing in some way.
This leads to defects that are very hard to identify and resolve. EK9 give the developer the minimal information just to be able to know a method was called.
Solutions
Error Prone and not Scalable
The first solution to this (not really viable) is to ensure that some form of logger and timer is provisioned in some 'base class/component' and so it can then be called manually by the developer when defining a new method. Not really viable as it is error-prone and focuses on Inheritance.
Too manual, but necessary if details are needed
Another solution would be to employ EK9 classes with traits and use delegation. So the new class can deal with the logging/timing and then just call the delegate for the actual processing. This is not too bad an idea, if you really want all the parameter details and are prepared to write lots of boilerplate code.
Using Aspects
This final solution uses 'Aspects' and components, as we are not really bothered about all the actual parameter details on the method calls (just the fact they happened/how long they took). We can use the EK9 solution. This is shown below.
To keep the example fairly short a single component that notionally holds a 'FileStoreName' as part of a configuration is used. It is the access to methods on the configuration component that need to be logged and in some cases timed.
Explanation
Three different BaseConfiguration solutions have been provided - typically they might have different performance characteristics, or costs, etc.
To use the 'wired' in BaseConfiguration solution; a class ConfigHandler has the dependency injected.
A simple function checkConfigHandler is used to create a new instance of the ConfigHandler and call 'showConfigDetails()' so details can be printed out. It is this method that triggers a call to method 'getFileStoreName()' on the component 'wired' in.
The Applications
Four different applications have been defined; these are:
noAspectApp
... noAspectApp register Solution1() as BaseConfiguration ...
This application does not do any logging or timing and uses Solution1 as the BaseConfiguration. This application is then used by program Program0. The main point of this is just to check the functionality works! Its output is shown below:
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") ...
Solution1 is also used; but does use both logging and timing 'Aspects'. These 'Aspects' are described in detail after the example code. Its output is shown below:
Program1 functionality Will check config handler to see if file store name is available DEBUG: Before com.customer.components.Solution1 getFileStoreName DEBUG: After com.customer.components.Solution1 getFileStoreName INFO: 3ms Milliseconds for com.customer.components.Solution1 getFileStoreName MainStore
secondApp
... secondApp register FileLogger() as ILogger register Solution2() as BaseConfiguration with aspect of LoggingAspect("WARN") ...
Solution2 is used in this case and only uses the logging 'Aspect'. Also note how the logging level has been altered to 'WARN', the secondApp took control of this when specifying the construction of the logging 'Aspect'. Its output is shown below:
Program2 functionality Will check config handler to see if file store name is available WARN: Before com.customer.components.Solution2 getFileStoreName WARN: After com.customer.components.Solution2 getFileStoreName SecondaryStore
thirdApp
... thirdApp register FileLogger() as ILogger register Solution3() as BaseConfiguration with aspect of TimerAspect(SystemClock()) ...
Finally this application uses Solution3 and this time only uses the timer 'Aspect'. Its output is shown below:
Program3 functionality Will check config handler to see if file store name is available INFO: 1ms Milliseconds for com.customer.components.Solution3 getFileStoreName DefaultStore
The Programs
Four different programs when linked with the four different applications could have different functionality. If the applications were much larger, different programs might be needed for a range of different tasks (all relating to the same application).
This is one of the main points of IOC/DI; separate the 'wiring' of the application and all of its components from the program itself. Split the control.
The full listing
#!ek9 defines module introduction defines component BaseConfiguration abstract getFileStoreName() abstract <- rtn as String? 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 defines class ConfigHandler //This component will get injected config as BaseConfiguration! showConfigDetails() stdout <- Stdout() stdout.println(config.getFileStoreName()) 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 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}`) defines class LoggingAspect extends Aspect loggingLevel as String? LoggingAspect() as pure this("WARN") LoggingAspect() as pure -> level as String 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()}`) TimerData extends PreparedMetaData before as Millisecond? default private TimerData() TimerData() -> millis as Millisecond joinPoint as JoinPoint super(joinPoint) before: millis before() <- rtn as Millisecond: before TimerAspect extends Aspect clock as Clock? default private TimerAspect() TimerAspect() -> clock as Clock this.clock: clock override beforeAdvice() -> joinPoint as JoinPoint <- rtn as TimerData: TimerData(clock.millisecond(), joinPoint) //overload the after method and EK9 will find this method. override afterAdvice() -> timerData as TimerData millisecondsTaken <- clock.millisecond() - timerData.before() joinPoint <- timerData.joinPoint() //Will be injected logger as ILogger! logger.log("INFO", `${millisecondsTaken} Milliseconds for ${joinPoint.componentName()} ${joinPoint.methodName()}`) //EOF
Logging
It would be normal to pull the ILogger, FileLogger and any other logging implementations out to a separate module and source file; as these would be widely reusable. Typically, they would form part of a 'core infrastructure' layer.
Timing/Logging Aspects
As you can see above, there is a dependency in the TimerAspect on ILogger and it expects it to be injected. You may or may not want that! In general the more decoupled things are; the better.
So an alternative here would be to develop and number of logger class implementations and then pass those in to a Logger component/'Aspect' or a Timer 'Aspect'. Then when each of those is created in the appropriate application it can be composed with the right implementation.
Aspects
You might be wondering what's all this 'beforeAdvice', 'afterAdvice', 'JoinPoint' and 'PrepareMetaData'; there seems to be a lot of jargon going on here! Well there is a lot of jargon in 'Aspect Oriented Programming'!
Advice
Basically; it is the two methods that get called before and after the method on the component we are looking to wrap an 'Aspect' around.
JoinPoint
So just before the component method is called; EK9 creates a 'JoinPoint' object and populates it with the component name and the method name. Then it calls 'beforeAdvice' and passes the 'JoinPoint' object in as a parameter. Now over to you; the developer - you can do what you like here! But you must return an Object that is or extends 'PrepareMetaData'. See the TimerData as an example of this. You can squirrel away all sorts of data in that object you return (you might need it in the 'afterAdvice').
afterAdvice and PreparedMetaData
When EK9 calls the 'afterAdvice', it will pass back the 'PreparedMetaData' that was returned by the 'beforeAdvice'; if you look at the TimerData you can see that you now have access to the millisecond data of when 'beforeAdvice' was called (and also the 'JoinPoint'). So in the case of the Timer Aspect it is possible to work out the duration of the call!
Benefits
Once you have defined a set of 'Aspects'; they can be used with any component! Yes they are limited to just textual representations of the component name and method name. It is not possible to filter and say you want some methods and not others (too fragile!). The details of all the parameters are not available to you. But these are reasonable limitations, anything more than this tends to lead to complexity, confusion and brittle code that fails when refactored.
Separation of Concerns
The separation of concerns of functionality from logging and timing is a major benefit. The same 'Aspects' can be used over and over again and a wide range of components and all their methods; none of which need to concern themselves with logging or timing. This leads to clarity and wider reuse/reliability.
Clearly in this example, having just one method to log and time is trivial. But if you have thousands of components/methods this approach works quite well.
This is also clearly a form of composition.
Inversion of Control
The inversion of control in terms of which component is now extended to include which 'Aspect' (if any) should be applied to that component. This is one of the main reasons the application construct was created.
So in the application it is possible to define all the high level components that should be used. When creating those components the appropriate classes and functions can be passed into the components as parameters. Any classes or functions that need access to components can either have the component passed in as a parameter or can use dependency injection.
This is a critical point; the application construct is only really aimed at facilitation the high level composition of our solution. You should strive to use inversion of control with constructs by passing in instances at construction. You should only employ dependency injection when you really need to.
Coupling
class ConfigHandler remains unchanged and not linked directly to any concrete implementation.
It is easy to alter if/how/where logging is done without needing to alter any code relating to the actual functionality in the components.
Summary
In general it is only when software gets to a certain size that IOC/Di becomes necessary. Employing 'Aspects' can be useful in a limited number of scenarios. But when comprehensive logging, security or other 'crosscutting concerns' are critical across a large number of components they really help reduce boilerplate code.
Conclusion
If you've used Spring or 'AspectJ' with the Java language, you'll probably consider what is included in EK9 as inadequate. The design of EK9 has been done in such a way as to curtail/reduce the extreme use of IOC/DI and Aspect Oriented programming. Some developers may consider this a bad thing. They may consider that developers should have the freedom to do what they want.
You can - but not in EK9; use Java and Spring as this may meet your needs.
The EK9 language is opinionated and provides more functionality in some areas and less in others, this is borne of experience and having to deal with nightmares of injection, tight coupling and hidden dependencies.
Next Steps
The next section on web services is the final construct and dovetails in with components/applications and programs.