C# Coding Solutions—More Sophisticated Factory Implementations

Microsoft .NET Framework, ASP.NET, Visual C# (CSharp, C Sharp, C-Sharp) Developer Training, Visual Studio


Jump to: navigation, search
CSharp-Online.NET:Articles
C# Articles

C# Coding Solutions

© 2006 Christian Gross

More Sophisticated Factory Implementations

There are multiple ways to define a factory’s implementation. Classically speaking, with some frameworks, there is one factory for each implementation. But that can very quickly become tedious and it might be wrong in terms of decoupling the implementation from the interface. For example, imagine releasing version 1.0 and then releasing version 2.0. Will the Factory method be updated to instantiate the new type?

The correct approach is not to create a specific factory for each implementation, but to consider the types that the factory is instantiating. Consider the factory as a way of instantiating type groupings, as illustrated in the following improved factory:

public sealed class MultiObjectSharedFactory {
    static const int INITIAL_VERSION = 1;
    static const int UPDATED_VERSION = 2;
 
    public static IUseIt AnImplementation(
        int version) {
        switch( version) {
            case MultiObjectSharedFactory.INITIAL_VERSION:
                return new UseItDirectly();
            case MultiObjectSharedFactory.UPDATED_VERSION:
                return new UseItDirectlyFixed();
        }
        return null;
    }
    public static IUseIt AnotherImplementation() {
        return new NewClassIntroduced();
    }
}

The class Factory has been renamed MultiObjectSharedFactory to indicate the factory’s varying capabilities. The class MultiObjectSharedFactory has its declaration changed to sealed, indicating that nobody can subclass the factory. The class contains two methods that instantiate various implementations supporting the interface IUseIt.

The method AnImplementation is different from previous factory methods because it requires a parameter, which is an int value. The parameter represents a version number of the object to instantiate. The reason for using an int has to do with the previous example, where the Factory had methods Version1 and Version2. Imagine if a third version of the product became available. At that point another method would be added to the factory, causing a recompile of each client that uses the factory. This is neither the intent nor the desire. The int value is a solution, but other solutions (buffers, enumerations, interface instances, etc.) could be employed. Another way to solve the versioning problem at an application level is presented in the solution "Versioning Assemblies" in Chapter 2. When creating a factory you want to decouple, but also to avoid requiring the client to recompile its sources. What this example illustrates is that a factory does not need to be a single method with a single instantiation. The factory can contain logic that determines which object to instantiate and the instantiation conditions.

The other method, AnotherImplementation, instantiates another type that implements the IUseIt interface. AnotherImplementation belongs to the factory class because both methods are grouped together. A grouping would not belong together if the types they instantiate were not related. For example, if you are a producer of wheels, a factory that instantiates all sorts of wheel types (plane, car, etc.) would not be grouped together because they change independently of each other. A change in instantiating wheels for planes would result in a recompile for programs that require wheels for cars.

When defining factory groupings, usually you are defining abstractions, and that could involve abstracting the factory to an interface, as in the following example:

public interface IWheel { }
public interface IWing { }
public interface IFactory {
    IWheel CreateWheel();
    IWing CreateWing();
}
public class BoeingPartsFactory : IFactory {
    public IWheel CreateWheel() {
        return null;
    }
    public IWing CreateWing() {
        return null;
    }
}
public class AirBusPartsFactory : IFactory {
    public IWheel CreateWheel() {
        return null;
    }
    public IWing CreateWing() {
        return null;
    }
}
public class AirplanePartsFactory {
    public static IFactory CreateBoeing() {
        return null;
    }
    public static IFactory CreateAirBus() {
        return null;
    }
}

In the example some application manipulates the interfaces IWheel and IWing. Both interfaces represent parts of an airplane. And on this planet there are two major big airplane manufacturers: Boeing and Airbus. Imagine a service station at an airport. It needs to order parts from both manufacturers. However, the parts cannot be substituted, as it is not possible to put an Airbus wing on a Boeing plane. But from an abstraction perspective both Airbus and Boeing have wings, and therefore one abstraction (IWing) can be used to define both. We are caught in a bind where the abstraction is identical, but the technical implementation unique.

The solution is to define an interface that defines a common abstract theme (IFactory), where each unique manufacturer implements its own IFactory class. The interface IFactory has two methods: CreateWheel and CreateWing. The two methods are used as factory methods to instantiate types that have the same intention but unrelated implementations. The classes AirBusPartsFactory and BoeingPartsFactory implement IFactory and their appropriate IWheel and IWing implementations. Then to create the appropriate IFactory interface instance, another factory, AirplanePartsFactory, is defined.

The example is an abstraction of an abstraction, and is used to implement functionality where you want to instantiate types that are similar but not related. In the case of the airport that means the process of costing a wing is identical for both Boeing and Airbus. What is different between Boeing and Airbus are the details related to the costing.

In the airplane example, each factory allowed the creation of a single wheel or a single wing. But, a Boeing airplane might have two wheels whereas an Airbus has three. A dilemma is created because a consumer has an IFactory interface instance that can be used to create the wheels and wings, but not assemble them. The consumer does not have the assembly information, nor can it decide what the assembly information is.

