Design patterns in Python: Abstract Factory

The Abstract Factory Pattern is a way to group the creation of related objects, like products of a certain brand or make by building a common factory interface. If this all sounds abstract, a picture can help:

A short breakdown:

  • All the factories have a common interface called AbstractFactory
  • The concrete factories, in our diagram there are two, implement this interface
  • These concrete factories produce objects with a common interface so those are interchangeable. In our example those are called AbstractProductOne and AbstractProductTwo

This is a somewhat more complicated design pattern, but when used well can lead to interchangeable concrete implementations. However, considering the relative complexity of this pattern, it will require extra initial work. Also, due to the high abstraction levels, it can lead in some cases to code that is more difficult to test, debug and maintain.

So, even though this pattern is very flexible and powerful, use it wisely and sparingly.

Implementation in Python

For this example we will implement a VehicleFactory which produces two brands of vehicles, namely either Brand A or Brand B. It also produces two kinds of vehicles, either a car or a bike.

But first

from abc import ABC, abstractmethod

We will start by importing the following:

Why do we need these?

  1. ABC stands for ‘Abstract Base Class’. This defines a special type of class which cannot be instantiated directly. Instead it serves as a blueprint or contract which other classes must follow. Think of it like a template enforcing certain rules. If a class inherites from an ABC it must implement all of its abstract methods.
  2. The @abstractmethod decorator marks methods as abstract within an ABC. Applying this decorator to a method declares the method has no implementation in the base class and must be overridden by a concrete subclass.

The interfaces

The CarInterface looks like this:

class CarInterface(ABC):
    @abstractmethod
    def description(self) -> str:
        pass

Note that the CarInterface derives from ABC so it is an abstract base class, with one abstract method description()

All this interface provides is a way to retrieve the vehicle description.

The BikeInterface is similar:

class BikeInterface(ABC):
    @abstractmethod
    def description(self) -> str:
        pass

Next we define the VehicleFactory interface. A factory in our example can only produce either a car or a bike:

class VehicleFactory(ABC):
    @abstractmethod
    def create_car(self,color:str) -> CarInterface:
        pass

    @abstractmethod
    def create_bike(self,wheels:int) -> BikeInterface:
        pass

Implementing the concrete classes

Next we need to define a concrete car BrandACar:

class BrandACar(CarInterface):
    def __init__(self,color:str)->None:
        self._color=color
        self._make="Brand A"

    def description(self) -> str:
        return f"This is a {self._color} {self._make} Car."

A car just get its brand, which is always set to “Brand A” in this case, and a color, which can vary.

The BrandABike is similar. The difference is that with a bike, you can specify the number of wheels instead of the color:

class BrandABike(BikeInterface):
    def __init__(self,wheels:int)->None:
        self._numberOfWheels=wheels
        self._make="Brand A"

    def description(self) -> str:
        return f"This is a {self._make} Bike with {self._numberOfWheels} wheels."

As you expected the BrandBCar and the BrandBBike are similarly fashioned:

class BrandBCar(CarInterface):
    def __init__(self,color:str)->None:
        self._color=color
        self._make="Brand B"

    def description(self) -> str:
        return f"This is a {self._color} {self._make} Car."
    
class BrandBBike(BikeInterface):
    def __init__(self,wheels:int)->None:
        self._numberOfWheels=wheels
        self._make="Brand B"

    def description(self) -> str:
        return f"This is a {self._make} Bike with {self._numberOfWheels} wheels."

Now we come to the heart of this pattern, the factory, in our case a BrandAVehicleFactory:

class BrandAVehicleFactory(VehicleFactory):
    def create_car(self,color:str) -> CarInterface:
        return BrandACar(color)

    def create_bike(self,wheels:int) -> BikeInterface:
        return BrandABike(wheels)

Some explanation is needed here:

  1. The create_car() method returns a CarInterface. Since BrandACar implements that interface, the return statement is valid.
  2. The same goes for the create_bike() method.
  3. The BrandAFactory derives from VehicleFactory which provides the above two methods.

As you might expect, the BrandBVehicleFactory is built along similar lines:

class BrandBVehicleFactory(VehicleFactory):
    def create_car(self,color:str) -> CarInterface:
        return BrandBCar(color)

    def create_bike(self,wheels:int) -> BikeInterface:
        return BrandBBike(wheels)

Fetching the factory

Now we need a way to create the correct factory. We do this by setting up a simple registry. Start by setting up a dictionary like this:

VEHICLE_FACTORIES={
    "BrandA": BrandAVehicleFactory(),
    "BrandB": BrandBVehicleFactory(),
}

This dictionary has strings as its keys, and the two vehicle factories as values. However, both factories implement the VehicleFactory interface, so we can do this:

def get_vehicle_factory(brand: str) -> VehicleFactory:
    factory = VEHICLE_FACTORIES.get(brand)
    if not factory:
        raise ValueError(f"Unknown brand: {brand}")
    return factory

This function has one parameter, the brand, and looks in our very simple registry to see whether we have a factory registered for the brand, if not, an exception is raised. The function returns a VehicleFactory which means it can return either of the two concrete vehicle factories.

Time to test

Well, all of this is nice, but does it work:

if __name__ == "__main__":
    brand_a_factory = get_vehicle_factory("BrandA")
    brand_b_factory = get_vehicle_factory("BrandB")

    car_a = brand_a_factory.create_car("Red")
    bike_a = brand_a_factory.create_bike(2)

    car_b = brand_b_factory.create_car("Blue")
    bike_b = brand_b_factory.create_bike(3)

    print(car_a.description())
    print(bike_a.description())
    print(car_b.description())
    print(bike_b.description())

A line by line description:

  1. We start by trying to retrieve a brand A factory and a brand B factory.
  2. Next we create a car and a bike for each factory.
  3. Finally we print out the description.

Conclusion

Conclusion: Harnessing the Power of Abstract Factory

The Abstract Factory Pattern offers a robust solution for grouping the creation of related objects, ensuring that product families—like our Brand A and Brand B vehicles—are always compatible and interchangeable. We’ve seen through the Python implementation, utilizing Abstract Base Classes (ABCs) and interfaces for CarInterface, BikeInterface, and VehicleFactory, that this pattern enforces a clear structure and contract for object creation.


Key Takeaways

While implementing the pattern, even in this simple vehicle example, requires significant initial effort and a high degree of abstraction, the payoff is substantial. The resulting code benefits from increased flexibility and runtime configurability, allowing the client code to switch between entire product families (factories) without being tied to concrete classes.

However, as noted, this complexity is a double-edged sword. Developers must wield this powerful tool judiciously, being mindful of the potential for increased difficulty in testing, debugging, and maintenance. Ultimately, when applied thoughtfully, the Abstract Factory Pattern is an invaluable design tool that promotes cleaner architecture and aids in the development of highly adaptable software systems. Choose wisely when the overhead is justified by the need for interchangeable product families. 💡

The Code Nomad
The Code Nomad
Articles: 167

Leave a Reply

Your email address will not be published. Required fields are marked *