Archives 04/13/2023

Project – Abstract Factory – Strategy, Abstract Factory, and Singleton Design Patterns

Context: We need to support the creation of multiple models of vehicles. We also need to be able to add new models as they become available without impacting the system. To begin with, we only support high-end and low-end models, but we know this will change sooner rather than later. The program must only support the creation of cars and bikes.For the sake of our demo, the vehicles are just empty classes and interfaces because learning how to model vehicles is not necessary to understand the pattern; that would be noise. The following code represents those entities:

public interface ICar { }
public interface IBike { }
public class LowEndCar : ICar { }
public class LowEndBike : IBike { }
public class HighEndCar : ICar { }
public class HighEndBike : IBike { }

Next, we look at the part that we want to study—the factories:

public interface IVehicleFactory
{
    ICar CreateCar();
    IBike CreateBike();
}
public class LowEndVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new LowEndBike();
    public ICar CreateCar() => new LowEndCar();
}
public class HighEndVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new HighEndBike();
    public ICar CreateCar() => new HighEndCar();
}

The factories are simple implementations that describe the pattern well:

  • LowEndVehicleFactory creates low-end models.
  • HighEndVehicleFactory creates high-end models.

The consumer of this code is an xUnit test project. Unit tests are often your first consumers, especially if you are doing test-driven development (TDD).To make the tests easier, I created the following base test class:

using Xunit;
namespace Vehicles;
public abstract class BaseAbstractFactoryTest<TConcreteFactory, TExpectedCar, TExpectedBike>
    where TConcreteFactory : IVehicleFactory, new()
{
    // Test methods here
}

The key to that class is the following generic parameters:

  • The TConcreteFactory parameter represents the type of concrete factory we want to test. Its generic constraint specifies that it must implement the IVehicleFactory interface and have a parameterless constructor.
  • The TExpectedCar parameter represents the type of ICar we expect from the CreateCar method.
  • The TExpectedBike parameter represents the type of IBike we expect from the CreateBike method.

The first test method contained by that class is the following:

[Fact]
public void Should_create_a_ICar_of_type_TExpectedCar()
{
    // Arrange
    IVehicleFactory vehicleFactory = new TConcreteFactory();
    var expectedCarType = typeof(TExpectedCar);
    // Act
    ICar result = vehicleFactory.CreateCar();
    // Assert
    Assert.IsType(expectedCarType, result);
}

The preceding test method creates a vehicle factory using the TConcreteFactory generic parameter, then creates a car using that factory. Finally, it asserts ICar instance is of the expected type.The second test method contains by that class is the following:

[Fact]
public void Should_create_a_IBike_of_type_TExpectedBike()
{
    // Arrange
    IVehicleFactory vehicleFactory = new TConcreteFactory();
    var expectedBikeType = typeof(TExpectedBike);
    // Act
    IBike result = vehicleFactory.CreateBike();
    // Assert
    Assert.IsType(expectedBikeType, result);
}

The preceding test method is very similar and creates a vehicle factory using the TConcreteFactory generic parameter but then creates a bike instead of a car using that factory. Finally, it asserts IBike instance is of the expected type.

I used the ICar and IBike interfaces to type the variables instead of var, to clarify the result variable type. In another context, I would have used var instead. The same applies to the IVehicleFactory interface.

Now, to test the low-end factory, we declare the following test class:

namespace Vehicles.LowEnd;
public class LowEndVehicleFactoryTest : BaseAbstractFactoryTest<LowEndVehicleFactory, LowEndCar, LowEndBike>
{
}

That class solely depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).Next, to test the high-end factory, we declare the following test class:

namespace Vehicles.HighEnd;
public class HighEndVehicleFactoryTest : BaseAbstractFactoryTest<HighEndVehicleFactory, HighEndCar, HighEndBike>
{
}

Like the low-end factory, that class depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).

In a more complex scenario where we can’t use the new() generic constraint, we can leverage an IoC container to create the instance of TConcreteFactory and optionally mock its dependencies.

With that test code, we created the following two sets of two tests:

  • A LowEndVehicleFactory class that should create a LowEndCar instance.
  • A LowEndVehicleFactory class that should create a LowEndBike instance.
  • A HighEndVehicleFactory class that should create a HighEndCar instance.
  • A HighEndVehicleFactory class that should create a HighEndBike instance.

We now have four tests: two for bikes and two for cars.If we review the tests’ execution, both test methods are unaware of types. They use the Abstract Factory (IVehicleFactory) and test the result against the expected type without knowing what they are testing but the abstraction. That shows how loosely coupled the consumers (tests) and the factories are.

We would use the ICar or the IBike instances in a real-world program to do something relevant based on the specifications. That could be a racing game or a rich person’s garage management system; who knows!

The important part of this project is the abstraction of the object creation process. The test code (consumer) is not aware of the implementations.Next, we extend our implementation.