Injection Container - coolsamson7/inject GitHub Wiki

Overview

The basic idea of this dependency injection container is to have a central instance that acts as a registry for different objects by storing recipes, that describe how to build them. Every recipe on the other hand may define dependencies to other objects ( by id or by type ) that will be resolved by the container. In order to do that the container will sort the dependency graph topologically, and start building objects starting with objects without dependencies. Resolving dependencies by type will regard the class hierarchy and will as well assure that only one candidate is available.

As soon, as the container is up and running, a simple api is provided that is used to retrieve the stored objects.

Different factors determine the lifecycle of objects

  • object scopes determine how often objects and when they are created: a singleton scope will create one instance of any type ( and do that when the container starts, unless it is marked as lazy ). A prototype scope will create new objects on every request. Other custom scopes may be added that are based on a session concept, etc.
  • lazy objects are created only on demand.

Before we continue with the container itself, we need to explain the concept how structural information of beans are analyzed and collected, since all other mechanisms rely on this concept

BeanDescriptor

BeanDescriptor maintains bean structures by analyzing classes with respect to properties and their corresponding types as well as the superclass hierarchy. In addition it offers reflection features.

A BeanDescriptor is created by calling class functions

BeanDescriptor.forType(type : Any.Type) throws -> BeanDescriptor
BeanDescriptor.forClass(clazz : AnyClass) throws -> BeanDescriptor
BeanDescriptor.forClass(className : String) throws -> BeanDescriptor

or by calling the constructor with an instance.

Once constructed, it offers a number of functions

  • getBeanClass() -> AnyClass returns the referenced class
  • getSuperBean() -> BeanDescriptor? the descriptor of the direct superclass, if any
  • getSubBeans() -> [BeanDescriptor] list of direct inherited classes
  • getProperties(local : Bool = false) -> [PropertyDescriptor] returns a list of properties, either local to the specific class or overall including all superclasses.
  • getProperty(name : String) -> PropertyDescriptor lookup and return a property by name. It will throw an error, if not found. A subscript operator is provided that calls this method!
  • findProperty(name : String) -> PropertyDescriptor? will return nil if the property is not found

Warning: the func getSubBeans() returns only instances with respect to already analyzed classes, since Swift does not offer any functions to retrieve subclasses ( Only superclasses work ). This may cause some strange effects!

Example: Assuming derived classes Base and Derived

// create base descriptor

var descriptor = BeanDescriptor.forClass(Base.self)

var subBeans = descriptor.getSubBeans() // will be empty!

BeanDescriptor.forClass(Derived.self)

subBeans = descriptor.getSubBeans() // will contain one member!

In order to analyze the class structure, a bean descriptor requires an instance in order to have a mirror parameter. This is either the provided instance or an instance which is cerated dynamically. Since creating instances based on an AnyClass arguments also does not work ( oh boy... ) there is a special protocol

public protocol Initializable : class {
    init()
}

that the corresponding classes need to implement! NSObject is already extended with this protocol, btw.!

The mirror is then used to analyze the class structure and the complete class hierarchy. What is not possible to analyze automatically are implemented protocols. For this purpose the descriptor class must be modified manually with the function implements(protocol : Any.Type).

This can be done manually or integrated in the class by implementing the protocol

/// this protocol defines a callback function that will be invoked by the bean descriptor whenever it analyzes an object of this type.
public protocol BeanDescriptorInitializer {
    /// implement any code to add information to the bean descriptor
    func initializeBeanDescriptor(descriptor : BeanDescriptor) -> Void
}

Example:

 class Foo : Initializable, BeanDescriptorInitializer {
    // Initializable

    required init() {
    }

    // implement BeanDescriptorInitializer

    func initializeBeanDescriptor(beanDescriptor : BeanDescriptor) {
       try! beanDescriptor.implements(Initializable.self, BeanDescriptorInitializer.self)
    }
}

A PropertyDescriptor offers the following interface

  • getName() -> String return the name
  • getPropertyType() -> Any.Type return the property type. If the original type is an Optional, the delegate is stored here, while an optional flag stores the information.
  • isOptional() -> Bool return true, if the original type was optional
  • get(object: AnyObject!) throws -> Any? returns the property value given an object
  • set(object: AnyObject, value: Any?) throws -> Void set a property value given an object

The current limitation for the reflection is the it only works for NSObject's!

Example:

