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()
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 – 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
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")
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!")
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)
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()
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)