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 ?

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

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 ?

Above, the method beforeAdvice needs to log messages. There were three ways to give it a logger:

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

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

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?

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:

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

The Applications

There are four applications:

noAspectApp
noAspectApp
  register Solution1() as BaseConfiguration

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

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

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

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 ?

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:

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 ?

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}`)

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.