OERI Dependency Injection Container: InjectABL - Wiki - OpenEdge Architecture - Progress Community

OERI Dependency Injection Container: InjectABL

OERI Dependency Injection Container: InjectABL

InjectABL is an Inversion of Control container/Dependency Injection module that is part of the OpenEdge Core package.

What is dependency injection (DI)?

The short and sweet answer is:

Dependency injection means giving an object its instance variables.

Contained in that statement is the statement "the object does not manage it's instance variables' lifecycles, creation in particular".

There are 3 main injection patterns: constructor, method and property injection. We assume that constructor injection is intended for mandatory dependencies, and method and property injection is for optional dependencies.
A good description of dependency injection is on the Ninject site.

What's an Inversion of Control (IoC) container?

While dependency injection means that an object does not manage its own dependencies' lifecycles, the objects have to come from somewhere: this is an IoC container's purpose. It manages the lifecycles of the injected objects.

Why do we need such a beast?

Most examples of dependency injection show reasonably simple examples; in these cases it's easy to see the concepts of injection. However, if we want our DI to scale, we need something to manage the dependencies. This is what the IoC container does, at the simplest level. Wikipedia has more, including pros and cons, and further reading. Note that the techniques available to IoC include the injection techniques above.

Coding against interfaces

The reference implementation aims to provide extensible and replaceable components (since one size does not fit all). Coding to an interface is a strategy used to achieve this goal. From Wikipedia:

The use of interface allows to implement a programming style called programming against interfaces. The idea behind this is to base the logic one develops on the sole interface definition of the objects one uses and do not make the code depend on the internal details. This allows the programmer the ability to later change the behavior of the system by simply swapping the object used with another implementing the same interface.

How does this tie into DI and IoC containers?

When a class is coded against interfaces, there's an additional requirement to know which class to instantiate that implements that interface (since an interface cannot be invoked itself). This knowledge will take the form of some kind of mapping from the interface to the concrete class.

This ties nicely into DI, since we can inject the implementing (or concrete) object into the relevant object. Furthermore, since the IoC container manages these injected objects' lifecycle, we have the perfect place to keep the interface mapping information.

InjectABL design

Origin/inspiration

The InjectABL IoC component is largely based - conceptually and to a lesser degree code - on the Ninject DI container. While all of the DI/IoC products listed below provide mapping facilities, the majority of them are XML based; Ninject starts with source code-based mappings, which can be extended to use XML. This provides more readability; in addition Ninject has usable defaults, and is relatively simple to use and understand. It is also itself easily extensible.

Kernel

The IoC kernel provides the core functionality of the container: in a sense it really is the container itself. The kernel provides an API via the OpenEdge.Core.InjectABL.IKernel interface. A standard kernel is provided in the OpenEdge.Core.InjectABL.StandardKernel.

Kernel Settings

The kernel's behavior is modified via the OpenEdge.Core.InjectABL.KernelSettings object. This contains settings such as a cache pruning interval.

Kernel Components

