Abstract Factory Design Pattern (C#)

An abstract factory is a design pattern that provides an interface a caller may use to create concrete instances of an abstract type, without having to concern itself with the details of how each concrete type is constructed. In C#, a concrete type is a class, unlike an abstract type, which can be instantiated. An abstract type, in the context of an abstract factory, is an abstract class that must be inherited and implemented by derived types, and cannot be instantiated.

Sections

Design Architecture

59df7468

[IFactory|Create() : T]
[IFactory]^-.-[BFactory|Create() : T]
[IFactory]^-.-[AFactory|Create() : T]
[T|F(x) : void]
[T]^-[B]
[T]^-[A]
[Caller]–>[AFactory]
[Caller]–>[BFactory]
[BFactory]–>[Create]
[Create]–>[A]
namespace danderson.io.DesignPatterns.AbstractFactory
{
    abstract class T
    {
        public abstract void F(int x);
    }

    class A : T
    {
        public override void F(int x)
        {
        }
    }

    class B : T
    {
        public override void F(int x)
        {
        }
    }

    interface IFactory
    {
        T Create();
    }

    class AFactory : IFactory
    {
        public T Create()
        {
            return new A();
        }
    }

    class BFactory : IFactory
    {
        public T Create()
        {
            return new B();
        }
    }
}

Design Usage

Using the design pattern extends upon the already existing benefits of abstract classes, which allow you to work with a family of objects in their abstract form, disregarding their specific concrete types. The pattern extends those benefits by allowing you to abstract away how those types are instantiated. While instantiating simple types can be done with minimal code, some types may require complex instantiation that involves one or more dependencies, or a significant number of lines of code, or both. Below is a demonstration of two methods; the left uses only abstract classes, while the right uses abstract classes in addition to the abstract factory pattern.

public static void PurchaseVehicle()
{
    FordVehicleModels model = ReadModelOrder();

    Vehicle vehicle = null;

    switch (model)
    {
        case FordVehicleModels.Ranger:
            vehicle = new FordRanger
                        {
                            Make  = "Ford",
                            Model = "Ranger",
                            Year  = DateTime.Today.Year,
                            Color = Color.Black,
                            Bed   = new TruckBed()
                        };
            break;
        case FordVehicleModels.Mustang:
            vehicle = new FordMustang
                        {
                            Make  = "Ford",
                            Model = "Mustang",
                            Year  = DateTime.Today.Year,
                            Color = Color.Red,
                            Trunk = new Trunk()
                        };
            break;
    }

    PrintVehicleDetails(vehicle);
}
public static void PurchaseVehicle()
{
    FordVehicleModels model = ReadModelOrder();

    IVehicleFactory factory = null;

    switch (model)
    {
        case FordVehicleModels.Ranger:
            factory = new FordRangerFactory();
            break;
        case FordVehicleModels.Mustang:
            factory = new FordMustangFactory();
            break;
    }

    Vehicle vehicle = factory.BuildVehicle();
                    
    PrintVehicleDetails(vehicle);
}

The sample models a real life situation where you might build and purchase a vehicle online. In this day and age, just about every vehicle manufacturer allows you to go online and customize a vehicle for purchase. You can add power windows, power locks, heating, air conditioning, leather seats, and maybe even a sun roof. At the end of your order, you expect a summary of the details you chose. Both samples produce the same result, but the sample on the left has more code, and many more details. In real life, you are going to be concerned that your vehicle has all the options you selected, which is indicated by the printed details page. What you don’t care about, is the exact process in which the vehicle was constructed, such as welding the truck bed to the vehicle frame. This is where the sample on the left falls short, and why the sample on the right prevails.

Imagine how a vehicle is actually built; a large manufacturing facility with assembly lines, thousands of parts, plenty of workers, and a complex but automated manufacturing process. Correlate that to the sample on the left, and imagine how much code it would take to describe that process. Now you should be able to see why the sample falls short, because it cannot scale when constructing objects becomes much more than a constructor call. All this method wanted to do was to make the order to have the vehicle built and print the order details, but it didn’t want to also actually build the vehicle too.

Each factory in the pattern, as demonstrated in the sample on the right, is responsible for constructing exactly one concrete type. All the details of how those types are a concern of the factory, allowing the calling code to do what it needs to, and cleanly. Factories might even have their own state and behavior, but if it takes 5,000 lines of code to describe the construction process of an object, it can go there, not where we need to just display some information.

The pattern strongly facilitates SOC, or separation of concerns.

Design Implementation

Based on the samples in the design usage, here is a basic implementation of the abstract factory pattern.

namespace danderson.io.DesignPatterns.AbstractFactory
{
    using System;
    using System.Drawing;

    public abstract class Vehicle
    {
        protected Vehicle()
        {
            BuiltOn = DateTime.Now;
        }

        public DateTime BuiltOn { get; set; }
        public Color Color { get; set; }
        public string Make { get; set; }
        public string Model { get; set; }
        public int Year { get; set; }

        public void Drive()
        {
        }
    }

    public class TruckBed
    {
    }

    public class FordRanger : Vehicle
    {
        public TruckBed Bed { get; set; }
    }

    public class Trunk
    {
    }

    public class FordMustang : Vehicle
    {
        public Trunk Trunk { get; set; }
    }

    public interface IVehicleFactory
    {
        Vehicle BuildVehicle();
    }

    public class FordRangerFactory : IVehicleFactory
    {
        public Vehicle BuildVehicle()
        {
            return new FordRanger
                   {
                       Make  = "Ford",
                       Model = "Ranger",
                       Year  = DateTime.Today.Year,
                       Color = Color.Black,
                       Bed   = new TruckBed()
                   };
        }
    }

    public class FordMustangFactory : IVehicleFactory
    {
        public Vehicle BuildVehicle()
        {
            return new FordMustang
                   {
                       Make  = "Ford",
                       Model = "Mustang",
                       Year  = DateTime.Today.Year,
                       Color = Color.Red,
                       Trunk = new Trunk()
                   };
        }
    }

    public enum FordVehicleModels
    {
        Unknown,
        Ranger,
        F150,
        Mustang
    }

    public class Program
    {
        public static void Main()
        {
            FordVehicleModels model = ReadModelOrder();
                    
            IVehicleFactory factory = GetVehicleFactory(model);
                    
            Vehicle vehicle = factory.BuildVehicle();
                    
            PrintVehicleDetails(vehicle);
        }

        private static IVehicleFactory GetVehicleFactory(FordVehicleModels model)
        {
            IVehicleFactory factory = null;

            switch (model)
            {
                case FordVehicleModels.Ranger:
                    factory = new FordRangerFactory();
                    break;
                case FordVehicleModels.Mustang:
                    factory = new FordMustangFactory();
                    break;
            }

            return factory;
        }

        static FordVehicleModels ReadModelOrder()
        {
            string input = Console.ReadLine();

            FordVehicleModels model;

            bool parsed = Enum.TryParse(input, true, out model);

            return parsed ? model : FordVehicleModels.Unknown;
        }

        static void PrintVehicleDetails(Vehicle vehicle)
        {
            Console.WriteLine("The factory built a new vehicle.");
            Console.WriteLine($"    Make: {vehicle.Make}");
            Console.WriteLine($"    Model: {vehicle.Model}");
            Console.WriteLine($"    Year: {vehicle.Year}");
            Console.WriteLine($"    Color: {vehicle.Color}");
            Console.WriteLine($"    Built on: {vehicle.BuiltOn}");
        }
    }
}

Design Guidelines

DO: Use the abstract factory pattern when working with one or more classes that inherit from the same base class, where their instantiation becomes too involved for simple constructor instantiation.
DO: Use an interface for the factories instead of an abstract class, unless the factories have functionality that can be shared.
DO: Use abstract types instead of concrete types, disregarding specific implementation details.
AVOID: Using the abstract factory pattern when abstract classes by themselves are sufficient.
CONSIDER: A mechanism must still be present to help indicate which factory will be used to instantiate your type.
CONSIDER: Encapsulate the creation of factories into a containing factory when there are a significant number of factories and concrete types.