spring.ioc
A Spring.NET Implementation
Enhancing Extensibility
Dependency Resolution
Conclusion
Today there is a greater focus than ever on reusing existing components and wiring together disparate components to form a cohesive architecture. But this wiring can quickly become a daunting task because as application size and complexity increase, so do dependencies. One way to mitigate the proliferation of dependencies is by using Dependency Injection (DI), which allows you to inject objects into a class, rather than relying on the class to create the object itself.
The use of a factory class is one common way to implement DI. When a component creates a private instance of another class, it internalizes the initialization logic within the component. This initialization logic is rarely reusable outside of the creating component, and therefore must be duplicated for any other class that requires an instance of the created class. For example, if class Foo creates an instance of class Bar and instances of Bar require several initialization steps, different for each instance of Bar, other classes that create instances of Bar will have to reproduce the same initialization logic found within Foo.
Developers like to automate monotonous and menial tasks, and yet most developers still perform functions such as object construction and dependency resolution by hand. Dependency resolution can be described as the resolving of defined dependencies of a type or object. Dependency Injection, on the other hand, aims to reduce the amount of boilerplate wiring and infrastructure code that you must write.
Containers provide a layer of abstraction in which to house components. DI containers, in particular, reduce the kind of dependency coupling I just described by providing generic factory classes that instantiate instances of classes. These instances are then configured by the container, allowing construction logic to be reused on a broader level.
Before diving into DI containers, let's first review a core pattern used through DI containers, the Abstract Factory pattern.
Factory Patterns Refresher
In Design Patterns (Addison-Wesley, 1995), authors Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides describe the intent of the Abstract Factory pattern like this: "To construct and instantiate a set of related objects without specifying their concrete objects." Utilizing the Abstract Factory pattern in your applications allows you to define abstract classes to create families of objects. By encapsulating the instantiation and construction logic, you retain control over the dependencies and the allowable states of your objects.
Frequently, objects need to be instantiated in a coordinated fashion, usually because of certain dependencies or other requirements. For example, when creating an instance of System.Xml.XmlValidatingReader in client code, an XmlSchemaCollection object is frequently populated with the relevant schemas for use when validating the XmlValidatingReader object. This is an example of needing to not only create an instance of a class, but also to configure it after creation and before it can be used.
Another type of factory pattern is called the factory method. A factory method is simply a method, usually defined as static, whose sole purpose is to return an instance of a class. Sometimes, in the case of facilitating polymorphism, a flag will be passed to the factory method to indicate the specific interface implementation or subclass to be returned. For example, the Create method of WebRequest takes in either a string or Uri instance, and returns a new instance of a class derived from WebRequest.
From this point forward, I will simply use the word "factories" to mean both the Abstract Factory pattern as well as the factory method implementation.
DI Implementation Using Factories
Factories allow for an application to wire together objects and components without exposing too much information about how the components fit together or what dependencies each component might have. Instead of spreading complex creation code around an application, factories allow for that code to be housed in a central location, thereby facilitating reuse throughout the application. Client code then calls creation methods on the factory, with the factory returning complete instances of the requested classes. Encapsulation is preserved, and the client is effectively decoupled from any sort of plumbing required to create or configure the object instance.
Figure 1 Factory Functions
Factories can do more than simply create objects and assemble their dependencies. They can also serve as a central configuration area for applying services or constraints uniformly across all instances of an object (see Figure 1). For example, instead of returning an instance of an object, a factory can return a proxy to the real instance of the object, thereby enabling distributed method calls. Since the client application is unaware that the object it is being handed is, in fact, a proxy, as opposed to the real instance of the object, no changes to the client code need to occur. An example of this type of service can be found within the .NET remoting infrastructure. Distributed objects can be configured declaratively, with a .NET configuration file, and the client application can simply create an instance of the class using "new". This is the same for local and distributed objects, as well as for client-activated objects and server-activated objects. All of this configuration and management takes place without the client application knowing about .NET remoting.
However, factories are not without drawbacks. While a factory implementation can be quite valuable for a certain application, most of the time, it is not reusable across other applications. Frequently all of the available creation options are hardcoded into the factory implementation, making the factory itself non-extensible. Also, most of the time the class calling the factories' creation methods must know which subclass of the factory to create.
Secondly, all dependencies for an object that is created using a factory are known to the factory at compile time. Leaving .NET reflection out of the picture for a moment, at run time there is no way to insert or alter the manner in which objects are created, or which dependencies are populated. This all must happen at design time, or at least require a recompile. For example, if a factory is creating instances of class Foo and instances of class Foo require an instance of class Bar, then the factory must know how to retrieve an instance of the Bar class. The factory could create a new instance, or even make a call to another factory.
Third, since factories are custom to each individual implementation, there can be a significant level of cross-cutting infrastructure that is held captive inside a particular factory. Take my example of a factory dynamically substituting a proxy object for a real object. That is an example of a piece of infrastructure, namely the wrapping of simple objects for deployment over a distributed wire, that is completely encapsulated inside that particular factory. If another object needs to be altered in a similar manner, the logic to do so is hidden inside a factory, and would have to be repeated for the other object. Once this functionality is desired outside of the original application, the problem now becomes how to reuse such functionality while still maintaining the existing factory concept.
Lastly, factories rely on well-defined interfaces to achieve polymorphism. In order for a factory implementation to be able to dynamically create different concrete subclass instances, there must be a common base class or shared interface implemented by all classes that the factory will create. Interfaces decouple the construction of the object from the specific implementation of the interface. The dilemma that arises now is how you can accomplish this decoupling without being forced to create an interface for everything.
These are just some of the problems facing DI implemented using conventional factory implementations. However, as you will see shortly, another viable option exists. Also, DI is not based solely around the factory pattern and in fact has many correlations with many other patterns, including the Builder, Assembly, and Visitor patterns. For more information on these useful patterns, the Design Patterns book (already mentioned) is the seminal reference.
Abstracting DI Using Containers
Many of the previous shortcomings to DI can be solved by using a container. A container is a compartment that houses some sort of abstraction within its walls. Typically, responsibility for object management is taken over by whatever container is being used to manage those objects. However, containers can also take over instantiations, configuration, as well as the application of container-specific services to objects.
Containers allow for objects to be configured by the container, as opposed to being configured by the client application. This allows for the container to serve a wide range of functions, such as object lifecycle management and dependency resolution. In addition, containers can apply cross-cutting services to whatever construct is being hosted inside the container. A cross-cutting service is defined as a service that is generic enough to be applicable across different contexts, while providing specific benefits. An example of a cross-cutting service is a logging framework that logs all method calls within an application.
Containers vs. Factories
There are several reasons to use containers in your application development. Containers provide the ability to wrap vanilla objects with a wealth of other services. This allows the objects to remain ignorant about certain infrastructure and plumbing details, like transactionality and role-based security. Oftentimes, the client code does not need to be aware of the container, so there is no real dependency on the container itself.
These services can be configured declaratively, meaning they can be configured via some external means, including GUIs, XML files, property files, or vanilla .NET-based attributes.
Containers that have cross-cutting services are also reusable across applications. One container can be used to configure objects across various applications within an enterprise. Many services that can be applied across an enterprise are low-level infrastructure and plumbing services. These services can be used across an enterprise without the need to deeply embed container-specific code or logic within an application.
Containers Are Not New
Containers have been around in some form or another for many years. As a matter of fact, containers were used back when Microsoft® Transaction Server (MTS) was released as part of the Windows NT® 4.0 option pack.
Containers are still an active part of Microsoft enterprise development strategy today. In fact, if you're writing .NET-based code, you're already using a container to deploy your application: the .NET common language runtime (CLR). The CLR performs a wide variety of important tasks at run time, including memory management, automatic bounds checking and overflow protection, and method call security, to name a few.
The next version of MTS, dubbed COM+, was a major evolution. The .NET equivalent, Enterprise Services, is still the recommended approach to constructing distributed enterprise applications. COM+ and Enterprise Services offer a wealth of services beyond what Microsoft Transaction Server originally offered. In the current version, this includes object messaging, object pooling, declarative automatic transactions, loosely coupled events, and role-based security.
The problem with some containers is that they can be costly. Despite being built upon a fairly stable architecture, the current container technology available to developers using .NET has some drawbacks. They require container-specific constructs to be introduced into domain code. Container infrastructure can adversely impact performance for many operations, even if only minimally.
An example of requiring container-specific constructs can be found in the Microsoft .NET Framework 1.x Enterprise Services requirement that any object that is under its control must derive from the ServicedComponent class. Since .NET does not support multiple inheritance, this constraint limits where Enterprise Services can be utilized.
Since heavyweight containers impact performance and increase the complexity of the client application, they are usually employed in only the largest distributed applications.
Microsoft also offers built-in support for a lightweight version of Dependency Injection, with the System.ComponentModel namespace. Unlike EnterpriseServices, it does not provide any extra services or functionality; it merely provides service injection. However, like Enterprise Services, in order to use the classes within the System.ComponentModel namespace, your classes must become container-aware. This is accomplished by implementing certain container-specific interfaces.
Lightweight Containers
There are a great number of applications that would benefit from many of the features of the containers I described, but their needs don't justify the use of a heavyweight container. At the opposite end of the containers spectrum, lightweight containers provide many of the same benefits that the heavyweight containers do, but without all of the overhead of COM+ and Enterprise Services. Many organizations still choose to use Enterprise Services in spite of the existence of lightweight containers, but this situation is changing. Many of these lightweight containers offer services in addition to simple DI. The containers can often be configured to add other valuable services to your objects.
Spring.NET
You can build your own lightweight DI container, though a few implementations of such systems already exist which you might consider taking advantage of. One such solution is Spring.NET, which I'll be using for the rest of this column to demonstrate some of the ideas discussed thus far. Spring.NET offers a lightweight DI container built around the concept of factories. It not only provides DI by allowing users to use pre-built factories within their code, but it also provides a suite of services that can be applied to any object under Spring.NET's control. And since Spring.NET is built using standard .NET-based code, applications that utilize Spring.NET have no additional dependency on COM, COM+, or Enterprise Services.
Factory Example
The following code shippet is a simple interface, IDomainObjectInterface, which my objects will implement. The interface contains one property, which returns a string representation of the name of my object:
复制代码
public interface IDomainObjectInterface { string Name{ get; } }
The code in Figure 2 contains two classes that implement the interface. As you can see, the Name property simply returns the name of the class, depending on which concrete class is used. I will use these two classes, as well as the interface they implement, as the basis of the example I will present.
Figure 2 Implementation Classes
复制代码
public class ImplementationClass1 : IDomainObjectInterface { public ImplementationClass1(){} public string Name { get { return "Implementation Class 1"; } } } public class ImplementationClass2 : IDomainObjectInterface { public ImplementationClass2(){} public string Name { get { return "Implementation Class 2"; } } }
Typically, either of these two classes would be created by a factory class, similar to the one in Figure 3. In addition to the factory class, ImplementationClassFactory, Figure 3 also contains one enumeration, ImplementationClassType. The factory class has one method, GetImplementationClass, which accepts one of the enumeration values. Based on the value of the enumeration, one of the two IDomainObjectInterface implementations will be returned. The client class is responsible for choosing which implementation it would like to use.
Figure 3 Factory Class
复制代码
public enum ImplementationClassType { ImplementationClass1, ImplementationClass2 } public class ImplementationClassFactory { public static IDomainObjectInterface GetImplementationClass( ImplementationClassType implementationClassType ) { switch ( implementationClassType ) { case ImplementationClassType.ImplementationClass1: return new ImplementationClass1(); case ImplementationClassType.ImplementationClass2: return new ImplementationClass2(); default: throw new ArgumentException("Class " + implementationClassType + " not supported." ); } } }
Now, there are several drawbacks to this factory method. The first is that the number of implementation classes is hardwired into the factory method. Therefore, even though there is an interface for the implementation objects, it's impossible for the factory method to return an implementation class that it does not know about. This limits extensibility, especially in the case of a public API and application frameworks, where the ability to dynamically introduce new implementation classes is not only desired, but often expected to achieve a certain degree of flexibility.
Secondly, even if the ability to dynamically introduce new implementations existed, the client application would still need to know which class to ask for. This eliminates some of the flexibility that the factory class was supposed to provide.
The ConsoleRunner class in Figure 4 illustrates how a client would use the factory class to create an instance of the desired implementation class. Notice how the client code has to explicitly ask for the desired implementation class. At this point many of the benefits of the factory class have been negated.
Figure 4 Using the Factory Class
复制代码
using System; using SpringDIExample; class ConsoleRunner { static void Main() { IDomainObjectInterface domainObjectInterface = ImplementationClassFactory.GetImplementationClass( ImplementationClassType.ImplementationClass1); Console.WriteLine("My name is " + domainObjectInterface.Name); } }
A Spring.NET Implementation
Now that you have seen the typical factory pattern, let's take a look at how a DI container not only achieves many of the same goals, but also adds a significant amount of flexibility and functionality to your application.
Figure 5 contains an updated version of the ConsoleRunner class. For this example I'm using the Spring.NET DI container, which requires a bit of initial setup. First, you must create an instance of the factory, using a config.xml file as the source of your object definitions. Next, replace a call to the custom factory class with a call to the Spring.NET factory class. Notice that since the generic factory doesn't know anything about third-party interfaces, everything coming back from the factory is downcast to object. So you must upcast the object instances returned by the factory to the interface that you expect. Finally, the last line of the ConsoleRunner class remains unaffected, even though you have changed the source of the object and how it's instantiated.
Figure 5 ConsoleRunner Using Spring.NET
复制代码
using System; using System.IO; using Spring.Objects.Factory.Xml; using SpringDIExample; class ConsoleRunner { static void Main() { // 1. Open the configuration file and create a new // factory, reading in the object definitions using (Stream stream = File.OpenRead("config.xml")) { // 2. Create a new object factory XmlObjectFactory xmlObjectFactory = new XmlObjectFactory(stream); // 3. Call my factory class with generic label for the object // that is requested. IDomainObjectInterface domainObjectInterface = (IDomainObjectInterface)xmlObjectFactory. GetObject("DomainObjectImplementationClass"); // 4. Use the object just like any other concrete class. Console.WriteLine("My name is " + domainObjectInterface.Name); } } }
Now, let's take a look at the make up of the config.xml file, which drives the factory class. Here is the full config.xml file:
复制代码
<?xml version="1.0" encoding="utf-8" ?> <objects xmlns="http://www.springframework.net"> <object name="DomainObjectImplementationClass" singleton="false" type="ImplementationClass1, SpringDIExample" /> </objects>
You will notice that the configuration file is not very large, being comprised of only two elements, <objects>, which contain all of the object definitions, and the individual <object> definitions. From the configuration file shown, you can see that there is one object definition. The object definition contains three basic attributes that define what object is created as well as how it's created.
The name attribute defines the name that my ConsoleRunner class uses when requesting an object from the factory. In this case, it is "DomainObjectImplementationClass". This name is just used to reference the definitions contained in the configuration file from the client code.
Next, the singleton attribute is a Boolean flag that designates if the object should be created as a singleton or not. Spring.NET has built-in support for making objects singletons, but since I do not require such functionality, I set this attribute to false.
Finally, the type attribute defines the actual type of the object to be created. This is the type that will actually be loaded and returned when the factory is queried. This string takes the form of "Type, Assembly", indicating not only the type of object, but also which assembly the type is located in. As indicated in the configuration file, the type desired is one of the types shown in Figure 2.
By simply changing the type of the implementation class listed in the configuration file from "ImplementationClass1" to "ImplementationClass2", you can dynamically alter the class that is returned to the client, all without a recompile.
Enhancing Extensibility
Until now, I have simply moved responsibility of object creation to an external factory implementation and configuration file. While this type of declarative configuration can be seen as more desirable than a static, custom factory implementation, there is much more that can be accomplished by using containers.
Let's say that you have published the IDomainObjectInterface in a public API, and you would like to allow users of the API to create their own implementations of the interface, all while still utilizing several existing clients that have already been built to use the IDomainObjectInterface. Getting the users' implementation to your clients could prove to be difficult, especially since you know nothing about how the class will be built or deployed. ImplementationClass3 is a third implementation of the IDomainObjectInterface, which is similar to the ImplementationClass1 and ImplementationClass2 classes, except this one resides in a separate assembly. Previously, both implementation classes and the interface resided in the same assembly:
复制代码
public class ImplementationClass3 : IDomainObjectInterface { public ImplementationClass3(){} public string Name { get { return "Implementation Class 3"; } } }
Using a framework like Spring.NET, getting my ConsoleRunner class to use the new ImplementationClass3 is easy to accomplish, and only requires a minor change to my original config.xml configuration file. The following is the configuration file with the necessary changes made. The only difference lies within the type attribute, which has been updated to point to the ImplemenationClass3 type and the SimpleDIExampleExtension assembly:
复制代码
<?xml version="1.0" encoding="utf-8" ?> <objects xmlns="http://www.springframework.net"> <object name="DomainObjectImplementationClass" singleton="false" type="ImplementationClass3, SpringDIExampleExtension"/> </objects>
When the ConsoleRunner class is rerun, an instance of ImplementationClass3 will be instantiated and returned. All of this is accomplished without a recompile of ConsoleRunner, even though ImplemenationClass3 resides in an assembly that's physically separate from the original implementation classes.
Dependency Resolution
Now that you have seen how containers can aid in object creation, let's take a look at how dependencies between objects are handled. The following class, DependentClass, has one read/write property that contains a message for the class to hold:
复制代码
public class DependentClass { private string _message; public DependentClass(){} public string Message { set { _message = value; } get { return _message; } } }
I will configure the container to automatically insert a message into the DependentClass.Message property. I will also dynamically insert an instance of the configured DependentClass object into the ImplemenationClass4.DependentClass property.
Figure 6 shows a new implementation of IDomainObjectInterface, ImplemenationClass4. As you can see, not only does ImplementationClass4 implement the IDomainObjectInterface, it also has an extra property, DependentClass, which holds an instance of DependentClass.
Figure 6 New Implementation of IDomainObjectInterface
复制代码
public class ImplementationClass4 : IDomainObjectInterface { private DependentClass _dependentClass; public ImplementationClass4(){} public DependentClass DependentClass { get { return _dependentClass; } set { _dependentClass = value; } } public string Name { get { return _dependentClass.Message; } } }
Figure 7 shows my updated config.xml file. There are three changes from the configuration files shown previously. The first is the addition of a new <object> element that configures an instance of the DependentClass to be used. All of the previously explained attributes of the <object> element are present, but this object definition has an extra element underneath the main object definition. The <property> element configures a property for a given object definition. In this case the name attribute contains the name of the property to populate; here it's DependentClass.Message. Since the DependentClass.Message property is a basic type, its configured value is wrapped in <value> tags. The text contained inside these tags is the configured value of the DependentClass.Message that will be populated at instantiation.
Figure 7 Updated config.xml
复制代码
<?xml version="1.0" encoding="utf-8" ?> <objects xmlns="http://www.springframework.net"> <object name="DomainObjectImplementationClass" singleton="false" type="ImplementationClass4, SpringDIExample"> <property name="DependentClass"> <ref object="DomainObjectDependentClass"/> </property> </object> <object name="DomainObjectDependentClass" singleton="false" type="DependentClass, SpringDIExample"> <property name="Message"><value>Dependent Class</value></property> </object> </objects>
The second change concerns my original DomainObjectImplementationClass definition. A new <property> element has been added to the definition, configured to populate the ImplementationClass4.DependentClass property. Since the value of this property is an instance of a complex type, a <ref> tag is used instead of wrapping the value in <value> tags. The object attribute of the <ref> tags references the name of a previously configured object definition, in this case, "DomainObjectDependentClass".
The last change to the configuration file can be found within the first object definition. I have updated the type reference to use the ImplementationClass4 class.
Now, redeploy the new config.xml file, and rerun the ConsoleRunner class. Notice that the configured DependentClass.Message property is displayed. The dependencies have been populated and resolved and the client app is using the new classes, all without knowing what class it's using and all without requiring a recompile.
Conclusion
Dependency Injection is a worthwhile concept to explore for use within apps that you develop. Not only can it reduce coupling between components, but it also saves you from writing boilerplate factory creation code over and over again. Spring.NET is an example of a framework that provides a ready to use DI container, but it is not the only .NET lightweight container out there. Other containers include Pico and Avalon.