Archives 05/15/2023

Project – The mid-range vehicle factory – Strategy, Abstract Factory, and Singleton Design Patterns

To prove the flexibility of our design based on the Abstract Factory pattern, let’s add a new concrete factory named MidRangeVehicleFactory. That factory should return a MidRangeCar or a MidRangeBike instance. Once again, the car and bike are just empty classes (of course, in your programs, they will do something):

public class MiddleGradeCar : ICar { }
public class MiddleGradeBike : IBike { }

The new MidRangeVehicleFactory looks pretty much the same as the other two:

public class MidRangeVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new MiddleGradeBike();
    public ICar CreateCar() => new MiddleGradeCar();
}

Now, to test the mid-range factory, we declare the following test class:

namespace Vehicles.MidRange;
public class MidRangeVehicleFactoryTest : BaseAbstractFactoryTest<MidRangeVehicleFactory, MidRangeCar, MidRangeBike>
{
}

Like the low-end and high-end factories, the mid-range test class depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).If we run the tests, we now have the following six passing tests:

 Figure 7.5: Visual Studio Test Explorer showcasing the six passing tests.Figure 7.5: Visual Studio Test Explorer showcasing the six passing tests. 

So, without updating the consumer (the AbstractFactoryTest class), we added a new family of vehicles, the middle-end cars and bikes; kudos to the Abstract Factory pattern for that wonderfulness!

Impacts of the Abstract Factory

Before concluding, what would happen if we packed everything in a large interface instead of using an Abstract Factory (breaking the ISP along the way)? We could have created something like the following interface:

public interface ILargeVehicleFactory
{
    HighEndBike CreateHighEndBike();
    HighEndCar CreateHighEndCar();
    LowEndBike CreateLowEndBike();
    LowEndCar CreateLowEndCar();
}

As we can see, the preceding interface contains four specific methods and seems docile. However, the consumers of that code would be tightly coupled with those specific methods. For example, to change a consumer’s behavior, we’d need to update its code, like changing the call from CreateHighEndBike to CreateLowEndBike, which breaks the OCP. On the other hand, with the factory method, we can set a different factory for the consumers to spit out different results, which moves the flexibility out of the object itself and becomes a matter of composing the object graph instead (more on that in the next chapter).Moreover, when we want to add mid-range vehicles, we must update the ILargeVehicleFactory interface, which becomes a breaking change (the implementation(s) of the ILargeVehicleFactory must be updated). Here’s an example of the two new methods:

public interface ILargeVehicleFactory
{
    HighEndBike CreateHighEndBike();
    HighEndCar CreateHighEndCar();
    LowEndBike CreateLowEndBike();
    LowEndCar CreateLowEndCar();
    MidRangeBike CreateMidRangeBike();
    MidRangeCar CreateMidRangeCar();
}

From there, once the implementation(s) are updated, if we want to consume the new mid-range vehicles, we need to open each consumer class and apply the changes there, which once again breaks the OCP.

The most crucial part is understanding and seeing the coupling and its impacts. Sometimes, it’s okay to tightly couple one or more classes together as we don’t always need the added flexibility the SOLID principles and some design patterns can bring.

Now let’s conclude before exploring the last design pattern of the chapter.

Conclusion

The Abstract Factory pattern is excellent for abstracting away the creation of object families, isolating each family and its concrete implementation, leaving the consumers unaware of the family created at runtime by the factory.We talk more about factories in the next chapter; meanwhile, let’s see how the Abstract Factory pattern can help us follow the SOLID principles:

  • S: Each concrete factory is solely responsible for creating a family of objects. You could combine Abstract Factory with other creational patterns, such as the Prototype and Builder patterns for more complex creational needs.
  • O: We can create new families of objects, like the mid-range vehicles, without breaking existing client code.
  • L: We aim at composition, so there’s no need for any inheritance, implicitly discarding the need for the LSP. If you use abstract classes in your design, you must ensure you don’t break the LSP when creating new abstract factories.
  • I: Extracting a small abstraction with many implementations where each concrete factory focuses on one family makes that interface very focused on one task instead of having a large interface that exposes all types of products (like the ILargeVehicleFactory interface).
  • D: By depending only on interfaces, the consumer is unaware of the concrete types it uses.

Next, we explore the last design pattern of the chapter.