Easy Abstract Factory Pattern Implementation in C#

Iede Snoek
November 13, 2023 0 Comment

Introduction

The Abstract Factory Pattern organizes the creation of related objects, making it easy to interchange them. In our example, we’ll delve into a simplified implementation in C#. If this all sounds abstract, a picture can help:

Key Concepts:

  1. Factories share a common interface called AbstractFactory.
  2. Concrete factories implement this interface and produce interchangeable objects.
  3. The pattern offers flexibility but may require extra initial work.
  4. Maintainability and testability might be challenging due to high abstraction.

Note: I wrote all the code in this article in one file. If you feel like it, you can distribute the classes and interfaces to the appropiate files and folders. I did this to keep things simple.

Implementation in C#

In our example we will define a Vehiclefactory interface, to encapsulate two factories, one who produces Brand A vehicles, and one who does the same for Brand B. The vehicles can be either cars or bikes.

The cars have an AbstractCar interface, and likewise, the bikes have an AbstractBike interface.

We will start by defining the IVehicleFactory interface itself:

    interface IVehicleFactory
    {
        AbstractCar createCar(string color);
        AbstractBike createBike(int numberOfWheels);
    }

In it we see two different methods used to make the two different products i.e. cars and bikes

Next we define the interfaces for both the car and the bike. Both interfaces have a description property to render a string representation of themselves:

    interface AbstractCar
    {
        string description { get; }
    }

    interface AbstractBike
    {
        string description { get; }
    }

Next we come to the vehicles themselves, first the Brand A vehicles:

class BrandACar:AbstractCar
{
    string make;
    string color;

    public BrandACar(string color)
    {
        this.make = "Brand A";
        this.color = color;
    }

    public string description
    {
        get
        {
            return $"{color} {make} Car";
        }
    }
}

class BrandABike:AbstractBike
{
    int numberOfWheels;
    string make;
    public BrandABike(int numberOfWheels)
    {
        this.numberOfWheels = numberOfWheels;
        this.make = "Brand A";
    }

    public string description
    {
        get
        {
            return $"{numberOfWheels} wheel {make} Bike";
        }
    }
}

These classes implement a constructor where the brand is set, as well as either the color or the number of wheels, and an implementation of the description property.

The implementation for the Brand B vehicles looks similar:

class BrandBCar : AbstractCar
{
    string make;
    string color;

    public BrandBCar(string color)
    {
        this.make = "Brand B";
        this.color = color;
    }

    public string description
    {
        get
        {
            return $"{color} {make} Car";
        }
    }
}

class BrandBBike : AbstractBike
{
    int numberOfWheels;
    string make;
    public BrandBBike(int numberOfWheels)
    {
        this.numberOfWheels = numberOfWheels;
        this.make = "Brand B";
    }

    public string description
    {
        get
        {
            return $"{numberOfWheels} wheel {make} Bike";
        }
    }
}

The concrete factories

Let’s build our first concrete factory, for Brand A:

class BrandAFactory:IVehicleFactory
{
    public AbstractCar createCar(string color)
    {
        return new BrandACar(color);
    }

    public AbstractBike createBike(int numberOfWheels)
    {
        return new BrandABike(numberOfWheels);
    }
}

A few noteworthy points:

  • The BrandAFactory implements the IVehicleFactory interface. If we are doing some form of Dependency Injection this is very handy as we can swap out factories.
  • Both methods also return an interface instead of a concrete class.

The Brand B factory as you might expect is very similar:

class BrandBFactory : IVehicleFactory
{
    public AbstractCar createCar(string color)
    {
        return new BrandBCar(color);
    }

    public AbstractBike createBike(int numberOfWheels)
    {
        return new BrandBBike(numberOfWheels);
    }
}

Testing

Let’s test this setup:

internal class Program
{
    static void Main(string[] args)
    {
        IVehicleFactory factory = new BrandAFactory();
        AbstractCar car = factory.createCar("Red");
        AbstractBike bike = factory.createBike(2);
        System.Console.WriteLine(car.description);
        System.Console.WriteLine(bike.description);
    }
}

In this code we have a variable of type IVehicleFactory set to a BrandAFactory instance. Next we create a car and a bike, and print out their descriptions.

Conclusion

As you can see, setting up the factory is quite some work even in a simple case like this one. However you gain a lot of flexibility and ease of use.

Mind you, as I said in the introduction, because of the higher level of abstraction, using this pattern can lead to extra initial work, and in some case, code that is harder to debug and maintain.

When done well, this pattern offers flexibility, even at runtime, and maintainability.