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.

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.

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 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).

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.

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:

There are other reasons, but the 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:

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.

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 boiler plate 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?

      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

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 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.

Aspects

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'!

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' 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!

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, 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.

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 ralating 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 'cross cutting concerns' are critical across a large number of components they really help reduce boiler plate 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 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.

Next Steps

The next section on web services is the final construct and dovetails in with components/applications and programs.