DEV Community

Cover image for Factory Design Pattern in Python
Khushboo Parasrampuria
Khushboo Parasrampuria

Posted on

Factory Design Pattern in Python

Factory pattern is a creational design pattern. As the name suggests, a "factory" is a place where things are created.

The factory pattern defines an interface for creating an object, but it lets the subclasses define which object to build; it does this through a factory method.

Scenario: We want to support several models of cars and their functionalities, like start and stop. But we don’t know which one we will need until runtime.

A brute force approach to implementing this would be: at runtime, when we get the name of the car model, we can write if-else statements or even switch case statements to decide which class object to create and call.

from cars.ford import Ford
from cars.ferrari import Ferrari
from cars.generic import car

def get_car(car_name):
  if car_name == 'Ford':
    return Ford()
  elif car_name == 'Ferrari':
    return Ferrari()
  else:
    return GenericCar()

for car_name in ['Jeep', 'Ferrari', 'Tesla']:
  car = get_car(car_name)
  car.start()
  car.stop()
Enter fullscreen mode Exit fullscreen mode

This code snippet works perfectly, but if we want to support more car models, we will have to modify the above implementation and add more class import statements, which breaks the open/ closed principle.

Also, we are directly instantiating the car classes, which is breaking the dependency-inversion principle as we are depending on the implementation of these classes.

Now let's take a look at how we can refactor our code to avoid breaking the SOLID principles and still be able to extend our application easily.

Class diagram showing factory design patternClass diagram – factory pattern

In the class diagram above, we see an AbsCars class, which is an abstract class that says we must implement start and stop methods in all its concrete classes, and in the bottom, we have three concrete classes; these are the car classes that we want to support: Jeep, Ford, and Ferrari, which implement the AbsCars class.

In the end, we have the CarFactory class, which creates the instance of the desired car class.

The AbsCars class will look like this:

import abc

class AbsCars(abc.ABC):
  @abc.abstractmethod
  def start(self):
    pass

  @abc.abstractmethod
  def stop(self):
    pass
Enter fullscreen mode Exit fullscreen mode

The start and stop methods are declared as abstract methods, which basically means all the concrete classes implementing the AbsCars class need to implement them as well.

For example, the Ford class:

from cars.abscars import AbsCars

class Ford(AbsCars):
  def start(self):
    if is_fuel_present:
      print("ford engine is now running")
      return
    print("Low on fuel")

  def stop(self):
    print("Ford engine shutting down")
Enter fullscreen mode Exit fullscreen mode

Other car classes are implemented in the same way. The factory pattern allows all the concrete classes to have their own implementations of these abstract methods, which may or may not be the same.

For example our GenericCar class can be implement like this:

from cars.abscars import AbsCars

class GenericCar(AbsCars):
  def __init__(self, car_name):
    self.name = car_name

  def start(self):
    print(f"{self.name} engine starting!")

  def stop(self):
    print(f"{self.name} engine stopping!")
Enter fullscreen mode Exit fullscreen mode

Now our CarFactory class

from inspect import getmembers, isclass, isabstract
import cars

class CarFactory():
  cars = {}

  def __init__(self):
    self.load_cars()

  def load_cars(self):
    classes = getmembers(cars, lambda m: isclass(m) and not isabstract(m))
    for name, _type in classes:
      if isclass(_type) and issubclass(_type, autos.AbsCars):
        self.cars.update([[name, _type]])

  def create_instance(self, car_name):
    if car_name in self.cars:
      return self.cars[car_name]()
    else:
      return cars.GenericCar(car_name)
Enter fullscreen mode Exit fullscreen mode

Cars dictionary here will keep a reference to the class against the name of the car model.

The init dunder method will call load_cars() which builds the cars dictionary.

The load_cars method will get all the classes in the cars package that are not abstract classes, and then it will look for classes that are subclasses of our AbsCars class and add them to the cars dictionary.

The create_instance function looks for the car name in the cars dictionary and, if found, returns a new instance of the class; otherwise, it returns an instance of the GenericCars class.

Lastly, in our main module, we just need to import and instantiate the AutoCars class, then loop through the car names, calling the create_instance method for each car.

from cars.carfactory import CarFactory

factory = CarFactory()

for car_name in ['Ford', 'Ferrari', 'Tesla']:
  car = factory.create_instance(car_name)
  car.start()
  car.stop()
Enter fullscreen mode Exit fullscreen mode

With this approach we can keep on adding support for more car models by just adding the car classes implementing the AbsCar class and not touch any other code in our project!

Top comments (0)