Services and the Service Container
- 1 The Service Container
- 2 Service Interfaces
- 3 Mandatory vs. optional services
- 4 Requesting Services from the Service Container
- 5 Adding Services to the Service Container
- 5.1 Using the Service Loader
- 5.2 Manually
- 5.3 Lazy initialization with get-service.i
- 5.4 At first request through IServiceCreator callback
The Service Container allows the usage of the Dependency Injection Pattern through a container for classes that another class depends on. Services are the implementation of the dependency of a class. The terms Services and Service Container have been adopted from the .NET Visual Designer implementation (http://msdn.microsoft.com/en-us/library/system.componentmodel.design.servicecontainer%28v=vs.110%29.aspx). In the .NET Visual Designer the design canvas, the property grid or the name generator for new control instances are services required to implement the whole designer functionality. The service container decouples the components in the whole system from each other.
When building systems of dependent classes the service container solves the problem of accessing central (to the session or an application context, e.g. a Form) objects providing functionality to other objects. The service container allows object instances to be registered and returns them on request from other classes. The “key” for requesting object instances is typically an interface that the requested class (the dependency) implements and thus the “role” it plays for the requestor (the dependent class).
The usage of dependency injection provides a number of benefits when designing large systems:
Systems become easier customizable. Classes do not need to know the exact implementation of a service that they depend on. The actual service implementations may have been registered in the service container way before they are needed or the service container may use a call back to create an instance when first needed. But in any way the class that depends on the service has no knowledge about the actual implementation or about how to create an instance of it.
This provides a huge advantage also when service implementations should be customizable. In this case a custom implementation will be registered in the service container under the service interface and will so become available to the dependent class without the need to change any code there.
Other patterns for providing central functionality such as the singleton pattern or static classes do not allow this customization as the dependent class needs to know exactly into which class to call to resolve the dependency.
As a consequence dependency injection makes unit testing much simpler. In essence the service container and the dependency injection pattern allow the injection of mockup services. Mockup services simulate the behavior of a dependency in a much simpler and thus potentially fault free way. As the result a unit test can be specialized on just testing the dependent bits of code – simplifying the location of the cause for failures compared to debugging a whole complex system.
The SmartComponent Library and the WinKit do heavily rely on dependency injection and the service container to allow customization (with mandatory and optional services).
Customers may use services and the service container also for application services such as price calculation, stock enquiry or for adapters to other systems.
The Service Container
The Service Container of the SmartComponent Library allows to register service classes under services types (interfaces or classes). The same object instance may be registered multiple interfaces in case it provides more than a single role to the system. The Service Container as a registry does not verify that services do actually implement the interface they are registered under.
The Service Container provides four public methods for the registration, deregistration and accessing of services.
The method AddService adds a service instance to the Service Container (registration). The service will be registered under the given type (instance of Progress.Lang.Class).
AddService syntax
/*------------------------------------------------------------------------------
Purpose: Adds the specified service to the service container.
Notes:
@param poClass The reference to the class or interface of the service to add
@param poObject An instance of the service type to add. This object must implement or inherit from the type indicated by the serviceType parameter
@return The reference to the service that was added (poObject). This allows fluid style usage of this routine
------------------------------------------------------------------------------*/
METHOD PUBLIC Progress.Lang.Object AddService (poClass AS Progress.Lang.Class,
poObject AS Progress.Lang.Object).The method RemoveService removes a service instance from the Service Container (deregistration). The service to be removed from the service container is identified by the type of the service (instance of Progress.Lang.Class). The object instance is not explicitly deleted.
RemoveService syntax
/*------------------------------------------------------------------------------
Purpose: Removes the specified service type from the service container.
Notes:
@param poClass The reference to the class or interface of the service to remove from the service container.
------------------------------------------------------------------------------*/
METHOD PUBLIC VOID RemoveService (poClass AS Progress.Lang.Class).The method GetService requests a service instance from the service container. The service is identified by the type (instance of Progress.Lang.Class). When the requested service is not registered the unknown value is returned (null reference). This is the optional lookup — the caller is expected to handle a missing registration.
GetService syntax
/*------------------------------------------------------------------------------
Purpose: Gets the service object of the specified type.
Notes: Returns ? when no service of that type is registered
@param poClass The reference to the class or interface of the service to return
@return The reference to the instance of the service of ? when the service is not registered with the service container
------------------------------------------------------------------------------*/
METHOD PUBLIC Progress.Lang.Object GetService (poClass AS Progress.Lang.Class).The method GetMandatoryService is the mandatory counterpart of GetService. When no service of the requested type is registered, the call raises a Consultingwerk.Framework.Exceptions.ServiceNotRegisteredException rather than returning the unknown value.
GetMandatoryService syntax
/*------------------------------------------------------------------------------
Purpose: Gets the service object of the specified type.
Notes: Throws a ServiceNotRegisteredException when no service of that type is registered
@param poClass The reference to the class or interface of the service to return
@return The reference to the instance of the service
------------------------------------------------------------------------------*/
METHOD PUBLIC Progress.Lang.Object GetMandatoryService (poClass AS Progress.Lang.Class).The default Service Container
The SmartComponent Library provides a single service container as the session default service container. This service container is accessible from the Consultingwerk.Framework.FrameworkSettings:ServiceContainer reference. This reference is of the IServiceContainer interface. Customers requiring the use of a different ServiceContainer (a custom implementation) may assign a different reference to the static property in the FrameworkSettings class.
When the application is started it is guaranteed that always a default Service Container instance is available from the static property.
Custom Service Containers
Additional Service Container instances may be useful in complex applications as well. Consider a complex Form where multiple controls may need to interact with each other. Rather than providing properties in each of the controls and assign each other’s references that way a Service Container managed by the Form may be a simpler way to resolve the dependencies. With a Service Container scoped to the Form it will also be possible to manage the dependencies in multiple parallel instances of the same Form.
Service Interfaces
Service should be referenced through Interface types rather than through actual class names. This will allow application developers to leverage all of the advantages of dependency injection. In the SmartComponent Library we refer to the Interfaces used for requesting services from the Service Container as Service Interfaces (which is not to be misunderstood as the Service Interface in the OERA architecture).
Mandatory vs. optional services
Before requesting a service it is important to decide whether the consumer can run without it.
A mandatory service is one whose presence is a precondition for the consumer to do its work. The consumer is written under the assumption that the service is registered; if it is not, the configuration of the application is broken and the only sensible reaction is to fail fast with a clear error. Most framework-internal services fall into this category — for example, the Consultingwerk.SmartFramework.Authentication.IAuthenticationService used by the SmartFramework login flow.
An optional service is one that the consumer can degrade gracefully without. The consumer asks the container whether the service is available and, if not, takes an alternative path — skips a non-essential side effect, falls back to a default behaviour, or returns early. A typical example is Consultingwerk.Framework.IStatusManager, which a long-running operation publishes progress through if it is registered.
The two cases differ only in how a missing registration is reported, but they have very different consequences for the consumer:
Aspect | Mandatory service | Optional service |
|---|---|---|
Container method |
|
|
Include file |
|
|
Behaviour when not registered | Throws | Returns the unknown value ( |
Consumer-side check | None — the returned reference is always valid |
|
Use when | The consumer cannot do its job without the service | The service contributes optional behaviour |
Pick get-mandatory-service.i whenever you would otherwise have to write a valid-object check that immediately throws an error on failure — the include file does this for you, with a consistent exception type.
Pick get-service.i whenever the consumer is supposed to keep working when the service is not configured.
Requesting Services from the Service Container
In order to request a service from a Service Container developers need to use the GetService (or GetMandatoryService) method of the Service Container. Both methods expect the service type (service interface) as the only input parameter and return the service instance — GetService returns the unknown value when no service is registered, GetMandatoryService throws.
The raw call is verbose because it requires the caller to repeat the type name twice — once to look it up via Progress.Lang.Class and once to CAST the returned Progress.Lang.Object reference back to the desired interface:
GetService() sample using Progress.Lang.Class:GetClass()
DEFINE VARIABLE oSettingsService AS Consultingwerk.Framework.ISettingsService NO-UNDO .
oSettingsService = CAST (FrameworkSettings:ServiceContainer:GetService (Progress.Lang.Class:GetClass("Consultingwerk.Framework.ISettingsService")),
Consultingwerk.Framework.ISettingsService) .From OpenEdge 11.4 on, the GET-CLASS function might be used as an alternative to the Progress.Lang.Class:GetClass() method. The GET-CLASS function provides compile time verification of the type name as well as support for abbreviated class names through USING statements.
GetService() sample using GET-CLASS (OpenEdge 11.4)
USING Consultingwerk.Framework.* FROM PROPATH .
oSettingsService = CAST (FrameworkSettings:ServiceContainer:GetService (GET-CLASS (ISettingsService)),
ISettingsService) .In both cases it is required to CAST the retrieved service reference to the target interface. This makes the code unfriendly to readers and the type name appears two times — a frequent source of bugs whenever the type name is changed only in one of the two places.
The include-file pattern: a typed accessor for any service
The SmartComponent Library ships two single-line include files that wrap the lookup-and-cast pair into a single expression:
Consultingwerk/get-service.i— the optional lookup. WrapsGetService(orAddNewService, see below).Consultingwerk/get-mandatory-service.i— the mandatory lookup. WrapsGetMandatoryService.
The include files take the service type name as their first argument and reuse it both for the class lookup and for the CAST. Because the CAST is part of the include file, the result of the include-file expression is already typed as the requested interface. ABL does not have generic methods, so this include-file pattern is the closest equivalent: a single point of expansion that yields a strongly-typed reference for any service type without the caller writing the type name twice.
The benefits of using the include files instead of calling the container methods directly are:
The service type name appears only once in calling code — eliminating the class-of-bug where the lookup type and the cast type drift apart.
The expression is short enough to inline directly into a variable initialiser or into a method argument list, instead of forcing a separate
CASTstatement.The resulting code is uniform across the entire codebase, which makes it easy to grep for service consumers and to refactor service interfaces.
Optional service — sample using get-service.i
DEFINE VARIABLE oSettingsService AS Consultingwerk.Framework.ISettingsService NO-UNDO .
oSettingsService = {Consultingwerk/get-service.i Consultingwerk.Framework.ISettingsService} .
IF VALID-OBJECT (oSettingsService) THEN
oSettingsService:ApplyUserPreferences () .The get-service.i form returns ? when no ISettingsService is registered; the VALID-OBJECT guard expresses that the consumer is happy to skip the optional behaviour when the service is missing.
Mandatory service — sample using get-mandatory-service.i
DEFINE VARIABLE oAuthenticationService AS Consultingwerk.SmartFramework.Authentication.IAuthenticationService NO-UNDO .
oAuthenticationService = {Consultingwerk/get-mandatory-service.i Consultingwerk.SmartFramework.Authentication.IAuthenticationService} .
oAuthenticationService:Login (cUserName, cPassword) .Here the consumer relies on the service being registered. Calling Login on the returned reference is unconditional; if the service is missing, the get-mandatory-service.i expression itself throws a ServiceNotRegisteredException from inside the include — never returning to the caller — and the application's normal error handling reports the misconfiguration with a clear, specific exception type.
Comparison: get-service.i vs. get-mandatory-service.i
Property |
|
|
|---|---|---|
Underlying container method |
|
|
Behaviour when service is not registered | Returns | Throws |
Caller must handle missing service | Yes — always check with | No — the returned reference is guaranteed valid |
Optional second argument (default impl.) | Yes — see Lazy initialization below | No |
Best fit | The service contributes optional behaviour | The consumer cannot run without the service |
The include files use Progress.Lang.Class:GetClass() up to OpenEdge 11.3 and the GET-CLASS function from OpenEdge 11.4 on. For this reason it is required to pass the full type name (not relying on any USING statement) up to OpenEdge 11.3.
Adding Services to the Service Container
The SmartComponent Library and the WinKit provide various methods to add services to the Service Container.
Using the Service Loader
The most common way to add services to a service container is to provide an XML file (temp-table form) to the ServiceLoader class. The Service Loader then creates an instance of the classes provided in the XML file and registers them under the provided service type or service interface.
Static class vs. instance
The ServiceLoader can be used in two equivalent ways:
As a static class. The simplest form for the typical case where services are loaded into the default Service Container. The static methods
ServiceLoader:LoadFromFile,ServiceLoader:LoadFromFilesandServiceLoader:Loadtake care of constructing a transientServiceLoaderinstance, performing the load and disposing of it. This is the recommended form for services destined forFrameworkSettings:ServiceContainer.As an instance. Required when services should be loaded into a custom Service Container instead of the default one. The custom container is supplied to the
ServiceLoaderconstructor; theLoadmethod is then called on the resulting instance. The default constructorNEW ServiceLoader()(without an argument) is equivalent toNEW ServiceLoader (FrameworkSettings:ServiceContainer)— an instance bound to the default container.
Sample: static usage with the default Service Container
USING Consultingwerk.Framework.* FROM PROPATH .
ServiceLoader:LoadFromFile ("Consultingwerk/SmartComponentsDemo/CustomerExplorer/services.xml":U) .No NEW, no DELETE OBJECT — the static method constructs and disposes the loader internally.
Sample: instance usage with a custom Service Container
USING Consultingwerk.Framework.* FROM PROPATH .
DEFINE VARIABLE oFormContainer AS IServiceContainer NO-UNDO .
DEFINE VARIABLE oLoader AS ServiceLoader NO-UNDO .
oFormContainer = NEW ServiceContainer () .
oLoader = NEW ServiceLoader (oFormContainer) .
oLoader:Load ("Consultingwerk/SmartComponentsDemo/CustomerExplorer/services.xml":U) .
FINALLY:
DELETE OBJECT oLoader .
END FINALLY.Loading from a single file, multiple files, or a temp-table
Both the static and instance APIs accept the service definitions in three input shapes:
Input | Static method | Instance method |
|---|---|---|
Single XML file |
|
|
Multiple XML files |
|
|
In-memory |
|
|
When multiple files are provided, the loader merges them in order before registering anything — later files may override entries from earlier files, matched by ServiceTypeName. This makes it straightforward to layer a customer-specific services.xml on top of a base configuration without editing the base file.
Duplicate handling: ignore vs. error
Each Load / LoadFromFile / LoadFromFiles overload exists in two forms — one that takes a plIgnoreDuplicates flag and one that does not.
Default behaviour (
plIgnoreDuplicates = false). When the XML defines a service interface that is already registered in the target Service Container, the loader raises aServiceLoaderException. This is the right default when eachservices.xmlfile is expected to define a fresh, non-overlapping set of services — a duplicate then signals a configuration error.plIgnoreDuplicates = true. When a service interface is already registered in the target container, the loader silently skips that row and continues with the next one. The pre-existing registration wins. This is useful when several files are loaded in sequence and the first registration of each interface should take precedence, or when a partial reload should not disturb services that have already been wired up earlier in the session.
The overloads without a plIgnoreDuplicates parameter behave as if false were passed.
Note: “Duplicate” here means the same service interface is already registered in the Service Container, not the same row appears twice in the XML. Within the merged file set, later XML rows for the same
ServiceTypeNamesimply override earlier ones — that merge always happens before any registration is attempted.
Sample: error on duplicates (default)
USING Consultingwerk.Framework.* FROM PROPATH .
/* Throws a ServiceLoaderException if any service interface in the XML
is already registered in the default Service Container. */
ServiceLoader:LoadFromFile ("Consultingwerk/SmartComponentsDemo/CustomerExplorer/services.xml":U) .Sample: ignore duplicates
USING Consultingwerk.Framework.* FROM PROPATH .
/* Silently skips entries whose service interface is already registered. */
ServiceLoader:LoadFromFile ("Consultingwerk/SmartComponentsDemo/CustomerExplorer/services.xml":U,
TRUE) .Sample: layered configuration with multiple files
USING Consultingwerk.Framework.* FROM PROPATH .
DEFINE VARIABLE cFiles AS CHARACTER EXTENT 2 NO-UNDO .
ASSIGN cFiles[1] = "Consultingwerk/SmartFramework/services-base.xml":U
cFiles[2] = "App/Customer-overrides.xml":U .
/* later files override earlier ones for the same ServiceTypeName */
ServiceLoader:LoadFromFiles (cFiles) .services.xml file format
Sample services.xml file
<?xml version="1.0"?>
<ttServiceLoader xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ttServiceLoaderRow>
<Order>1</Order>
<ServiceTypeName>Consultingwerk.OERA.Context.IContextDatasetFactory</ServiceTypeName>
<ServiceClassName>Consultingwerk.OERA.Context.ContextDatasetFactory</ServiceClassName>
</ttServiceLoaderRow>
<ttServiceLoaderRow>
<Order>2</Order>
<ServiceTypeName>Consultingwerk.RollbaseAdapter.IRollbaseCredentials</ServiceTypeName>
<ServiceClassName>Consultingwerk.RollbaseAdapter.RollbaseCredentials</ServiceClassName>
</ttServiceLoaderRow>
</ttServiceLoader>The ttServiceLoaderRow may have the following nested tags:
Tag Name | Description |
|---|---|
Order | INTEGER The order in which services should be loaded, non-unique primary index |
ServiceTypeName | CHARACTER Comma-delimited list of service interface types under which the service instance should be registered (1 or many) |
ServiceClassName | CHARACTER The name of the class to create the instance from |
Disabled | LOGICAL Optional, set to yes to disable processing of the current row |
RequiredDatabases | CHARACTER Comma-delimited list of logical database names required to load the given entry. When the databases are not connected, the current row is ignored |
LazyLoading | LOGICAL Optional, set to yes to defer instantiation of the service class until it is first requested from the container (a |
Removing services with the Service Loader
ServiceLoader also exposes Unload (instance) and UnloadFromFile (static) methods that take the same XML file format and remove the listed service registrations from the container. An optional plForceDelete flag additionally calls delete object on each removed service instance.
ServiceLoader:UnloadFromFile ("Consultingwerk/SmartComponentsDemo/CustomerExplorer/services.xml":U) .Manually
In cases that services may require parameters to their constructor or are only needed under certain circumstances it is possible to register services manually using the ServiceContainer:AddService method:
Sample loading services manually
DEFINE VARIABLE oSmtpConfiguration AS SmtpConfiguration NO-UNDO .
oSmtpConfiguration = NEW SmtpConfiguration () .
oSmtpConfiguration:SmtpHostName = oConfigurationProvider:GetValue ("SmtpHostName":U) .
oSmtpConfiguration:SmtpPassword = oConfigurationProvider:GetValue ("SmtpPassword":U, "":U) .
oSmtpConfiguration:SmtpPortNumber = INTEGER (oConfigurationProvider:GetValue ("SmtpPortNumber":U, ?)) .
oSmtpConfiguration:SmtpSenderName = oConfigurationProvider:GetValue ("SmtpSenderName":U, "":U) .
oSmtpConfiguration:SmtpUserName = oConfigurationProvider:GetValue ("SmtpUserName":U, "":U) .
FrameworkSettings:ServiceContainer:AddService (Progress.Lang.Class:GetClass ("Consultingwerk.Framework.ISmtpConfiguration":U),
oSmtpConfiguration) .Lazy initialization with get-service.i
get-service.i supports a second, optional argument: a NEW expression that creates a default implementation. With the second argument supplied, the include file behaves like a register-on-first-use helper rather than a plain optional lookup. This is a distinct usage pattern from the optional-service lookup described in Requesting Services from the Service Container — it is, in effect, a third way of getting a service into the Service Container, alongside the Service Loader and Manually options above.
The expansion of the include file in this form is:
Ask the container for the service.
If a service is already registered, return it.
Otherwise, evaluate the
NEWexpression, register the new instance under the service type viaAddNewService, and return that instance.
The result is that the consumer always gets back a valid object — but registration of the default implementation is deferred until the service is actually needed for the first time.
Sample using get-service.i to load service instance at first usage
DEFINE VARIABLE oAdapter AS IRollbaseAdapter NO-UNDO .
oAdapter = {Consultingwerk/get-service.i Consultingwerk.RollbaseAdapter.IRollbaseAdapter
"NEW Consultingwerk.RollbaseAdapter.RollbaseAdapter()"} .This code sample returns an instance of the IRollbaseAdapter service. When no service of that type is registered yet, a new instance of the RollbaseAdapter class will be created (DYNAMIC-NEW) and registered in the Service Container as the IRollbaseAdapter service. Subsequent calls in the same session will return that same instance — the lazy initialisation runs at most once.
This pattern is useful when a service:
has a sensible default implementation that ships with the framework, but
should still be replaceable through the regular service registration mechanism (the
ServiceLoaderor a manualAddServicecall earlier in the session).
A consumer that uses get-service.i with a default does not need to know whether anyone has pre-registered an alternative implementation. The container takes care of preferring an explicit registration over the default.
Note: the second-argument variant is only available on
get-service.i.get-mandatory-service.ideliberately has no default implementation parameter — a mandatory service that has a default would not be mandatory.
At first request through IServiceCreator callback
See IServiceCreator factories to create Service instances at first usage for detailed instructions.
Related documentation
Overview of service.xml files