Creating Dynamic Factories in .NET Using Reflection | ||
|
||
Download the code for this article: DesignPatterns0303.exe (79KB) |
||
Two of the sacred tenets of sound software design are keeping elements loosely coupled, yet highly cohesive. Coupling determines how strongly or closely related software components are, while cohesion relates to how well focused a software unit is, whether it's a method, class, or class library. The Concrete Factory Design Pattern The Concrete Factory is one of the most widely used patterns. In order for an object A to send a message to object B, A must have a reference to B, which means that class B must be instantiated and a reference to object B must be available to A. If A instantiates B directly, A has a direct reference to B, but now they are tightly coupled, since A must know how to create B. In order to reduce tight coupling between these two objects, it is best to give the responsibility of creating B to a factory class, C, so that in addition to reducing the strength of coupling between A and B, other potential consumers of B can request an instance of B from C as well. This is what the Concrete Factory design pattern provides. Note that coupling is not eliminated between objects A and B; rather, it is weakened because A no longer needs to know how to create B. A further weakening of this coupling would be achieved if factory class C returned an interface to object B (let's call it I), rather than the concrete object itself. This would ensure that no matter how many implementations of I exist, object A would only know of I. The reason an interface renders coupling even weaker is that object A is further abstracted from new implementations of I. In the Factory pattern there is a client, a factory, and a product. The client is any object that needs an object from the factory. The product is the object returned to the client by the factory. Common Implementations There are quite a few variations of the Factory pattern. I'll look at the pros and cons of two of the most common approaches, then I'll introduce a new approach that uses reflection. The first implementation I'll examine uses abstract and concrete factories. This implementation should not be confused with the Abstract Factory pattern, which handles the creation and return of families of objects. Suppose you have a computer parts store inventory application in which InventoryMgr is the Client class, PartsFactory, MonitorInvFactory, and KeyboardInvFactory make up the factory, and IPartsInventory is the product returned by the factory. The Unified Modeling Language (UML) diagram in Figure 1 illustrates this implementation. Here, MainClass is a class that sends a request to InventoryMgr. Figure 1 Abstract and Concrete Factories In Figure 1, MainClass simply represents an object that passes on the user's request to InventoryMgr. If MainClass needed to replenish the inventory of monitors, for example, it would send a message to the InventoryMgr (see Figure 2). Take a look at the code in the abstract factory PartsFactory and the concrete factories MonitorInvFactory and KeyboardInvFactory in Figure 3. Although this is a reasonable approach to implementing the Factory pattern, it has some shortcomings. Consider how coupling manifests itself and to what degree it is present in this pattern. The InventoryMgr class looks good (coupling is low) since it's shielded from knowing about any of the concrete factories. The addition of new concrete factories in the future will not affect it. InventoryMgr is also decoupled from the different implementations of IPartsInventory by having a reference to the interface instead of a concrete object. So, what is the problem? One of the axioms in object-oriented design is "don't talk to strangers," meaning that objects should not have a reference to objects they do not absolutely need in order to function properly. If you look at the code fragment for MainClass in Figure 2, this rule is violated because MainClass creates concrete factories. MainClass does not truly need to know about concrete factories since it does not send a message to them. It simply creates them and passes them on to InventoryMgr. This results in an unnecessary coupling between MainClass, MonitorInvFactory, and KeyboardInvFactory. As a general rule, objects should only create other objects if they plan to send a message or messages to those objects, unless their main responsibility is the creation and return of those objects. Factories, for instance, are such classes. An adverse effect of unnecessary coupling is lower cohesion. MainClass has lowered its cohesion as well since, given its role as a message forwarding agent, it should not concern itself with what concrete factories are needed to process the request. Its focus should simply be taking a request, possibly repackaging it, and sending it to InventoryMgr for further processing. As you'll see later on, reflection helps remove unnecessary coupling and improve cohesion in all classes that participate in the Factory pattern. Let's examine another popular implementation of the Factory pattern. This approach does not make use of abstract factories. It only uses a concrete factory to create objects; therefore it is considerably less complex. Let's take a look at what the code looks like in MainClass, InventoryMgr, and PartsFactory. IPartsInventory has not changed (see Figure 2). MonitorInvFactory and KeyboardInvFactory (see Figure 3) are no longer needed, since a single concrete factory class in now being used. The most striking change is the addition of an enumerator, enmInvParts. Figure 4 shows that the code in MainClass is significantly cleaner, less coupled, and more cohesive than the previous implementation in Figure 2. MainClass is no longer "talking to strangers." Instead of being coupled to factories, it is now coupled to the enumeration enmInvParts, which simply provides a repackaging mechanism of the command-line request. Repackaging the request comes naturally to MainClass since it fits well with its responsibility of simply passing on the request to InventoryMgr. Although InventoryMgr is coupled to PartsFactory, coupling has not truly changed, since InventoryMgr was previously coupled to the abstract version of PartsFactory by means of parameter passing (see Figure 2). At first glance, it looks like the coupling and cohesion issues have disappeared, so I can pop the champagne cork and declare victory. Well, not yet. If you take a closer look, you see that the coupling has moved from MainClass to PartsFactory. Although PartsFactory is now coupled to IPartsInventory and its implementor objects, coupling does not affect it nearly as adversely as MainClass was affected in the implementation of the Factory pattern in Figure 2 because class cohesion has not deteriorated. In other words, PartsFactory remains focused. If you could remove this hardcoded coupling from the factory, you would achieve the lowest possible coupling and highest cohesion in all the classes that make up the implementation of the Factory pattern. Stay tuned; next I'll describe how reflection can get you pretty close to that lofty goal. A Dynamic Factory Using Reflection Reflection is simply a mechanism that allows components or classes to interrogate each other at run time and discover information that goes beyond the information gleaned from the publicly available interfaces the objects expose. In essence, reflection enables objects to provide information about themselves (metadata). The .NET Framework provides objects the ability to describe themselves through the use of attributes. An attribute is declared as a class that inherits from System.Attribute. Once an attribute is defined, it can be attached to types such as interfaces, classes, or an assembly. The code in Figure 5 defines two attributes. The first attribute is InventoryPartAttribute, which will be attached to the classes that implement the IPartsInventory interface in order to specify which type of parts they handle. The following code illustrates how this is achieved: [InventoryPartAttribute(enmInvParts.Monitors)] class MonitorInventory : IPartsInventory { public void Restock() { Console.WriteLine("monitor inventory restocked"); } } [InventoryPartAttribute(enmInvParts.Keyboards)] class KeyboardInventory : IPartsInventory { public void Restock() { Console.WriteLine("keyboard inventory restocked"); } } The second attribute defined in Figure 5 is attached to the IPartsInventory interface. This allows for the interface to be interrogated about which types implement it, like so: [ImplAttr(new Type[]{typeof(MonitorInventory),typeof(KeyboardInventory)})] interface IPartsInventory() { public void Restock(); } So far, I have created two attributes and attached them to both the IPartsInventory interface and the classes that implement it (MonitorInventory and KeyboardInventory). The class with the largest amount of code changes is PartsFactory. This should certainly be no surprise, since it is the class whose hardcoded switch statement I'm trying to replace with code that's more dynamic through the use of attributes. Let's examine each line of code of the newly modified PartsFactory class in Figure 6. The first line retrieves the ImplAttr attribute of the IPartsInventory as shown here: Attr = Attribute.GetCustomAttribute(typeof(IPartsInventory), typeof(ImplAttr));Next, I cast the Attr attribute to my custom ImplAttr attribute and the array of types that implement IPartsInventory is retrieved by reading the attribute's ImplementorList property: IntrfaceImpl = ((ImplAttr)Attr).ImplementorList;I determine the number of classes that implement the interface by obtaining the length of the IntrfaceImpl array: ImplementorCount = IntrfaceImpl.GetLength(0);Next, I loop through the IntrfaceImpl array. The first thing I do within the loop is retrieve the InventoryPartAttribute attribute of each interface implementor class: Attr = Attribute.GetCustomAttribute(IntrfaceImpl[i], typeof(InventoryPartAttribute));Then, I cast the Attr attribute to my custom InventoryPartAttribute attribute and extract its value into the enmInventoryPart variable: InvPartAttr = (InventoryPartAttribute)Attr; enmInventoryPart = InvPartAttr.InventoryPartSupported; If the value of the extracted enumerator matches the client's enumerator, the class supports the right type of inventory part, so I instantiate it and break out of the loop: if((int) enmInventoryPart == (int)vInvPart) { Obj = Activator.CreateInstance(IntrfaceImpl[i]) InvPart = (IPartsInventory)Obj; break; }Finally, the factory returns the IPartsInventory object: return InvPart; Let's look at what happens when the need arises to add another object that derives from the IPartsInventory interface. I'll name this object MousePadInventory. After updating the enmInvParts enumerator to accommodate the new inventory part, I define the new class and attach the InventoryPartAttribute attribute to it: public enum enmInvParts : int {Monitors = 1, Keyboards, MousePads}; [InventoryPartAttribute(enmInvParts.MousePads)] class MousePadInventory : IPartsInventory { public void Restock() { Console.WriteLine("The mouse pad inventory has been restocked"); } }Next, the ImplAttr attribute of the IPartsInventory interface needs to reflect that a new class is implementing the interface: [ImplAttr(new Type[]{typeof(MonitorInventory),typeof(KeyboardInventory), typeof(MousePadInventory)})] interface IPartsInventory() { public void Restock(); } Then, I update MainClass to handle the request to replenish the newly added mouse pad inventory. The only change is to add another case for "MousePads" to the MainClass class shown in Figure 4. Add the following code right after the "Keyboards" case: case "MousePads": InvMgr.ReplenishInventory(enmInvParts.Keyboards); break; Now I'm finished. There is no need to change PartsFactory or InventoryMgr. This shows that I have successfully removed all remaining coupling issues caused by the switch statement inside PartsFactory and that adding a new class requires minimal effort. Conclusion The Concrete Factory pattern is one of the simplest yet most powerful design patterns. Because of its simplicity, the subtle coupling and cohesion implications that accompany the implementation you choose when applying the pattern are sometimes overlooked. Indeed, it is not always possible or necessary to severely reduce coupling or completely maximize cohesion; however, you need to be mindful of their presence and their implications in whatever context you happen to apply this design pattern. |