Unlocking the Magic: Mastering the Factory Method in Python

Introduction

In this article, we’ll explore a concept called the Factory Method in Python. The Factory Method is a way to make object creation simpler and more flexible, allowing subclasses to determine which objects to create. To illustrate this, we’ll look at an example involving vehicles from two brands, BrandA and BrandB.

It looks like this:

The ConcreteCreator uses an Abstract Factory to build the object. The AbstractCreator just creates an object which implements the Product interface.

It will all become clearer in the code.

Implementation in Python

Because we are going to use enums, we will need this import first:

from enum import Enum

Next we will define an AbstractCar and an AbstractBike class, both of which only have one method: get_description() which returns a description of the vehicle.

class AbstractCar:
    def get_description(self) -> str:
        pass


class AbstractBike:
    def get_description(self) -> str:
        pass

We’ll define vehicle-specific classes for BrandA and BrandB, where cars have colors and bikes have a number of wheels. Both BrandA and BrandB cars and bikes will implement these methods.

Let’s look at the code for the BrandA-vehicles:

class BrandACar(AbstractCar):
    _make: str = None
    _color: str = None

    def __init__(self, make: str, color: str):
        self._make = make
        self._color = color

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


class BrandABike(AbstractBike):
    _make: str = None
    _number_of_wheels: int = None

    def __init__(self, make: str, number_of_wheels: int):
        self._make = make
        self._number_of_wheels = number_of_wheels

    def get_description(self) -> str:
        return f"Make {self._make} with {self._number_of_wheels} wheels"

Not surprisingly, the BrandB-vehicle implementation is very similar:

class BrandBCar(AbstractCar):
    _make: str = None
    _color: str = None

    def __init__(self, make: str, color: str):
        self._make = make
        self._color = color

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


class BrandBBike(AbstractBike):
    _make: str = None
    _number_of_wheels: int = None

    def __init__(self, make: str, number_of_wheels: int):
        self._make = make
        self._number_of_wheels = number_of_wheels

    def get_description(self) -> str:
        return f"Make {self._make} with {self._number_of_wheels} wheels"

We’ll use an enum to represent the vehicle brands, and this will become clear in the code.

class VehicleBrand(Enum):
    BrandA = 1
    BrandB = 2

We will see later why this is a good practice.

Next, we’ll create an abstract VehicleFactory class with methods to create cars and bikes. This abstraction allows us to define how vehicles are created without specifying their concrete types.

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

    def create_bike(self, number_of_wheels: int) -> AbstractBike:
        pass

The method names speak for themselves.

Then, we’ll implement two factories, BrandAFactory and BrandBFactory, for creating vehicles of BrandA and BrandB. These factories return objects that implement the car and bike interfaces.

class BrandAFactory(VehicleFactory):
    def create_car(self, color: str) -> AbstractCar:
        return BrandACar("Brand A", color)

    def create_bike(self, number_of_wheels: int) -> AbstractBike:
        return BrandABike("Brand A", number_of_wheels)


class BrandBFactory(VehicleFactory):
    def create_car(self, color: str) -> AbstractCar:
        return BrandBCar("Brand B", color)

    def create_bike(self, number_of_wheels: int) -> AbstractBike:
        return BrandBBike("Brand B", number_of_wheels)

One thing to note is that because both for example the BrandACar and the BrandBCar all implement AbstractCar we can return that from our methods.

The heart of our pattern is the VehicleCreator class, which extends VehicleCreatorBase. In its methods, we’ll match the brand requested and create the appropriate vehicle using the corresponding factory. This is the essence of the Factory Method pattern.

class VehicleCreatorBase:
    def create_car(self, brand: VehicleBrand, color: str) -> AbstractCar | None:
        pass

    def create_bike(self, brand: VehicleBrand, number_of_wheels: int) -> AbstractBike | None:
        pass

Some notes:

  1. We pass the VehicleBrand enum to both methods, to show our intention of building a vehicle of the desired brand
  2. We can either return either AbstractCar or AbstractBike or None if we can not create the desired vehicle.

Now we finally build our VehicleFactory:

class VehicleCreator(VehicleCreatorBase):
    _brand_a_factory: BrandAFactory = None
    _brand_b_factory: BrandBFactory = None

    def __init__(self):
        self._brand_a_factory = BrandAFactory()
        self._brand_b_factory = BrandBFactory()

    def create_car(self, brand: VehicleBrand, color: str) -> AbstractCar | None:
        match brand:
            case VehicleBrand.BrandA:
                return self._brand_a_factory.create_car(color)
            case VehicleBrand.BrandB:
                return self._brand_b_factory.create_car(color)
            case _:
                return None

    def create_bike(self, brand: VehicleBrand, number_of_wheels: int) -> AbstractBike | None:
        match brand:
            case VehicleBrand.BrandA:
                return self._brand_a_factory.create_bike(number_of_wheels)
            case VehicleBrand.BrandB:
                return self._brand_b_factory.create_bike(number_of_wheels)
            case _:
                return None

To be clear: this is the factory method, which names this pattern. There is quite a lot going on here:

  1. In the constructor, we create our two factories.
  2. In the methods, we match our enum, and produce either a car or a bike, or we return None if we can not match the value of brand.

Time to test

To test our setup, we’ll create a VehicleCreator, request it to build a car of BrandA, and print its description. We’ll do the same for a bike of BrandB.

if __name__ == "__main__":
    creator: VehicleCreatorBase = VehicleCreator()
    car = creator.create_car(VehicleBrand.BrandA, "red")
    print(car.get_description())

    bike = creator.create_bike(VehicleBrand.BrandB, 3)
    print(bike.get_description())

Line by line:

  1. We construct a VehicleCreator.
  2. We request that it builds a car of BrandA and print the description
  3. We do the same for a bike of BrandB.

Conclusion

In conclusion, implementing the Factory Method pattern in Python can simplify object creation, improve code maintainability, and make it more testable. It’s a powerful tool for abstracting object construction and promoting flexibility and extensibility by working with interfaces rather than concrete types.

Leave a Reply

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