class Foo : NSObject {
        // instance data
        
        var string : String = ""
        var int : Int = 0
        var float : Float = 0.0
        var double : Double = 0.0
        var strings : [String] = []
    }

...
let foo = Foo()
let bean = try BeanDescriptor.forClass(Foo.self)

try! bean["string"].set(foo, value: "hello")

Environment

Environment is the central container. It is created with the constructor

init(name: String, parent : Environment? = nil, traceOrigin : Bool = false) throws

  • name: a suitable name
  • parent: an optional parent environment
  • traceOrigin: if true all bean definitions will remember where - file, line, column - they where created

Whenever a parent is specified all of its definitions and other internals - configuration manager, etc. - are inherited

The function

report() -> String

may be called to get a report of all bean definitions.

Defining Beans

The basic - fluent - function to define a bean is

func define(declaration : Environment.BeanDeclaration) throws -> Self

This inner class is created by different bean(...) functions and in turn offers an api, that can be used to add additional configuration options such as property declarations.

Example:

let environment = try Environment(name: "environment")
try environment
   .define(environment.bean(Foo.self, id: "foo")
      .property("id", value: "foo")
      .property("number", value: 1))

   .define(environment.bean(Bar.self)
      .property("id", value: "bar")
      .property("age", value: 4711))

Let's look at the possible parameters of every bean

Name Description
class the class of the bean.
id an optional id that may be used to reference specific instances
lazy boolean value. if true, the instance will be created on demand
abstract the bean declaration is only a template and will not be instantiated
scope the scope will determine the lifecycle of beans. Builtin scopes are "singleton" and "prototype". Others may be added
factory the factory will create instances. Depending on the configuration, different flavors exist. ( e.g. call init, call closure, return value, etc. )

The following functions are provided

  • func bean(instance : AnyObject, id : String? = nil) -> Environment.BeanDeclaration Specify a singleton - non lazy - bean by passing a constructed object and an optional id
  • func bean(className : String, id : String? = nil, lazy : Bool = false, abstract : Bool = false) throws -> Environment.BeanDeclaration Specify a bean given the - fully qualified - class name, an optional id, and parameters for the lazy and abstract attributes
  • func bean<T>(clazz : T.Type, id : String? = nil, lazy : Bool = false, abstract : Bool = false, factory : (() throws -> T)? = nil) Specify a bean with the appropriate type by passing a factory closure function.

Let's look at some examples

Example: init call

