In a previous lesson, “Designing Enterprise Level End to End Systems from Idea to Product, ” Hassan Habib broke down a great approach to developing software. For someone new, this might feel like a lot to take in, so please consider watching this video again at a later stage.
One of the big takeaways from this video was how to deconstruct requirements and then construct smaller “systems” into different “blocks” that have “Exposure > System > Dependency” components. What he does discuss in this video is how to construct these technically. If you want to do a deep dive, you might want to read through https://github.com/hassanhabib/The-Standard.
A very nieve explanation of how to construct these technically is known as the “factory pattern.”
The Factory Pattern
When you think of a factory, the first thing that comes to mind is the place where things are made. This type of facility is usually centralized where all production activities are carried out.
As an example, a car factory can create a customized car based on the specifications of a work order. Once it’s complete, the Car will be delivered to the customer.
What is essential to understand is that the customer does not know how the Car is made, they send in the details (parameters) of what they want, and the factory creates and delivers the Car.
Like their real-world counterparts, a software factory (that is, software that implements the factory design pattern) is a type of object (section of the software) responsible for delivering other objects to the customer based on the inputs received from the design pattern.
There are three variations of the factory pattern:
- Simple Factory Pattern. This allows interfaces for creating objects without exposing the object creation logic to the client.
- Factory Method Pattern. This allows interfaces for creating objects but will enable subclasses to determine which class to instantiate.
- Abstract Factory Pattern. Unlike the above two patterns, an abstract factory is an interface to create related objects without specifying/exposing their classes. We can also say that it provides an object of another factory responsible for creating required objects.
Let’s clear up some of those technical words before we continue:
| Technical Term | Definition |
| Data Type | A data type is a set of possible values and a set of allowed operations on it. A data type tells the compiler or interpreter how the programmer intends to use the data.
Examples: |
| Class | Classes are user-defined data types that act as blueprints for individual objects, attributes and methods. |
| Object | Objects are instances of a class created with specifically defined data. An object has a state (data) and behaviour (code). |
| Attribute | An attribute is a changeable property or characteristic of some component of a program that can be set to different values. Classes can have user-defined attributes that help describe what the object is or assist in the functionality. |
| Function | A function is simply a “chunk” of code that you can use repeatedly, rather than writing it out multiple times. Functions enable programmers to break down or decompose a problem into smaller chunks, each of which performs a particular task. It is a programmed procedure of steps to achieve a specific goal. |
| Method | A method is a “Function” that is defined as part of a class and included in any object of that class. A class (and thus an object) can have more than one method. |
The Problem
Let’s assume you have a class focused on creating a car with all the necessary attributes and methods related to a specific car.
# car.py
class Car():
make: str
model: str
colour: str
speed: float = 2
location_x: float = 0.0
location_y: float = 0.0
def drive(x_direction: str, y_direction: str):
y_multiplier = 1
x_multiplier = 1
if y_direction= 'down':
y_multiplier = -1
if x_direction= 'left':
x_multiplier = -1
move(x_multiplier, y_multiplier)
def move(x_multiplier: int, y_multiplier:int):
self.location_x = self.location_x + (x_multiplier * self.speed)
self.location_y = self.location_y + (y_multiplier * self.speed)
As you start working on the Car, you’ll eventually need to make changes to how the object is created. To maintain the consistency of the design, you should create classes that are based on the cart type.
As the project progresses, you’ll eventually need to change how the Car is created. For instance, instead of just creating classes focused on the Car, you should create classes based on the Car type.
The “Factory design pattern” might help us make this more extendable and follow what we learned from Hassan.
How will the factory pattern help?
The Factory Method pattern suggests replacing direct object construction calls (using the new operator) with calls to a special factory method. Don’t worry: the objects are still created via the new operator, but it’s being called from within the factory method. Objects returned by a factory method are often referred to as products.
To put it simply, the factory pattern is a way of creating objects in a program without specifying exactly what type of object will be created. The factory class handles the creation of the objects, and the specific type of object that is created is determined by the parameters that are passed to the factory method. This allows for more flexibility in the code, and makes it easier to add new types of objects without having to make changes to existing code.
Extended Example:
If we look at the Car example class, you’ll eventually need to change or extend how the object is created.
In this example, we define two new classes that inherit from the Car class: SUV and ElectricCar. Each of these new classes has its own unique property (four_wheel_drive and number_of_batteries, respectively) in addition to the properties inherited from the Car class.
# car.py
class SUV(Car):
four_wheel_drive: bool = True
class ElectricCar(Car):
number_of_batteries: int = 1
We then define a CarFactory class with a static method create_car() that takes four parameters (car_type, make, model, and colour) and returns a new Car object based on the car_type parameter. If car_type is "SUV", it creates a new SUV object, if it’s "ElectricCar", it creates a new ElectricCar object, otherwise it creates a regular Car object.
# car.py
class CarFactory:
@staticmethod
def create_car(car_type: str, make: str, model: str, colour: str) -> Car:
if car_type == "SUV":
return SUV(make=make, model=model, colour=colour)
elif car_type == "ElectricCar":
return ElectricCar(make=make, model=model, colour=colour)
else:
return Car(make=make, model=model, colour=colour)
Previous lessons “write-shorter-conditionals-using-dictionaries” and “Avoid endless if-else statements with this Python Tip!!” showed us we can simplify the function and rather use a dictionary to look up which types of cars can be created:
# car.py
class CarFactory:
def __init__(self):
self.car_types = {
"SUV": SUV,
"ElectricCar": ElectricCar,
}
@staticmethod
def create_car(car_type: str, make: str, model: str, colour: str) -> Car:
car_class = self.car_types.get(car_type,Car)
return car_class(make=make, model=model, colour=colour)
This will simplify the Factory class and make it clean to add additional car types, as well as help developers ensure they are not duplicating car names for users in a clear way
This implementation allows us to create different types of Car objects (Car, SUV, and ElectricCar) by calling the create_car() method on the CarFactory class, passing in the appropriate car_type parameter, and the other required parameters for the Car object. For example:
# car.py
regular_car = CarFactory.create_car(car_type="Car", make="Toyota", model="Corolla", colour="Blue")
suv = CarFactory.create_car(car_type="SUV", make="Jeep", model="Wrangler", colour="Black")
electric_car = CarFactory.create_car(car_type="ElectricCar", make="Tesla", model="Model S", colour="Red")
Here, regular_car will be a regular Car object, suv will be an SUV object with four_wheel_drive set to True, and electric_car will be an ElectricCar object with number_of_batteries set to 1.
Applicability
Question
Use the Factory Method when you don’t know beforehand the exact types and dependencies of the objects your code should work with.
Considerations
The Factory Method separates product construction code from the code that actually uses the product. Therefore, extending the product construction code independently from the rest of the code is easier.
For example, to add a new product type to the app, you’ll only need to create a new creator subclass and override the factory method in it.
Question
Use the Factory Method when you want to provide users of your library or framework with a way to extend its internal components.
Considerations
Inheritance is probably the easiest way to extend the default behaviour of a library or framework. But how would the framework recognize that your subclass should be used instead of a standard component?
The solution is to reduce the code that constructs components across the framework into a single factory method and let anyone override this method and extend the component itself.
Let’s see how that would work. Imagine that you write an app using an open source UI framework. Your app should have round buttons, but the framework only provides square ones. You extend the standard Button class with a glorious RoundButton subclass. But now you need to tell the main UIFramework class to use the new button subclass instead of a default one.
To achieve this, you create a subclass UIWithRoundButtons from a base framework class and override its createButton method. While this method returns Button objects in the base class, you make your subclass return RoundButton objects. Now use the UIWithRoundButtons class instead of UIFramework. And that’s about it!
Question
Use the Factory Method when you want to save system resources by reusing existing objects instead of rebuilding them each time.
Considerations
You often experience this need when dealing with large, resource-intensive objects such as database connections, file systems, and network resources.
Let’s think about what has to be done to reuse an existing object:
- First, you need to create some storage to track all the created objects.
- When someone requests an object, the program should look for a free object inside that pool.
- … and then return it to the client code.
- If there are no free objects, the program should create a new one (and add it to the pool).
That’s a lot of code! And it must all be put into a single place so you don’t pollute the program with duplicate code.
Probably the most obvious and convenient place where this code could be placed is the class’s constructor whose objects we’re trying to reuse. However, a constructor must always return new objects by definition. It can’t return existing instances.
Therefore, you need a regular method capable of creating new objects and reusing existing ones. That sounds very much like a factory method.
Summary of How to Implement
- Make all products follow the same interface. This interface should declare methods that make sense in every product.
- Add an empty factory method inside the creator class. The return type of the method should match the common product interface.
- In the creator’s code, find all references to product constructors. One by one, replace them with calls to the factory method while extracting the product creation code into the factory method. You might need to add a temporary parameter to the factory method to control the type of returned product. At this point, the code of the factory method may look pretty ugly. It may have a large
switchstatement that picks which product class to instantiate. But don’t worry. We’ll fix it soon enough. - Now, create a set of creator subclasses for each type of product listed in the factory method. Override the factory method in the subclasses and extract the appropriate bits of construction code from the base method.
- If there are too many product types and it doesn’t make sense to create subclasses for all of them, you can reuse the control parameter from the base class in subclasses.
- If the base factory method has become empty after all of the extractions, you can make it abstract. If there’s something left, you can make it a default behaviour of the method.
Pros
- You avoid tight coupling between the creator and the concrete products.
- Single Responsibility Principle. You can move the product creation code into one place in the program, making the code easier to support.
- Open/Closed Principle. You can introduce new types of products into the program without breaking existing client code.
Cons
- The code may become more complicated since you need to introduce a lot of new subclasses to implement the pattern. The best-case scenario is when you’re introducing the pattern into an existing hierarchy of creator classes.
Responses