Creational Design Patterns in Software Development
Introduction
Creational design patterns provide various ways to instantiate objects while abstracting the instantiation process. They lead to more flexible and reusable code, making it easier to design robust and maintainable software systems. This post will cover the Singleton, Prototype, Factory Method, Builder, and Abstract Factory patterns.
Singleton Pattern
Introduction
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is often used in scenarios where exactly one object is needed to coordinate actions across the system.
Definition
The Singleton pattern restricts the instantiation of a class to one single instance and provides a global access point to that instance.
Use Cases
- Logging
- Configuration settings
- Managing connections to a database
- Caching
Benefits
- Controlled access to the sole instance
- Reduced namespace pollution
- Permits refinement of operations and representation
- Provides a single point of access
Examples
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1 is singleton2) # Output: True
Real-world Applications
In web applications, the Singleton pattern is often used for database connection pooling. For example, a web server may need a single database connection pool to manage access to the database, ensuring efficient resource utilization and connection management.
Prototype Pattern
Introduction
The Prototype pattern is used to create a duplicate object while keeping performance in mind. This pattern involves implementing a prototype interface which tells to create a clone of the current object.
Definition
The Prototype pattern is a creational design pattern in which new objects are created by copying an existing object, known as the prototype.
Use Cases
- When the type of objects to create is determined by a prototypical instance
- To avoid the overhead of creating new objects from scratch
- When creating an object is costly or complex
Benefits
- Reduces the need for creating subclasses
- Speeds up the instantiation process
- Simplifies the creation of complex objects
Examples
import copy
class Prototype:
def __init__(self, value):
self.value = value
def clone(self):
return copy.deepcopy(self)
prototype1 = Prototype([1, 2, 3])
prototype2 = prototype1.clone()
prototype2.value.append(4)
print(prototype1.value) # Output: [1, 2, 3]
print(prototype2.value) # Output: [1, 2, 3, 4]
Real-world Applications
In game development, the Prototype pattern is used to create different game characters or objects dynamically. For instance, a game might have a prototypical enemy object that can be cloned and customized for different enemy types.
Factory Method Pattern
Introduction
The Factory Method pattern defines an interface for creating an object but allows subclasses to alter the type of objects that will be created. This pattern lets a class defer instantiation to subclasses.
Definition
The Factory Method pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created.
Use Cases
- When a class cannot anticipate the type of objects it needs to create
- To allow subclasses to provide specific implementations
- To delegate the responsibility of instantiation to subclasses
Benefits
- Promotes loose coupling
- Enhances code reusability
- Facilitates the addition of new products without modifying existing code
Examples
from abc import ABC, abstractmethod
class Product(ABC):
@abstractmethod
def operation(self) -> str:
pass
class ConcreteProductA(Product):
def operation(self) -> str:
return "Result of ConcreteProductA"
class ConcreteProductB(Product):
def operation(self) -> str:
return "Result of ConcreteProductB"
class Creator(ABC):
@abstractmethod
def factory_method(self) -> Product:
pass
def some_operation(self) -> str:
product = self.factory_method()
return f"Creator: The same creator's code has just worked with {product.operation()}"
class ConcreteCreatorA(Creator):
def factory_method(self) -> Product:
return ConcreteProductA()
class ConcreteCreatorB(Creator):
def factory_method(self) -> Product:
return ConcreteProductB()
creator_a = ConcreteCreatorA()
print(creator_a.some_operation()) # Output: Creator: The same creator's code has just worked with Result of ConcreteProductA
creator_b = ConcreteCreatorB()
print(creator_b.some_operation()) # Output: Creator: The same creator's code has just worked with Result of ConcreteProductB
Real-world Applications
In web development frameworks, the Factory Method pattern is often used to create objects like controllers, services, or handlers based on configuration or user input.
Builder Pattern
Introduction
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
Definition
The Builder pattern is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.
Use Cases
- When an object requires numerous steps to be created
- To construct composite objects
- To improve readability and maintainability of the code
Benefits
- Allows for more control over the construction process
- Enhances code readability and maintainability
- Supports the creation of different representations of an object
Examples
class House:
def __init__(self):
self.rooms = []
self.floors = 0
self.has_garage = False
def __str__(self):
return f"House with {self.floors} floors, {'a garage' if self.has_garage else 'no garage'}, and rooms: {', '.join(self.rooms)}"
class HouseBuilder:
def __init__(self):
self.house = House()
def add_room(self, room):
self.house.rooms.append(room)
return self
def set_floors(self, floors):
self.house.floors = floors
return self
def add_garage(self):
self.house.has_garage = True
return self
def build(self):
return self.house
builder = HouseBuilder()
house = builder.set_floors(2).add_room("Kitchen").add_room("Bedroom").add_garage().build()
print(house) # Output: House with 2 floors, a garage, and rooms: Kitchen, Bedroom
Real-world Applications
The Builder pattern is used in many modern development frameworks to construct complex objects such as UI components, configuration objects, or even SQL queries.
Abstract Factory Pattern
Introduction
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Definition
The Abstract Factory pattern is a creational design pattern that lets you produce families of related objects without specifying their concrete classes.
Use Cases
- When a system needs to be independent of how its objects are created
- To manage and organize a group of related or dependent objects
- When families of related objects need to be used together
Benefits
- Promotes consistency among products
- Isolates concrete classes
- Facilitates the exchange of product families
- Supports the addition of new products
Examples
from abc import ABC, abstractmethod
class AbstractFactory(ABC):
@abstractmethod
def create_product_a(self):
pass
@abstractmethod
def create_product_b(self):
pass
class ConcreteFactory1(AbstractFactory):
def create_product_a(self):
return ProductA1()
def create_product_b(self):
return ProductB1()
class ConcreteFactory2(AbstractFactory):
def create_product_a(self):
return ProductA2()
def create_product_b(self):
return ProductB2()
class ProductA1:
def useful_function_a(self) -> str:
return "The result of the product A1."
class ProductA2:
def useful_function_a(self) -> str:
return "The result of the product A2."
class ProductB1:
def useful_function_b(self) -> str:
return "The result of the product B1."
class ProductB2:
def useful_function_b(self) -> str:
return "The result of the product B2."
def client_code(factory: AbstractFactory) -> None:
product_a = factory.create
_product_a()
product_b = factory.create_product_b()
print(product_b.useful_function_b())
print(product_a.useful_function_a())
client_code(ConcreteFactory1())
# Output:
# The result of the product B1.
# The result of the product A1.
client_code(ConcreteFactory2())
# Output:
# The result of the product B2.
# The result of the product A2.
Real-world Applications
Abstract Factory patterns are commonly used in GUI toolkits to create families of related objects such as buttons, checkboxes, and windows, ensuring consistency across different platforms.
Conclusion
Creational design patterns provide various ways to instantiate objects while abstracting the instantiation process, leading to more flexible and reusable code. By understanding and applying these patterns, developers can design more robust and maintainable software systems.