Creational Patterns
- Focus: How objects are created in a system.
- Goal: Decouple object creation from specific classes, making the system more flexible and adaptable.
- Examples:
- Factory Method: Creates objects without exposing the specific class being created.
- Singleton: Ensures only one instance of a class exists throughout the application.
- Builder: Separates object construction from its representation, allowing for step-by-step object creation.
Factory Method
The "Factory" creational pattern is a powerful tool in object-oriented programming that helps manage the process of object creation. It essentially acts as a middleman between your code and the actual object creation, offering several benefits:
class Animal:
pass
class Dog(Animal):
pass
class Duck(Animal):
pass
class Cat(Animal):
pass
class AnimalFactory():
def createAnimal():
pass
class RandomAnimalFactory(AnimalFactory):
def createAnimal():
a = Animal()
return a
class BalancedAnimalFactory(AnimalFactory):
def createAnimal():
a = Animal()
return a
# animal
class Product:
pass
#animal factory - creates products
class Creator:
def factory():
return Product
# cat,dog,ducks
class ConcreteProduct(Product):
pass
# random factory or balanced factory
class ConcreteCreator(Creator):
def factory():
return Product
Concept:
- Decoupling: It separates the code that uses an object from the code that creates the object. This makes your code more flexible and easier to maintain, as you can change how objects are created without affecting the rest of your code.
- Centralization: Object creation logic is centralized in one place, making it easier to control and understand.
- Flexibility: You can easily create different types of objects based on different conditions without changing the client code.
How it works:
Imagine you have a coffee shop application. Instead of directly calling constructors for different types of coffee (latte, espresso, etc.), you use a "CoffeeFactory" class. This factory class provides a method like "createCoffee" that takes your order (e.g., "latte") and returns the appropriate coffee object.
Types of Factory patterns:
There are two main types:
- Factory Method: Defines an interface for creating objects, but lets subclasses decide which specific class to instantiate. This allows for flexibility in choosing the concrete object based on different scenarios.
- Abstract Factory: Creates families of related objects. This is useful when you need to create multiple objects that belong together, like a furniture set or a UI component suite.
Benefits:
- Reduced code duplication: You don't need to repeat object creation logic throughout your code.
- Increased flexibility: You can easily change how objects are created without affecting client code.
- Improved maintainability: Your code is easier to understand and modify.
When to use it:
- When you need to create different types of objects based on certain conditions.
- When you want to centralize object creation logic.
- When you want to decouple the code that uses an object from the code that creates it.
Remember:
- Factory patterns are not always necessary, and using them too much can add complexity.
- Choose the right type of factory pattern based on your needs.
Abstract Factory
Builder
The builder creational pattern is another valuable tool in your object-oriented programming toolbox, focusing on building complex objects in a step-by-step manner.
class Pizza:
def __init__(self, dough, sauce, toppings):
self.dough = dough
self.sauce = sauce
self.toppings = toppings
def __str__(self):
return f"Pizza: {self.dough} dough, {self.sauce} sauce, {', '.join(self.toppings)} toppings"
class PizzaBuilder:
def __init__(self):
self.dough = None
self.sauce = None
self.toppings = []
def with_dough(self, dough):
self.dough = dough
return self
def with_sauce(self, sauce):
self.sauce = sauce
return self
def with_topping(self, topping):
self.toppings.append(topping)
return self
def build(self):
if not all([self.dough, self.sauce]):
raise ValueError("Missing required ingredients: dough and sauce")
return Pizza(self.dough, self.sauce, self.toppings)
# Usage examples
pizza1 = PizzaBuilder().with_dough("thin").with_sauce("tomato").with_topping("cheese").build()
print(pizza1) # Output: Pizza: thin dough, tomato sauce, cheese toppings
pizza2 = PizzaBuilder().with_dough("thick").with_sauce("pesto").with_topping("cheese").with_topping("mushrooms").build()
print(pizza2) # Output: Pizza: thick dough, pesto sauce, cheese, mushrooms toppings
# Attempting to build without required ingredients raises an error
try:
pizza3 = PizzaBuilder().with_topping("pepperoni").build()
except ValueError as e:
print(e) # Output: Missing required ingredients: dough and sauce
Concept:
- Separation of construction and representation: This pattern separates how an object is built from the actual object itself. This allows for more control and flexibility in constructing complex objects with many optional parts.
- Step-by-step construction: You build the object piece by piece using a dedicated "builder" class. Each step adds or configures a specific aspect of the final object.
- Immutable intermediate states: The builder ensures the object remains in a valid state throughout construction, preventing incomplete or inconsistent objects.
How it works:
Imagine building a sandwich. Instead of directly creating a "Sandwich" object with all ingredients at once, you use a "SandwichBuilder" class. This builder offers methods like "addBread", "addLettuce", "addCheese", etc. You call these methods in the order you want, building your sandwich step-by-step. Finally, the builder provides a method like "build()" that returns the complete "Sandwich" object.
Benefits:
- Clarity and control: The step-by-step approach makes construction easier to understand and manage, especially for complex objects.
- Optional parameters: You can easily create objects with different configurations by skipping or customizing certain builder steps.
- Immutable objects: The pattern encourages building immutable objects, leading to better thread safety and easier reasoning about object state.
When to use it:
- When creating objects with many optional or configurable parts.
- When the construction process is complex and needs to be controlled step-by-step.
- When you want to encourage immutable object creation.
Remember:
- Builder patterns can add some overhead compared to simpler constructors.
- Evaluate the complexity of your object before applying this pattern.
Comparison with Factory:
Both patterns deal with object creation, but they serve different purposes:
- Factory: Focuses on choosing the right object type to create based on conditions.
- Builder: Focuses on building a specific object step-by-step, allowing for customization.
class Person:
def __init__(self,personBuilder):
self.firstName = personBuilder.firstName
self.lastName = personBuilder.lastName
self.middle_attr = personBuilder.middle_attr
self.email_attr = personBuilder.email_attr
self.phone_attr = personBuilder.phone_attr
self.address_attr = personBuilder.address_attr
self.dob_attr = personBuilder.dob_attr
self.idNumber_attr = personBuilder.idNumber_attr
def __repr__(self):
return str(self.__dict__)
class PersonBuilder():
def __init__(self,firstName,lastName):
self.firstName = firstName
self.lastName = lastName
self.middle_attr = ''
self.email_attr = ''
self.phone_attr = ''
self.address_attr = ''
self.dob_attr = ''
self.idNumber_attr = ''
def middleName(self,middleName):
self.middle_attr = middleName
return self
def dob(self,dob):
self.dob_attr = dob
return self
def email(self,email):
self.email_attr = email
return self
def address(self,address):
self.address_attr = address
return self
def phone(self,phone):
self.phone_attr = phone
return self
def idNumber(self,idNumber):
self.idNumber_attr = idNumber
return self
def build(self):
return Person(self)
p1 = Person.PersonBuilder('Joe','Smith').middleName("Cook").build()
p2 = Person.PersonBuilder('Henry','Jones').build()
print(p1)
print()
print(p2)
Prototype
The prototype creational pattern is another useful tool in your programming arsenal, specifically designed for creating new objects by copying existing ones. Here's the breakdown:
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def clone(self):
# Return a shallow copy of this object
return User(self.name, self.age)
# Usage
original_user = User("John", 30)
cloned_user = original_user.clone()
print(f"Original user: {original_user.name}, {original_user.age}")
print(f"Cloned user: {cloned_user.name}, {cloned_user.age}")
# Modify cloned user
cloned_user.name = "Jane"
print(f"Original user: {original_user.name}, {original_user.age}")
print(f"Cloned user: {cloned_user.name}, {cloned_user.age}")
Concept:
- Cloning objects: Instead of constructing objects from scratch, you create new ones by making a copy of an existing "prototype" object. This prototype serves as a template, containing the default configuration and state you want new objects to inherit.
- Efficiency: Cloning can be faster than creating new objects, especially for complex ones with many internal states and configurations.
- Consistency: Ensures all new objects share the same base configuration and properties, promoting consistency and maintainability.
How it works:
Imagine you need to create multiple enemies in a game. Instead of writing code to create each enemy individually, you have a base "Enemy" prototype object with default attributes like health and attack power. When you need a new enemy, you simply clone the prototype, potentially modifying some specific attributes for variation.
Types of Prototype patterns:
There are two main approaches:
- Shallow copy: Creates a new object with its own references to the original object's data. Changes to the copied object's data will also affect the original.
- Deep copy: Creates a new object with independent copies of the original object's data. Changes to the copied object's data will not affect the original.
Benefits:
- Performance improvement: Cloning can be faster than constructing objects from scratch, especially for complex objects.
- Code reuse: Reduces code duplication by leveraging existing object configurations.
- Consistency: Ensures all new objects have a consistent starting point.
When to use it:
- When creating many objects of the same type with similar configurations.
- When object creation involves expensive operations like database lookups.
- When you want to ensure consistency among similar objects.
Remember:
- Prototype patterns may not be suitable for objects with complex internal structures or circular references.
- Choose the appropriate copy type (shallow or deep) based on your data needs and desired behavior.
Singleton
class Database:
__instance = None # Private variable to store the instance
def __new__(cls):
"""
This method controls the creation of instances.
If an instance already exists, it returns that instance.
Otherwise, it creates a new instance and assigns it to the private variable.
"""
if not cls.__instance:
cls.__instance = super().__new__(cls)
return cls.__instance
def __init__(self, host, username, password):
"""
This method is called only once, when the first instance is created.
It initializes the database connection with the provided parameters.
"""
if cls.__instance: # Check if already initialized
raise RuntimeError("Cannot create multiple instances of Singleton")
self.host = host
self.username = username
self.password = password
# Connect to database using provided credentials
# ...
def get_users(self):
# Fetch users from database
# ...
return users
# Usage
database1 = Database("localhost", "user", "password")
database2 = Database("another_host", "another_user", "another_password")
# Check if both instances are the same
print(database1 is database2) # Output: True
# This will raise an error since only one instance is allowed
# database3 = Database("third_host", "third_user", "third_password")
Resources
https://refactoring.guru/design-patterns/factory-method
https://github.com/wesdoyle/design-patterns-explained-with-food
Creational Extended
Let's break down how creational design patterns can streamline aspects of a restaurant business:
Core Concept: Creational design patterns give you more structured and flexible ways to create objects (in this case, elements of your restaurant operation). They take the nitty-gritty details of object creation and place them behind a cleaner process, which has several advantages.
Specific Patterns and Restaurant Analogies
Factory Method: Think of this as your "recipe book." Instead of directly creating dishes, you have recipes (your factory methods). These recipes outline how to create "subclasses" within a dish family.
- Example: Main "Pasta Recipe" could have subclasses for Spaghetti, Fettuccine, etc. This lets you easily change pasta types without altering the whole dish-making process.
Abstract Factory: Like having multiple specialized kitchens. You don't make everything in one go, you have different stations optimized for specific outputs.
- Example: "Breakfast Factory" (eggs, pancakes, etc.), "Lunch Factory" (sandwiches, soups), "Dessert Factory" (cakes, ice cream).
Builder: For when an item has lots of possible variations. Like a build-your-own-burger experience. The builder pattern separates construction into steps, letting you customize as needed.
- Example: Base burger, then steps for bun type, toppings, sauces, etc.
Prototype: Think of it as using a "master template." You have a pre-made object, which you can clone and tweak.
- Example: Base menu template. Clone and adjust for daily specials, seasonal changes, etc.
Singleton: Ensures you only have one instance of something important.
- Example: A single inventory system. You don't want conflicting stock information across different parts of your restaurant.
Why Are These Useful?
- Flexibility: Easily add new dishes or menu types without rewriting your entire ordering and kitchen system.
- Organization: Group related objects and processes together (like with the Abstract Factory).
- Control: You control how objects are produced, not just the output, offering fine-grained customization.
Let me know if you'd like a specific pattern explored in more detail, or a real-world scenario of how you might implement one of these in your restaurant!