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 Renault or Volkswagen (I am affiliated with neither, this is just for this example). It also produces two kinds of vehicles, either a car or a bike.

The CarInterface looks like this:

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

All this interface provides is a way to describe the vehicle.

The BikeInterface is similar:

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

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

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

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

Next we need to define a concrete car, VolkswagenCar:

class VolkswagenCar(CarInterface):
    def __init__(self, color: str):
        self._make = "Volkswagen"
        self._color = color

    def description(self) -> str:
        return f"Make {self._make} with color: {self._color}"

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

The VolkswagenBike again is similar. The difference is that with a bike, you can specify the number of wheels:

class VolkswagenBike(BikeInterface):
    def __init__(self, wheels: int):
        self._make = "Volkswagen"
        self._numberOfWheels = wheels

    def description(self) -> str:
        return f"Make {self._make} with number of wheels: {self._numberOfWheels}"

As you expected the RenaultCar and the RenaultBike are similarly fashioned:

class RenaultCar(CarInterface):
    def __init__(self, color: str):
        self._make = "Renault"
        self._color = color

    def description(self) -> str:
        return f"Make {self._make} with color: {self._color}"


class RenaultBike(BikeInterface):
    def __init__(self, wheels: int):
        self._make = "Renault"
        self._numberOfWheels = wheels

    def description(self) -> str:
        return f"Make {self._make} with number of wheels: {self._numberOfWheels}"

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

class VolkswagenFactory(VehicleFactory):

    def create_car(self, color: str) -> CarInterface:
        return VolkswagenCar(color)

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

Some explanation is needed here:

  1. The create_car() method returns a CarInterface. Since VolkswagenCar implements that interface, the return statement is valid.
  2. The same goes for the create_bike() method.
  3. The VolkswagenFactory derives from VehicleFactory hence we get the above two methods.

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

class RenaultFactory(VehicleFactory):

    def create_car(self, color: str) -> CarInterface:
        return RenaultCar(color)

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

Time to test

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

if __name__ == "__main__":
    factory = RenaultFactory()
    car = factory.create_car("blue")
    bike = factory.create_bike(2)

    print(car.description())
    print(bike.description())

A line by line description:

  1. We create RenaultFactory. Because this derives and implements the VehicleFactory methods, we can call the two create methods.
  2. Note that whether we return a VolkswagenCar or a RenaultCar does not matter: the create methods return an interface.
  3. Next we print out the 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, also at runtime, and maintainability.

Leave a Reply

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