The kernel operates on its component parts using the same code-to-interface principle that it supports. However, since the kernel cannot use the kernel to provide such mappings or injection (there's a wee race condition likely to happen), it manages these mappings via the OpenEdge.Core.InjectABL.ComponentContainer, which stores mappings and resolves them on request.

The standard kernel, and any other custom kernels can add (or replace) components used by the kernel. These components include lifecycle strategies, pipelines, caches and more. There is a custom kernel provided in the Common Infrastructure package (OpenEdge.CommonInfrastructure.InjectABL.ComponentKernel) for managing the reference implementation'sOpenEdge.CommonInfrastructure.Common.IComponent objects.

Lifecycle

The kernel manages an object's lifecycle, from soup to nuts.

Object invocation

The kernel invokes ("NEWs") a requested object. A request typically provides an interface and the kernel's mapping ("binding" in InjectABL) resolves the interface into a concrete class that it invokes. The kernel will also inject any arguments into the object's constructor. The kernel determines which constructor to use by the arguments it receives; a Provider (more below) performs the actual invocation and injection.

The constructor may have arguments that are themselves be managed by the kernel.

Activation and deactivation

An object's lifecycle has 2 phases: activation and deactivation. Activation is the process of getting an object ready for use after invocation: calling any additional methods that the application requires in order for the object to be ready for use. Once an object is activated, it will be returned to the caller (this would typically be an application manager, form or some other component) and be used as per its design. It may also be cached (more below).

Deactivation is the process of destroying an object, and calling any extra methods before any references are released.

Notes

Deactivation doesn't usually call DESTROY OBJECT on the object, but rather releases any references and lets the garbage collector take care of that

Objects which have a Transient scope do not have their deactivation managed by the kernel, by design (following Ninject).

The determination of which (if any) methods are additionally required is controlled by means of Lifecycle Strategies.

Lifecycle Strategies

Lifecycle strategies define what actions happen during activation and deactivation. Strategies must implement OpenEdge.Core.InjectABL.Lifecycle.ILifecycleStrategy. These strategies may include general actions such as method injection or more specific actions such as calling a particular method during activation or deactivation (see table). Lifecycle strategies are kernel components. Multiple lifecycle strategies can be used contemporaneously.

Lifecycle Strategy Examples

Class

Purpose

OpenEdge.Core.InjectABL.Lifecycle.PropertyInjectionLifecycleStrategy

Provides for generic property injection in the Provider during activation

OpenEdge.Core.InjectABL.Lifecycle.MethodInjectionLifecycleStrategy

Provides for generic method injection in the Provider during activation                                      

OpenEdge.CommonInfrastructure.InjectABL.ComponentLifecycleStrategy

Calls CreateComponent during activation and DestroyComponent during deactivation

Pipeline

A pipeline manages the running of Activation and Deactivation strategies. Pipelines are defined by the OpenEdge.Core.InjectABL.Lifecycle.IPipeline interface. The standard implementation -OpenEdge.Core.InjectABL.Lifecycle.StandardPipeline - simply iterates through the strategies in the order in which they were added to the kernel; customized pipelines may change this. Pipelines are kernel components. Only one pipeline is used per kernel (the first added).

Providers

Providers perform the actual injection work, whether it's by means of constructors, methods properties. Providers must implement the OpenEdge.Core.InjectABL.Lifecycle.IProvider interface. The standard provider - OpenEdge.Core.InjectABL.Lifecycle.StandardProvider - is used unless a custom provider is specified in the binding (mapping).

Notes

As of OpenEdge Release 10.2B01, property setting is not supported via reflection. Custom providers are required for cases where property injection is needed

Scope

In many cases, the application needs (or simply wants) to run a single instance of an object for the entire application's lifespan. The traditional method for this is to use the singleton pattern, which usually requires hard-coding. InjectABL has the concept of scope which allows the soft-coding of the lifespan of the managed objects. When a scope-holding object is deactivated, it deactivates any objects that are scoped to it.

There are a number of standard scopes, as enumerated in OpenEdge.Core.InjectABL.Lifecycle.StandardScope.

Scope

Description

Transient (Default)

Fire and forget: no instance reuse, no deactivation support

Singleton

Only one instance at a time: scoped to the life of the kernel

Agent

Scoped to the life of the (AppServer/WebSpeed) agent

Custom

Custom scope

The reference implementation has custom scope in OpenEdge.CommonInfrastructure.InjectABL.ManagerScope, which provides for lifecycles scoped to a specific manager.

Cache

The kernel has a cache with which it manages non-transient objects. This cache is a kernel component, and implements OpenEdge.Core.InjectABL.Lifecycle.ICache. Only one cache is used per kernel (the first added).

Lifecycle Context

Each request made to the kernel has context for the request, which is stored in a Lifecycle Context object (standard implementation OpenEdge.Core.InjectABL.Lifecycle.LifecycleContext, implementsOpenEdge.Core.InjectABL.Lifecycle.ILifecycleContext).

The lifecycle context contains information about the bindings and parameters that will be used to create the object, and to inject dependencies into it.

Binding

The binding stores the mapping between an interface and a concrete type in InjectABL. Bindings allow us to easily change the objects being injected, to provide the necessary parameters for injection, and to specify conditions under which the bindings are valid.

Modules

Modules are collections of bindings that are loaded into the kernel; multiple modules can (and will typically) be loaded into a single kernel. This allows an application to keep its bindings at an appropriate level of granularity (ie. there aren't accounting bindings mixed together with the infrastructure bindings).

Modules typically inherit from the abstract OpenEdge.Core.InjectABL.Binding.Modules.InjectionModule class, which implementsOpenEdge.Core.InjectABL.Binding.Modules.IInjectionModule. The individual bindings are created (destroyed) when the module is loaded (unloaded) into the kernel.

InjectABL supports the concept of a ModuleLoader which can discover the modules in the application: this may work on some kind of name pattern (e.g. "*Module.cls").

Note that there's nothing precluding the storage of bindings in XML or some other data store, such as a database table.

Binding syntax

The syntax for binding is defined in the OpenEdge.Core.InjectABL.Binding.IBindingSyntax interface, and can be described as below. <type> below refers to an instance of Progress.Lang.Class. A type is specified since it is the kernel that creates the instance from this type. The binding is not responsible for instantiating the implementation (that's the kernel's job).

The type passed in to the Bind() method is referred to a service in the InjectABL module; this should not be confused with the notion of a service as it pertains to the rest of the reference implementation (as in SOA).

Bind(<type>)  /* [required] service/interface or concrete class */
 :ToSelf()|To(<type>)  /* [required] concrete class type. ToSelf requires that the binding above be to a concrete type */

 :Using(<type>)  /* provider type */
 :InSingletonScope()|InTransientScope()|InAgentScope()|InScope(<type>, <enum>)  /* The lifecycle scope of the object. The InScope() allows for custom scope */
 :OnClientSession()|OnWebSession()|OnServerSession()  /* To which session type this binding applies */
 :Named(<char name>)  /* An instance name for the object */

 :WithConstructorArgument(<parameter>)  /* One or more constructor arguments to use. These can be ABL primitives, types or objects */
 :WithPropertyValue(<property name>, <parameter>)  /* One or more properties to set. These can be ABL primitives, types or objects */
 :WithMethodValue(<method name>, <parameter>)  /* One or more method arguments for one or more methods. These can be ABL primitives, types or objects */
 :When:<condition>  /* One or more conditions conforming to IConditionSyntax */

Notes

A bug in OE 10.2B01+ prevents us from fully using the fluent interface, and we need to use intermediary variables. The syntax above assumes for illustrative purposes that a fully-fluent interface is possible.

 

The "With" syntax (parameters)

There are 3 groups of "With" syntax elements: for constructor, method and property injection. The "With" syntax allows developers to specify parameters or arguments for the injection call. Traditionally (in other languages/frameworks), these parameters are used for primitive or hard-coded values, and the determination of which constructor/method to call and it's parameters is done by the kernel, usually by reflection. Since OE 10.2B doesn't have sufficient reflection capabilities, the arguments must be specified, especially for constructor injection; in addition, the standard Provider will attempt to use all of the constructor arguments it knows about to invoke the object.

Notes

If there are certain types of objects that an application knows about, that always require the same parameters, it may be more useful to create a custom Provider and add the arguments at that point, rather than requiring the arguments to be added for every binding. For an example see OpenEdge.CommonInfrastructure.InjectABL.ComponentProvider

Conditions

Binding can be made conditional using a syntax defined in the OpenEdge.Core.InjectABL.Binding.Conditions.IConditionSyntax interface, which evaluates a series of conditions (which need to implement OpenEdge.Core.InjectABL.Binding.Conditions.ICondition). A condition is indicated by the When binding syntax statement.

Selecting the correct binding

The kernel has a component which is responsible for selecting the matching bindings for a requested type; this component must implement OpenEdge.Core.InjectABL.Binding.IBindingResolver. The default InjectABL binding resolver is OpenEdge.Core.InjectABL.Binding.StandardBindingResolver .

Once all the matching bindings are selected for the requested type, the default behavior is to select the first matching binding (first in). This behavior can be customized by overriding the SelectBinding method in the kernel.

Example

There is an example of using the InjectABL IoC container in the support/tests folder, starting with test_injectabl.p. Objects referenced are in the OpenEdge.Test package.

/* file: test_injectabl.p */
using OpenEdge.Test.*.
using OpenEdge.Core.InjectABL.*.
using OpenEdge.Core.InjectABL.Binding.Parameters.*.
using OpenEdge.Core.InjectABL.Binding.Modules.*.
using OpenEdge.Lang.*.
using Progress.Lang.*.

def var kernel as IKernel.
def var modules as IInjectionModuleCollection.
def var params as IParameterCollection.

def var warrior as Samurai.

modules = new IInjectionModuleCollection().
modules:Add(new WarriorModule()).

kernel = new StandardKernel(modules).

warrior = cast(kernel:Get('OpenEdge.Test.Samurai'), Samurai). 

warrior:Attack("the evildoers").

params = new IParameterCollection().
params:Add(
new MethodArgument('SetPrimaryWeapon',
Class:GetClass('OpenEdge.Test.Shuriken'))).
  
params:Add(new PropertyValue('UseAlternate', 'true', DataTypeEnum:Logical)).

kernel:Inject(warrior, params).

warrior:Attack("a melon").

The example of binding below is taken from the OpenEdge.Test.WarriorModule test module.

method override public void Load():
/* use oBS as bug workaround */
def var obs as IBindingSyntax.
def var ocs as IConditionSyntax.

/* Binding #1 */
obs = Bind('OpenEdge.Test.IWeapon').
obs = obs:To('OpenEdge.Test.Sword').

/* Binding #2 */
obs = Bind('OpenEdge.Test.IWeapon').
obs = obs:To('OpenEdge.Test.Shuriken'):Named('alternateweapon').

/* Binding #3 */
obs = Bind('OpenEdge.Test.Samurai').
obs = obs
:ToSelf()
:Using('OpenEdge.Test.SamuraiProvider')
 
:WithPropertyValue('Dojo', 'Ninja Gym East')
 
:WithMethodValue('SetNinjaMasters', 'Mike')
:WithMethodValue('SetNinjaMasters', 'Dwight')

:WithMethodValue('SetAlternateWeapon',
Class:GetClass('OpenEdge.Test.Shuriken'))

ocs = obs:When().
ocs:Session:Not(SessionTypeCondition:Client).

/* Binding #4 */
obs = Bind('OpenEdge.Test.Samurai').
obs = obs
:To('OpenEdge.Test.ClientSamurai')
:Using('OpenEdge.Test.SamuraiProvider').

ocs = obs:When().
ocs:Session:Is(SessionTypeCondition:Client).
end method.

Note: The types can be passed in as strings, but are stored as instances of Progress.Lang.Class

Integration into OERA reference components

The InjectABL package

The InjectABL module forms part of the support code in the Core package. As such, it does not depend on the OERA reference implementation.

Customization

There are various extension/customization points available for an application. These are summarized here. Customizations and extensions are not restricted to their locations. A customized kernel is required when a kernel component (such as a lifecycle strategy or pipeline) is customized.

Module

Modules are the primary integration point into an application, since they contain the mappings between the interfaces (types, services) and implementations. Modules are selected programmatically, or loaded via a ModuleLoader. The latter case will typically use a pattern or some other form of convention over configuration.

Provider

Custom providers allow for certain categories of objects to be invoked in a constant fashion, for instance, a reference implementation IComponent requires a constructor that takes 2 parameters: the Service Manager and a ComponentInfo object. Rather than specifying these on every component binding, the reference implementation uses a custom Provider.

Examples:
OpenEdge.CommonInfrastructure.InjectABL.ServiceProvider; for invoking OpenEdge.CommonInfrastructure.Common.IService-implementing objects
OpenEdge.CommonInfrastructure.InjectABL.ComponentProvider; for invoking OpenEdge.CommonInfrastructure.Common.IComponent-implementing objects

Scope

An object's lifespan/scope can also be customised from the standard, allowing for generic parenting of objects.

Manager scope is an example: OpenEdge.CommonInfrastructure.InjectABL.ManagerScope.

Kernel & components

A kernel will usually be customized so as to customize the kernel components that are used, either by adding to the or replacing them. This can be done in a AddComponents override.

The OpenEdge.CommonInfrastructure.InjectABL.ComponentKernel is an example of this. In the ComponentKernel a specialized lifecycle strategy is added -OpenEdge.CommonInfrastructure.InjectABL.ComponentLifecycleStrategy - that calls CreateComponent and DestroyComponent on activation and deactivation, respectively.

References

Overview

http://martinfowler.com/articles/injection.html

http://en.wikipedia.org/wiki/Dependency_injection

http://jamesshore.com/Blog/Dependency-Injection-Demystified.html

http://kozmic.pl/archive/2010/06/20/how-i-use-inversion-of-control-containers.aspx

Selected IoC/DI containers

Framework

Technology

Link

Ninject

C#

http://ninject.org/

PicoContainer

Java

picocontainer.com

Windsor Castle

C#

http://castleproject.org/container/index.html

TinyIoC

C#

http://hg.grumpydev.com/tinyioc/wiki/Home

The Spring enterprise framework has an IoC component

Java

http://static.springsource.org/spring/docs/2.5.x/reference/beans.html

Other

Good discussion of programming to an interface here.

Comments
  • Someone needs to clean this up so the right margin isn't clipped and the entire article is readable.

  • Thanks for fixing the margin issue on this page.

  • Link "Good discussion of programming to an interface", fatagnus.com/.../ is dead.

  • For those interested here a nice presentation where DI and IoC are discussed in a broad context:

    www.youtube.com/watch