Assembling the parts is not the responsibility of the factory that we have been discussing, but rather the responsibility of a pattern called Builder. You can think of the Builder pattern as a Factory pattern with a mission. A Builder pattern implementation exposes itself as a factory, but goes beyond the factory implementation in that various implementations are wired together. That would mean creating an airplane with wings and wheels. In code, the Builder pattern would be implemented as follows:

public interface IPlane {
    IWheel[] Wheels { get; }
    IWing[] Wings { get; }
    IFactory CreateParts();
}
public interface IPlaneFactory {
    IPlane CreatePlane(  );
}
class Plane123Factory : IPlaneFactory {
    public IPlane CreatePlane() { return null;}
}
public class BoeingFactory {
    IPlaneFactory CreatePlane123Factory();
    IPlaneFactory CreatePlane234Factory();
}

The Builder pattern implementation does not create new factory implementations, but builds on top of the old ones. Therefore, the Builder pattern assumes that the previously defined factory interfaces and implementations (IWheel, IFactory, etc.) still exist. The new interfaces related to the Builder Pattern are IPlane and IPlaneFactory. When instantiated, the IPlane interface represents an assembled plane with wings and wheels. The IPlane interface exposes the wings and wheels as readonly properties for consistency. Had the properties been read-write, then in theory a developer could have added an Airbus wheel to a Boeing airplane. This is technically possible at an algorithm level, but in reality it is not possible to mix and match parts.

The Builder pattern also makes it possible to consistently assemble types such that their states cannot be violated. The Builder pattern implementation is responsible for putting the correct wheels and wings on the airplanes. This does not mean that all interfaces have read-only properties. If there is no problem with mixing and matching interface instances, then there is no problem with having read-write properties.

The interface IPlaneFactory is an implementation of the Builder pattern because it contains the method CreatePlane, which is used to create an instance of IPlane. The implementation of CreatePlane would add the right number of wheels and wings to an IPlane instance.

The method IPlane.CreateParts returns a factory that can be used to create wheels and wings. The method is associated with IPlane because from a logical sense any plane instance that needs parts should have an associated IFactory instance. The Builder pattern implementation manages the association of the IPlane implementation with the appropriate IFactory implementation.

The class Plane123Factory implements the IPlaneFactory interface and is responsible for instantiating and wiring together IPlane, IWing, IWheel, and IFactory interface instances. The class BoeingFactory is a general factory used to instantiate and return a Builder pattern implementation (IPlaneFactory) that can be used to instantiate planes and instantiate other interfaces. There is always a central factory that instantiates other implementations, which can be factories or classes that implement some business logic.

A factory can also instantiate an object based on another object. This is called cloning an object or implementing the Prototype pattern. The big-picture idea of the Prototype pattern is to be able to create a copy of an object that already exists. In .NET the way to clone an object is to implement the ICloneable interface, as in the following example:

class Boeing123Plane : IPlane, System.ICloneable {
    public Object Clone() {
        Boeing123Plane obj =
            (Boeing123Plane)this.MemberwiseClone();
        obj._factory = (IFactory)((System.ICloneable)_factory).Clone();
        return obj;
}

The example method Clone implements both a shallow clone and deep clone. A shallow clone is when only the type’s local data members are copied. The shallow clone is realized using the method call MemberwiseClone. Calling MemberWiseClone does not require an instantiation because the process of cloning will automatically instantiate the type. To implement a deep clone, the clone methods of each data member in a class are called.

One of the debates about cloning is whether to implement a deep clone. For example, if type A references type B, then a deep clone will copy both type instances. But imagine if type A references type B, and type B references another instance of type B, which references the original type-B instance. A cyclic reference scenario has been established, which when cloned using a deep clone could lead to a never-ending cloning scenario. To avoid such tricky situations, adding a didClone flag could be useful. However, even with a didClone flag problems will arise. The better solution is to rethink the cloning scenario and to clone each data member manually and rebuild the structure using manual object assignment. There is no silver-bullet solution when implementing a deep clone.

When implementing a factory, remember the following points:

  • Using the Factory pattern means using component-oriented development, where there is an interface or a base class and some specific class is instantiated. The idea is to decouple the consumer from the implementation.
  • There are multiple levels to creating a factory. In the lowest level a factory creates a specific implementation. In the next level factory methods are grouped into a single type to indicate a relation between the factories. In the higher level the Builder pattern is used to instantiate types that are wired together. Each level represents a type of factory functionality and is used when building applications.
  • When grouping together factory methods, often interfaces are defined to indicate related factory operations. When converting a factory from a single class to an interface, you must manage additional considerations, such as interface relations.
  • The Builder pattern is an abstraction that defines factory-like methods. The factory-like methods use other Factory classes to instantiate and wire together various instances.
  • You can clone objects when you have an object instance but not the factory. Cloning requires more thought than a Factory or Builder pattern implementation to avoid cloning items that should have been referenced.
  • In all of our examples the factory methods had no parameters. More often than not this will be the exception, not the rule.


Previous_Page_.gif Next_Page_.gif


Personal tools