Python: Start using Factory Pattern design
We all know that the Design patterns are a set of best practices that can be used to solve recurring problems in software development. One of the most popular design patterns is the Factory Pattern
.
In this article, I will show you how Factory Pattern
can simplify object creation and how you can use it to separate creation from use. So lets dive in —
Introducing Factory Method
The Factory Pattern
is a creational design pattern that provides a way to create objects without exposing the creation logic to the client. So this allows you to create objects in a superclass, but allows subclasses to alter the type of objects that will be created
In other words, it provides a way to encapsulate object creation logic and decouple it from the rest of the code.
Basic Implementation of Factory Pattern
Let’s look at an example to see how the Factory Pattern can be implemented in Python.
- Here I have created a simple class hierarchy for different types of animals as shown below:
class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
- Here, an
Animal
class have aspeak
method. And also have two subclasses,Dog
andCat
, that override thespeak
method to return different sounds. - Now, let’s say I want to create a program that creates different types of animals based on user input. I can use the
Factory Pattern
to create objects without knowing the exact type of animal that will be created, which is very useful as in the later stage when you decide to change out those objects by other objects, you don’t have to change the original code.
Here is the implementation of Factory Pattern:
class AnimalFactory:
def create_animal(self, animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
raise ValueError("Invalid animal type")
# Client code
factory = AnimalFactory()
animal1 = factory.create_animal("dog")
animal2 = factory.create_animal("cat")
As you can see, I have an AnimalFactory
class that defines a create_animal
method. The create_animal
method takes an animal_type argument and returns an instance of the corresponding animal class. If an invalid animal_type is provided, a ValueError
is raised.
In the client code or you can say in the main() method, I created an instance of the AnimalFactory
class and use the create_animal
method to create two different types of animals: a Dog
and a Cat
.
How does the Factory Pattern Work?
In the Factory Pattern, there is a Creator class that defines an abstract method for creating objects.
The Creator class can be an abstract class or an interface.
Subclasses of the Creator class are responsible for implementing the create method to create the specific type of object.
The Factory Pattern allows the client code to create objects without knowing the exact type of object that will be created.
The basic flow of the Factory Pattern is as follows:
- The client calls the factory method on the Creator interface.
- The Creator interface then creates a Concrete Product object and returns it to the client.
- The client then uses the Concrete Product object.
Use Cases
- When faced with the task of creating objects in complex systems, the factory pattern can be a useful tool:
Object creation can be a complex task, especially in large-scale systems where there may be many different types of objects that need to be created based on different configurations or requirements.
Creating objects manually can be time-consuming, error-prone, and may lead to code duplication. This is where design patterns such as the Abstract Factory Pattern can be useful.
The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Suppose you are building a coffee shop application where customers can order different types of coffee. Each coffee has a name, a price, and a set of ingredients. You could represent each type of coffee as a Python class, with a method for calculating the cost of the coffee based on its ingredients.
class Coffee:
def __init__(self, name, price, ingredients):
self.name = name
self.price = price
self.ingredients = ingredients
def cost(self):
return sum([ingredient.cost() for ingredient in self.ingredients])
class Espresso(Coffee):
def __init__(self):
super().__init__('Espresso', 2.0, [CoffeeBean(), Water()])
class Latte(Coffee):
def __init__(self):
super().__init__('Latte', 3.0, [CoffeeBean(), Milk(), Water()])
The Coffee
class is an abstract class that defines the basic attributes of a coffee. The Espresso
and Latte
classes are concrete subclasses that define the specific ingredients and price for each type of coffee.
Now, suppose you want to add a new type of coffee to your application, such as a cappuccino. You would need to create a new subclass of the Coffee
class and define the specific ingredients and price for the cappuccino. However, this approach has a limitation: if you have many different types of coffee, the code for creating each type of coffee can become repetitive and difficult to maintain.
This is where the factory pattern comes in. Instead of creating each type of coffee directly, you can define a CoffeeFactory
class that has a method for creating each type of coffee:
class CoffeeFactory:
def create_coffee(self, coffee_type):
if coffee_type == 'espresso':
return Espresso()
elif coffee_type == 'latte':
return Latte()
else:
raise ValueError(f"Invalid coffee type: {coffee_type}")
The CoffeeFactory
class has a create_coffee
method that takes a coffee_type
parameter and returns a new instance of the appropriate type of coffee. Now, when you want to create a new coffee in your application, you can simply call the create_coffee
method of the CoffeeFactory
class:
factory = CoffeeFactory()
espresso = factory.create_coffee('espresso')
latte = factory.create_coffee('latte')
- Another use case for the factory pattern involves the issue of creating multiple object instances:
Suppose you are building a simple banking application in Python, and you have defined a BankAccount
class to represent a bank account. The class has a balance
attribute to store the current balance of the account, and methods to deposit and withdraw money from the account:
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient balance")
self.balance -= amount
Now, suppose you create two instances of the BankAccount
class, one for each of two customers:
customer1_account = BankAccount()
customer2_account = BankAccount()
If both customers deposit some money into their accounts, you would expect their account balances to be updated accordingly. For example:
customer1_account.deposit(100)
customer2_account.deposit(50)
print(customer1_account.balance) # prints 100
print(customer2_account.balance) # prints 50
However, what if you accidentally assign one account object to another? For example:
customer1_account = customer2_account
customer1_account.deposit(100)
print(customer2_account.balance) # prints 100, not 50!
This happens because both customer1_account
and customer2_account
are now pointing to the same object in memory. So when you deposit money into customer1_account
, you are actually updating the balance of the shared object, which affects the balance of customer2_account
as well. This can lead to unexpected behavior and difficult-to-debug errors in your program.
To avoid this problem, you can use a design pattern like the Singleton Pattern to ensure that only one instance of a class is created and shared among all parts of your program that need to use it.
This can help you to manage object creation and prevent issues related to multiple object instances.
Here is the implementation in Factory Pattern for the above scenario:
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient balance")
self.balance -= amount
class BankAccountFactory:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.accounts = {}
return cls._instance
def create_account(self, account_number, balance=0):
if account_number not in self.accounts:
account = BankAccount(balance)
self.accounts[account_number] = account
else:
account = self.accounts[account_number]
return account
In this implementation, we use the Factory Pattern to create and manage instances of the BankAccount
class. The BankAccountFactory
class is designed as a Singleton, so there is only one instance of it in the program. The create_account
method takes an account number and balance as arguments, and returns a BankAccount
object. If the account number already exists in the accounts
dictionary, the method returns the existing BankAccount
object; otherwise, it creates a new one and adds it to the dictionary.
Here’s an example of how to use the BankAccountFactory
class to create and manage BankAccount
objects:
factory = BankAccountFactory()
account1 = factory.create_account('123')
account2 = factory.create_account('456')
account1.deposit(100)
account2.deposit(50)
print(account1.balance) # prints 100
print(account2.balance) # prints 50
account3 = factory.create_account('123') # returns existing account
account3.deposit(50)
print(account1.balance) # prints 150, not 50!
As you can see, the BankAccountFactory
ensures that there is only one instance of each BankAccount
object, which prevents issues related to multiple object instances.
- Data pipeline implementation is another scenario where the factory pattern can be useful:
For example, consider a data pipeline that performs data cleaning, transforming and loading into a database.
You could create a factory class that generates the appropriate steps for each type of data.
The factory class would have a method to generate the processing steps based on the type of data provided as input.
Here is the implementation:
class DataProcessor:
def process(self, data):
pass
class CleanDataProcessor(DataProcessor):
def process(self, data):
# code to clean data
return data
class TransformDataProcessor(DataProcessor):
def process(self, data):
# code to transform data
return data
class LoadDataProcessor(DataProcessor):
def process(self, data):
# code to load data
return data
class DataProcessorFactory:
@staticmethod
def create_data_processor(processor_type):
if processor_type == "clean":
return CleanDataProcessor()
elif processor_type == "transform":
return TransformDataProcessor()
elif processor_type == "load":
return LoadDataProcessor()
else:
raise ValueError("Invalid processor type")
data = [1, 2, 3, 4, 5]
processor = DataProcessorFactory.create_data_processor("clean")
data = processor.process(data)
processor = DataProcessorFactory.create_data_processor("transform")
data = processor.process(data)
processor = DataProcessorFactory.create_data_processor("load")
data = processor.process(data)
In this example, the DataProcessor
class serves as the base class for different types of data processing steps (Cleaning, Transforming and Loading). The DataProcessorFactory
class is the factory class that encapsulates the object creation process.
The create_data_processor
method takes in a processor_type
argument and returns an instance of either the CleanDataProcessor
, TransformDataProcessor
or LoadDataProcessor
class based on the type provided.
In conclusion, the Factory Pattern
is a powerful and flexible design pattern that is widely used in software development.
It provides a way to create objects without specifying the exact class of object that will be created and centralizes the object creation process in a single place.
By using the Factory Method, developers can create objects of different types based on some input, making the code more flexible, maintainable, and less prone to errors.
I believe this article helped you in understanding and getting a basic idea about this design pattern as it is an essential tool for any software developer to have in their toolkit and can be used in a variety of situations where objects need to be created dynamically based on some input.
Connect with me on LinkedIn