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.
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? 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.
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 also then very hard to write pure functions and code.
... 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()) ... //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.
Rather than do that; 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).
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.
It is best practice 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.
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 'cross cutting concerns'. These typically involve:
- Timing of calls
- Transactional boundaries
- Permission processing
There are other reasons, but the above are the main ones.
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 compoents 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.
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 boiler plate code.
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.
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.
Four different applications have been defined; these are:
... 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 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 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 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
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? TimerData() assert false TimerData() -> millis as Millisecond joinPoint as JoinPoint super(joinPoint) before: millis before() <- rtn as Millisecond: before TimerAspect extends Aspect clock as Clock? TimerAspect() assert false 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
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.
As you can see above these 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.
You might be wondering whats 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'!
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.
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' you returned in 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 workout the duration of the call!
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, any thing 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.
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. In addition; 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.
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 ralating to the actual functionality in the components.
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 'cross cutting concerns' are critical across a large number of components they really help reduce boiler plate code.
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 to 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 - use Java and Spring.
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.
The next section on web services is the final construct and dovetails in with components/applications and programs.