
Welcome to the final blog in our series on basic design patterns! Over the past six posts, we’ve explored Factory, Singleton, Repository, Builder, Dependency Injection, and Observer patterns. These fundamental patterns help solve common software design challenges, making your code more modular, maintainable, and efficient.
In this post, we’ll summarize each pattern, compare their purposes, and guide you on when to apply them in different scenarios. Whether you’re a beginner or brushing up your skills, this summary will help you distinguish between these patterns and choose the right one for your project. Let’s get started!
What Are Design Patterns?
Design patterns are reusable solutions to recurring problems in software design. Think of them as blueprints that you can adapt to your specific needs. They don’t dictate exact code but provide a structure to make your applications more flexible and easier to scale. With that in mind, let’s recap the six patterns we’ve covered.
Summaries of the Six Design Patterns
1. Factory Pattern
Definition: A creational pattern that allows creating objects without specifying their exact class. It uses a factory method to decide which object to instantiate based on input.
Example: Ordering a pizza. You tell the pizza shop what type (cheese or pepperoni), and it makes the right one without you needing to know the recipe.
Code:
class Pizza:
def __init__(self, name):
self.name = name
class CheesePizza(Pizza):
def __init__(self):
super().__init__("Cheese Pizza")
class PepperoniPizza(Pizza):
def __init__(self):
super().__init__("Pepperoni Pizza")
class PizzaFactory:
def create_pizza(self, pizza_type):
if pizza_type == "cheese":
return CheesePizza()
elif pizza_type == "pepperoni":
return PepperoniPizza()
else:
raise ValueError("Unknown pizza type")
# Usage
factory = PizzaFactory()
pizza = factory.create_pizza("cheese")
print(pizza.name) # Output: Cheese Pizza
2. Singleton Pattern
Definition: A creational pattern that ensures a class has only one instance and provides a global point of access to it.
Example: A single TV remote in your house. No matter who uses it, it’s the same remote controlling the TV.
Code:
class Logger:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def log(self, message):
print(f"Log: {message}")
# Usage
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2) # Output: True
logger1.log("Hello, world!") # Output: Log: Hello, world!
3. Repository Pattern
Definition: A structural pattern that abstracts data access logic, providing a collection-like interface to interact with data sources.
Example: A library catalog. You ask for a book by title, and the librarian fetches it without you knowing where it’s stored.
Code:
class User:
def __init__(self, id, name):
self.id = id
self.name = name
class UserRepository:
def __init__(self):
self.users = [] # Simulating a database
def add_user(self, user):
self.users.append(user)
def get_user_by_id(self, user_id):
for user in self.users:
if user.id == user_id:
return user
return None
# Usage
repo = UserRepository()
user = User(1, "Alice")
repo.add_user(user)
found_user = repo.get_user_by_id(1)
print(found_user.name) # Output: Alice
4. Builder Pattern
Definition: A creational pattern that constructs complex objects step by step, separating the construction from the object’s representation.
Example: Building a custom sandwich. You choose bread, add toppings, and specify extras like sauce, one step at a time.
Code:
class House:
def __init__(self):
self.walls = 0
self.roof = None
self.garage = False
def __str__(self):
return f"House with {self.walls} walls, {self.roof} roof, {'and a garage' if self.garage else 'no garage'}"
class HouseBuilder:
def __init__(self):
self.house = House()
def add_walls(self, count):
self.house.walls = count
return self
def add_roof(self, type):
self.house.roof = type
return self
def add_garage(self):
self.house.garage = True
return self
def build(self):
return self.house
# Usage
builder = HouseBuilder()
house = builder.add_walls(4).add_roof("gable").add_garage().build()
print(house) # Output: House with 4 walls, gable roof, and a garage
5. Dependency Injection
Definition: A technique where dependencies are provided to a class externally rather than created inside it, promoting loose coupling.
Example: A coffee machine. You insert a specific pod (espresso or latte), and it uses that to make your drink without brewing it itself.
Code:
class MessageSender:
def send(self, message):
pass
class EmailSender(MessageSender):
def send(self, message):
print(f"Sending email: {message}")
class SMSSender(MessageSender):
def send(self, message):
print(f"Sending SMS: {message}")
class NotificationService:
def __init__(self, sender: MessageSender):
self.sender = sender
def notify(self, message):
self.sender.send(message)
# Usage
email_sender = EmailSender()
notification = NotificationService(email_sender)
notification.notify("Hello via email!") # Output: Sending email: Hello via email!
6. Observer Pattern
Definition: A behavioral pattern that allows multiple objects (observers) to be notified when another object’s state changes.
Example: A weather app. When the temperature updates, your phone and smartwatch both display the new value automatically.
Code:
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, temperature):
pass
class WeatherStation:
def __init__(self):
self._observers = []
self._temperature = 0
def register_observer(self, observer):
self._observers.append(observer)
def notify_observers(self):
for observer in self._observers:
observer.update(self._temperature)
def set_temperature(self, temperature):
self._temperature = temperature
self.notify_observers()
class PhoneDisplay(Observer):
def update(self, temperature):
print(f"Phone: Temperature updated to {temperature}°C")
# Usage
station = WeatherStation()
phone = PhoneDisplay()
station.register_observer(phone)
station.set_temperature(25.0) # Output: Phone: Temperature updated to 25.0°C
These examples and code snippets illustrate how each pattern solves a specific problem, making your software more flexible and maintainable. Experiment with them to see how they fit your needs!
Comparing the Patterns: How They Differ
These patterns serve distinct purposes, often grouped into categories:
- Creational Patterns (Factory, Singleton, Builder):
- Focus on object creation.
- Factory: Creates objects dynamically without hardcoding classes.
- Singleton: Limits instantiation to one object.
- Builder: Builds complex objects incrementally.
- Structural Patterns (Repository):
- Focus on object relationships.
- Repository: Organizes data access, separating it from business logic.
- Behavioral Patterns (Observer):
- Focus on object interactions.
- Observer: Manages communication between a subject and its observers.
- Dependency Management (Dependency Injection):
- A technique that enhances flexibility across patterns by injecting dependencies.
Key Distinctions
Pattern | Category | Main Purpose | Differentiator |
---|---|---|---|
Factory | Creational | Flexible object creation | Hides class details from client |
Singleton | Creational | Single instance management | Ensures one object globally |
Builder | Creational | Complex object construction | Step-by-step building process |
Repository | Structural | Data access abstraction | Decouples data logic from app |
Dependency Injection | Technique | Dependency management | External dependency provision |
Observer | Behavioral | State change notifications | One-to-many updates |
Complementary Uses
Patterns often work together:
- A Factory might create a Singleton.
- Dependency Injection can provide a Repository to a service.
- Observer can notify objects created by a Factory.
When to Use Which Pattern: Scenarios
Here’s how to decide which pattern fits your scenario:
- Scenario: You need to generate different report types (e.g., PDF, HTML) based on user selection.
- Pattern: Factory Pattern
- Why: It handles dynamic object creation cleanly.
- Scenario: You need one configuration manager shared across your app.
- Pattern: Singleton Pattern
- Why: Guarantees a single instance for consistent access.
- Scenario: You want to isolate your business logic from database-specific code.
- Pattern: Repository Pattern
- Why: Abstracts data access for flexibility and testing.
- Scenario: You’re creating a customizable object, like a pizza with optional toppings.
- Pattern: Builder Pattern
- Why: Simplifies step-by-step construction.
- Scenario: You need to test a class with a mock database connection.
- Pattern: Dependency Injection
- Why: Lets you inject the mock easily.
- Scenario: Multiple UI widgets must update when a data source changes.
- Pattern: Observer Pattern
- Why: Automatically notifies all dependents.
Tips for Choosing
- Creating objects? Use Factory, Singleton, or Builder based on complexity and instance needs.
- Handling data? Repository keeps it clean.
- Managing dependencies? Dependency Injection is your go-to.
- Updating multiple objects? Observer shines here.
- Avoid overuse: Don’t force a pattern—e.g., Singleton can create global state issues if misapplied.
Conclusion
Design patterns are powerful tools for tackling software challenges. In this series, we’ve covered:
- Factory for dynamic creation,
- Singleton for single instances,
- Repository for data abstraction,
- Builder for complex objects,
- Dependency Injection for flexibility,
- Observer for event-driven updates.
Understanding their differences and use cases empowers you to design better systems. Experiment with them in your projects, and use them wisely—not every problem needs a pattern! Thanks for following along—happy coding!
Leave a Reply