Python Design Patterns Cookbook: Recipes for Clean and Reusable Code (Factory Method)
Factory Method Design Pattern in Python: A Guide to Creating Objects
Design Patterns: An Introduction and Explanation
Design patterns are like recipes that software developers use to solve common problems that come up when building complex software. Like how different chefs might use slightly different ingredients or techniques to make the same dish, developers can use different approaches to implement the same design pattern. Design patterns are not strict rules that apply to every situation, but rather a blueprint that can be customized to fit the specific problem at hand. They can be used with any programming language and it's important to choose the correct design pattern for each situation.
Factory Design Pattern
Introduction
When a software program has multiple components that share similarities but have different implementations for general operations, a junior developer may use conditionals like if-else or switch-case to handle the differences. However, as the number of components grows, the code becomes difficult to manage and maintain. For example, imagine a program that handles different types of shapes, such as circles and squares. A junior developer may write a method that handles the calculation of area for each shape separated by if-else, but as new shapes are added, the code becomes longer and harder to manage.
To avoid the problems that come with using conditionals for handling multiple components' behavior, a better approach is to use the Factory Method pattern. The Factory Method pattern involves creating an interface or abstract class that represents the category, such as the "Shape" interface in the previous example. Each shape, such as a "Circle" or "Square," then implements this interface and defines its behavior for calculating area. This approach simplifies the code because each new shape can simply implement the "Shape" interface that will enforce the new shape to implement a method for calculating area without affecting the existing code.
Issue You May Face
You've been tasked with creating software that guides users through repairing electronic devices, starting with mobile phones and laptops. However, the process for repairing different components, such as screens, batteries, and ports, is unique to each device, and not all repairs are possible in both. Additionally, the company plans to add seven more devices to the system within the next three months, and the repair instructions may need to be updated as technology evolves. To ensure scalability and ease of modification, the system needs to be designed accordingly.
To simplify matters, it is often assumed that all models of a particular mobile phone or laptop can be repaired using the same process.
A Logical Solution
One way to solve this problem is by categorizing the devices based on their common characteristics, such as whether or not they have a SIM slot or a HDD. You could then create separate sets of instructions for each category, which would allow you to provide device-specific instructions while still keeping the instructions organized and easy to manage. This would also make it easier to add new devices to the system in the future, since you could simply categorize them based on their characteristics and create a new set of instructions for that category.
Using Factory Method
To implement the Factory method, you can start by creating an abstract class called RepairGuide. This class will define the interface for all the repair guides and should include abstract methods for repairing the screen, RAM, and SIM slot. Then, you can create two concrete classes, MobileRepairGuide and LaptopRepairGuide, that implement the RepairGuide interface and provide device-specific repair instructions.
Next, you can create a RepairGuideFactory class that will create the appropriate RepairGuide object based on the device type passed to it. In your main program, you can create a RepairGuide object for a mobile device using the RepairGuideFactory. Then, you can display a list of available repair options to the user by getting the list of subclasses of the RepairGuide class using the subclasses() method.
Once the user selects a device, you can create the appropriate RepairGuide object using the RepairGuideFactory. Then, you can display a list of available repair options for the selected device by getting a list of all the non-private methods of the object using the inspect.getmembers() function.
You can iterate through this list and display the name of each method, which corresponds to a repair option. Then, prompt the user to select a repair option. Once the user selects a repair option, you can call the corresponding method on the RepairGuide object. This will execute the device-specific repair instructions for the selected repair option.
Code Example
import inspect
from abc import ABC, abstractmethod
class RepairGuide(ABC):
@abstractmethod
def repair_screen(self):
pass
@abstractmethod
def repair_ram(self):
pass
@abstractmethod
def repair_sim_slot(self):
pass
class MobileRepairGuide(RepairGuide):
def repair_screen(self):
print("Mobile screen repair instructions.")
def repair_ram(self):
print("RAM repair is not possible on mobile phones..")
def repair_sim_slot(self):
print("Mobile SIM slot repair instructions.")
class LaptopRepairGuide(RepairGuide):
def repair_screen(self):
print("Laptop screen repair instructions.")
def repair_ram(self):
print("Laptop RAM repair instructions.")
def repair_sim_slot(self):
print("SIM slot repair is not possible on laptops.")
class RepairGuideFactory:
@staticmethod
def create_repair_guide(device_type):
if device_type == "mobile":
return MobileRepairGuide()
elif device_type == "laptop":
return LaptopRepairGuide()
else:
return None
@staticmethod
def sub_class_list():
return RepairGuide.__subclasses__()
options = []
count = 1
for sub_class in RepairGuideFactory.sub_class_list():
print(str(count) + ': ' + sub_class.__name__[:sub_class.__name__.find('R')])
options.append(sub_class.__name__[:sub_class.__name__.find('R')].lower())
count += 1
selected = input('Which device you want to repair? \n')
obj = RepairGuideFactory.create_repair_guide(options[int(selected)-1])
repair_options = []
count = 1
functions = inspect.getmembers(type(obj), predicate=inspect.isfunction)
for func in functions:
if not func[0].startswith('__'):
print(str(count) + ': ' + func[0].capitalize().replace('_', ' '))
repair_options.append(getattr(obj, func[0]))
count += 1
selected = input('Which part you want to repair? \n')
repair_options[int(selected)-1]()
With the current implementation of the Factory Method, you can add as many new device types as you want without having to modify any of the existing code. Additionally, if you need to add repair instructions for a new device type, you can do so with confidence because any new child class that you create must implement all the abstract methods that are defined in the superclass.
Why not Simple if-else?
As previously demonstrated, the Factory Method pattern can provide a high degree of flexibility to your code. Although this level of dynamic implementation may not always be necessary, using the Factory Method approach offers additional benefits. By avoiding the use of conditional statements like if-else for object creation, the code becomes easier to maintain. Additionally, the Factory Method pattern increases testability by allowing for the entire method to be mocked during testing.
Furthermore, the Factory Method pattern strongly supports four fundamental Object-Oriented Programming principles: encapsulation, abstraction, polymorphism, and the Open/Closed principle.
Factors Should be Taken into Consideration
By considering following factors, you can determine whether the Factory Method pattern is the right solution for your specific situation.
The number of products or object types: The Factory Method pattern is most useful when you have a large number of related objects that need to be created.
The complexity of object creation: If creating an object is a complex process that involves multiple steps, it makes sense to use a Factory Method to encapsulate that complexity.
Dependency injection: If you need to create objects that depend on other objects, you can use the Factory Method to inject those dependencies into the objects being created.
Testability: Using the Factory Method can make your code more testable by allowing you to easily create mock objects for testing.
Extensibility: The Factory Method pattern can make your code more extensible by allowing you to add new products or object types without changing the existing code.
Conclusion
The Factory Method is a design pattern that has many benefits and can make your code more dynamic, flexible, and maintainable. The pattern allows you to create objects without having to know the exact class of the object that will be created. This level of abstraction provides many benefits, including better encapsulation, abstraction, polymorphism, and adherence to the open/closed principle.
When creating a Factory Method, there are several factors that you should take into consideration. These include the complexity of the objects being created, the need for flexibility and extensibility, and the maintainability of the code. By carefully considering these factors, you can create a Factory Method that provides maximum benefits and reduces the risk of errors and maintenance problems.
In conclusion, the Factory Method is a powerful design pattern that can help you create more dynamic, flexible, and maintainable code. By following best practices and considering the key factors, you can create a Factory Method that provides many benefits and improves the overall quality of your code. Whether you are a beginner or an experienced developer, the Factory Method is a pattern that you should consider adding to your design toolbox.