Sun, Apr 24 2005 11:18 PM
Olaf Conijn
IOC3: Once me we got rid of the new operator…
[Follow-up on ‘IOC2: Enabling SOA-like services on non-enterprise scale projects.’]
Instantiation of types is usually done by the runtime. In code, you instruct the runtime to make a new instance out of a given type using the new operator and the expression evaluates into a new instance. Leaving the instantiation logic over to the runtime limits an implementer to put initialization logic in the type’s constructor.
In the .net framework there is no way to override the runtime’s default behavior of type instantiation. You can’t return your own instances from a class constructor; there is no way to manage an instances lifecycle from within a constructor.
Solving the lifecycle problem usually is done by implementing a Singleton pattern in your classes design and disabling the constructor for external use. I’d like to introduce the concept of a ‘Container’ to manage lifecycle support and add some other (less known) patterns enabled by encapsulating type instantiating.
A Container can be looked at as a generic object factory. It encapsulates the frameworks behavior of instantiating types. Types that you wish should be controlled in such a way can be added to a Container. Instances can be consumed through a container by calling a generic ‘CreateInstance’ method. Here I called the method ‘CreateService’; I’ll stay in a SO A&D mode and continue to call instances served by the Container as ‘services’.
Patterns enabled by encapsulating the runtime’s instantiation of services:
(code snippets refer to previous post)
Singleton pattern
When service definitions are added to the container (either from configuration or code) the service definition should contain information on how the services lifecycle is managed. When services are marked as ‘static’ it is the Containers job to cleanup the services when its own finalization occurs.
/*done from configuration*/
Container.AddService(
typeof(ITinyTextService),
typeof(ResourceTinyTextService),
LifeCycle.Static);
Dependency injection pattern
Services that are contained by the same container could be automatically wired into each other by exposing fields or constructor arguments. When using interface types to identify services, the type of a field or constructor argument sets up the requirements that should be met for the container when wiring a service.
class DefaultLoginFunctionality
{
//constructor params are injected by the container this class belongs to
//if no service of the given type is available null is passed.
public DefaultLoginFunctionality(
ITinyTextService resourceManager,
ILogWriterService errorLogger)
{
_resourceManager = resourceManager;
_errorLogger = errorLogger;
}
}
Configuration injection pattern
Services that require configuration can expose configuration values as fields or constructor arguments in order for the container to inject configuration values private to the service instantiated.
public class ResourceTinyTextService
{
public ResourceTinyTextService(
string baseName,
string assemblyName)
{
if (baseName == null)
throw new ConfigurationException("baseName not defined in configuration");
/*...*/
}
}
Aspect weaving pattern
When a Container allows aspect to be added, these could be mixed with service implementations prior to returning the service.
public class ResourceTinyTextService
{
[Cacheable(VaryByParam="*")] //attribute recognized by caching aspect.
string ITinyTextService.GetText(string key, CultureInfo culture)
{
return _resourceManger.GetString(key, culture);
}
}
/*done from configuration*/
Container.AddAspect(
typeof(CachingAspect));
Containers work from configuration and behavior and availability of services usually depends upon the programs environment (think of development environments/ testing/ staging/ production). Also because the behavior of a Container can be configured, behaviors could change runtime.
With the configurations flexibility described above (and good tooling to manage configuration) in mind you could consider the following scenarios:
- When developing web applications your development environment might not have the need for logging and exception management the production environment does. These services could be coded against, though only be available in a production environment
- On a production environment you might want to cache a methods output. If this is something that you would not like it to do on your development machine (or left this concern out of your programs design until load increases) a caching aspect could be loaded into your production server’s configuration later on.
- When a sysadmin encounters unexpected behavior on one of the servers he could configure the environments configuration to log more verbose output in order to collect debug information.