let environment = ...
environment.define(environment.bean(Foo.self, factory: Foo.init) // a factory for type T is a function () -> T 

Example: closure

let environment = ...
environment.define(environment.bean(Foo.self, factory: { Foo() })  

The Environment.BeanDeclaration offers a number of additional fluent functions

  • func id(id : String) -> Self set the id
  • func lazy(lazy : Bool = true) -> Self set the lazy attribute
  • func abstract(abstract : Bool = true) -> Self set the abstract attribute
  • func parent(parent : String) -> Self set the id of an abstract bean definition that acts as a template
  • func scope(scope : String) -> Self set the bean scope.
  • func requires(id id: String) -> Self specifies that this bean needs to be constructed after specified bean given the id
  • func requires(class clazz: AnyClass) -> Self specifies that this bean needs to be constructed after a bean of the specified type
  • func target(clazz : AnyClass) throws -> Self this will determine the target class for FactoryBean's.
  • func property(name: String, value : Any? = nil, ref : String? = nil, resolve : String? = nil, bean : BeanDeclaration? = nil, inject : InjectBean? = nil) this function will define a property injection. Given a property name, different parameters determine the injection type. Only one of the following optional parameters may be specified:
    • value a value
    • ref the id of a referenced bean
    • resolve a string that contains configuration placeholder that will be resolved with the internal configuration manager and possibly converted to the expected type
    • bean a recursive bean definition
    • inject an InjectBean instance - that optionally contains an id -. The result is that a the container will find a matching candidate ( and will throw an error if multiple candidates exist ) and inject it

Let's look at an example for a closer factory with external dependencies:

Example: init call

let environment = ...
environment.define(
   environment.bean(Foo.self, factory: {
      return Foo(bar: environment.getBean(Bar.self)) 
      })
      .requires(Bar.self) // we beed to make sure that bar is constructed first!

In addition to injections that are expressed by the fluent interface it is also possible to directly stick it to the class properties.

Injections

Since Swift does not allow for user defined attributes which could be used to define injections ( such as the @Inject in Java ) we have to do it differently.

Since BeanDescriptor is a class that collects structural information about classes, the concept has been extended.

First of all, all injections are expressed by subclasses of the base class

Inject

This specific subclass will identify the required injection and possibly add additional parameters for the injection process.

Attaching the instance to a property is done by calling the property function

inject(inject : Inject)

Implemented injections are currently:

  • InjectBean with init(id : String? = nil)
  • InjectConfigurationValue with init(namespace : String = "", key : String, defaultValue : Any? = nil)

InjectBean without parameters can also be expressed by calling the property function

autowire().

Let's look at a real sample as used in the framework:

public class AbstractConfigurationSource : NSObject, ConfigurationSource, Bean, BeanDescriptorInitializer {
    // MARK: instance data
    
    ...
    var configurationManager : ConfigurationManager? = nil // injected

   // MARK: implement BeanDescriptorInitializer
    
    public func initializeBeanDescriptor(beanDescriptor : BeanDescriptor) {
        beanDescriptor["configurationManager"].autowire()
    }

    // MARK: implement Bean
    
    public func postConstruct() throws -> Void {
        try configurationManager!.addSource(self)
    }

   ...
}

Internally all injections are executed by corresponding subclasses of the class Injection which contains the code to compute the necessary value. All Injection's on the other hand are maintained by a central class Injector that acts a a registry for the different injections. Every environment manages an Injector instance which is part of the post processing of beans. More injections can be added by simply defining the corresponding Injection class within the environment!

XML Configuration

The environment function

loadXML(data : NSData) throws -> Self

loads a xml file according to the basic spring syntax. Not supported are

  • constructor injections
  • method injections
  • all property injections other than literal values ( e.g. dictionary... )

Configuration

Every environment stores a configuration manager that keeps track of different configuration sources and will return configuration values ( via api or within xml ).

The environment function

addConfigurationSource(source : ConfigurationSource)

may be used to add different sources. The other possibility is to add manual configuration values which is done like this:

 environment.define(environment.define.settings()
              .setValue(key: "key", value: 4711))

On the other hand the public function

getConfigurationValue<T>(type : T.Type, namespace : String = "", key : String, defaultValue: T? = nil, scope : Scope? = nil) throws -> T

is used to retrieve values.

Reflection Requirements

Depending on the exact configuration, reflection may be used or not. Any property definition or injections expressed in the class itself will make use of a reflection feature in order to set the property value. In the current Swift version ( 2.x ) this requires the objects to derive from NSObject!

In all other cases at least the bean structure needed to be determined - see the chapter on BeanDescriptor's -. Deepening on the configuration the technical framework may need to - unless instances are provided by value or via a factory - construct a sample object in order to analyze ist structure. The default is to look for a Initializable protocol that simply defines a init()constructor ( NSObject already is extended ).

Abstract Beans

An abstract bean defines a construction template which may be completed or altered by child beans.

Example:

let environment = Environment()

environment
   .define(environment.bean(Bar.self, id: "bar-template", abstract: true)
      .property("flavor", value: "vanilla")

   .define(environment.bean(Bar.self, id: "bar-1", parent: "bar-template")
      .property("id", value: "1")

   .define(environment.bean(Bar.self, id: "bar-1", parent: "bar-template")
      .property("id", value: "2")
)

let bar = environmet.getBean(Bar.self, byId: "bar-1")

The property "flavor" is inherited in the example, while "id" is separately set.

Bean Scopes

A bean scope defines the lifecycle of beans and ist define be the following protocol.

public protocol BeanScope {
    /// the name of the scope
    var name : String {
        get
    }

    /// prepare a bean declaration of an environment after validation
    /// specific implementing classes may use this callback to create instacnes on demand
    /// - Parameter bean: the `BeanDeclaration`
    /// - Parameter factory: the factory that cerates an instance
    func prepare(bean : Environment.BeanDeclaration, factory : BeanFactory) throws

    /// return a new or possibly cached instance given the bean declaration
    /// - Parameter bean: the `BeanDeclaration`
    /// - Parameter factory: the factory that cerates an instance
    func get(bean : Environment.BeanDeclaration, factory : BeanFactory) throws -> AnyObject

    /// execute any cleanup code after a scope has ended. ( e.g. after session removal )
    func finish()
}

An abstract class AbstractBeanScope is implemented that can serve as a base class.

Builtin scopes are

  • "singleton" whenever a bean is created that instance is cached as a singleton
  • "prototype" whenever a bean is requested a new instance will be created.

If new scopes need to be referenced the new class can registered manually with the environment function

registerScope(scope : beanScope) -> Self

or simply defined in the current environment.

environment.define(environment.bean(SampleBeanScope.self, factory: SampleBeanScope.init))

Bean Post Processors

All beans of type

protocol BeanPostProcessor {
    // post process the given object
    // - Parameter bean: the bean
    // - Returns: the possibly modified object
    // - Throws any error
    func process(bean : AnyObject) throws -> AnyObject
}

will be called by the container giving it the possibility to modify or completely replace an instance ( e.g. proxy ). Different post processors are possible. The order is defined by the definition order!

Lifecycle Callbacks

All classes that implement the following protocol

public protocol Bean {
    /// called after the instance is constructed including all injections
    func postConstruct() throws -> Void ;
}

will get called after all injections and prost processors have been executed.

All classes that implement the following protocol

protocol EnvironmentAware {
    /// the setter will be called by the environment

    var environment: Environment? { get set }
}

will be notified by the container about the current environment.

Factory Beans

The following protocol

public protocol FactoryBean {
    /// create the corresponding bean
    /// - Returns: the bean instance
    /// - Throws: any possible error
    func create() throws -> AnyObject
}

may be implemented and is used to create instances of the target type ( which needs to be part of the definition ).

Example:

class BarFactoryBean: NSObject, FactoryBean {
   // MARK: instance data

   var flavor : String = ""
 
   // MARK: implement FactoryBean

    func create() throws -> AnyObject {
        let result = Bar()

        result.flavor = flavor

        return result
    }
}

...


let environment = Environment()

environment.define(environment.bean(BarFactoryBean.self)
   .property("flavor", value: "vanilla")
   .target(Bar.self)
)

let bar = environment.getBean(Bar.self) // this is created by the factory

Environment Startup

An environment will startup, if any one of the functions that retrieve beans is called or if the function

startup()

is called manually.

The following steps occur:

  • all definitions are analyzed with respect to their dependencies. A dependency is established be one of the following constructs
    • injection as expressed in the bean descriptor
    • explicit dependency expressed by requires(...) or dependsOn(), ref() or bean()
  • the resulting graph is sorted topologically and traversed starting with beans without dependencies. Every bean that with singleton scope which are not lazy are resolved - e.g. constructed - directly.
  • the construction process involves
    • the creation of the instance
    • execution of all property injections
    • execution of all prost processors. The environment self is a prost processor which includes the steps
      • execution of injections
      • callback execution for the protocols Bean and EnvironmentAware

Retrieving Beans

The public api is pretty simple:

  • func getBean<T>(type : T.Type, byId id : String? = nil) throws -> T return a suitable object instance given a type and an optional id.
  • func getBeansByType<t>(type : T.Type) throws -> [T] return a list of beans that are assignable to the class.

If getBean() is called without an id, it will assume that there is only one match. If not, it will throw an error.

The getBean() function will respect class hierarchies as well as protocols. Requesting an object with no result will try to check all inherited classes or implementing classes in case of a protocol parameter.

Some remarks on this feature:

Protocols

Correlations between classes and protocols have to be specified manually. ( Swift does not help here ) This can be done by adding specific code to the class itself:

class Swift : BeanDescriptorInitializer {
        // Initializable

        required init() {
        }

        // implement BeanDescriptorInitializer

        func initializeBeanDescriptor(beanDescriptor : BeanDescriptor) {
            try! beanDescriptor.implements(BeanDescriptorInitializer.self)
        }
    }

or by adding it to a bean declaration

environment
   .define(environment.bean(Swift.self, factory: Swift.init)
      .implements(BeanDescriptorInitializer.self))

Timing

The container - or rather the BeanDescriptor - will know about subclasses and implementing classes as soon as the corresponding descriptor of a subclass is created. When he container is starting up it will create all non lazy singletons and will create the appropriate bean descriptors on the fly.

If a subclass relationship is not known - "no candidate found for " - this may be due to the fact that the requested bean is lazy and has not been created yet. The solution is to force the class BeanDescriptor to create the appropriate instance. ( e.g. manual constructor call )