Python Design Patterns [ Factory Method, Builder, Decorator, Adapter, Iterator, Observer, Singleton, Dependency Injection ]
Ariful Islam
Solution Developer @ Shadhin Lab LLC | Backend Developer (Spring Boot) | AI & Microservices Enthusiast
Design patterns are like pre-built solutions for common problems encountered in software development, specifically in object-oriented programming (OOP). Here's why they're important:
Benefits of Design Patterns:
Here's a breakdown of some common categories and popular choices within them:
Creational Patterns:
Example of Factory Pattern:
First, we'll create an abstract class Pet with a method speak(). This method will be implemented by each concrete pet class.
from abc import ABC, abstractmethod
class Pet(ABC):
@abstractmethod
def speak(self):
pass
Next, we'll create concrete pet classes Dog and Cat that inherit from Pet and implement the speak() method.
class Dog(Pet):
def speak(self):
return "Woof!"
class Cat(Pet):
def speak(self):
return "Meow!"
Then, we'll create a PetFactory class with a method get_pet(). This method will return an instance of the appropriate pet class based on the input.
class PetFactory:
def get_pet(self, pet_type):
if pet_type == "Dog":
return Dog()
elif pet_type == "Cat":
return Cat()
else:
raise ValueError("Invalid pet type")
Finally, we can use the PetFactory to create pet instances.
factory = PetFactory()
pet = factory.get_pet("Dog")
print(pet.speak()) # Outputs: Woof!
pet = factory.get_pet("Cat")
print(pet.speak()) # Outputs: Meow!
Example of Builder design pattern:
First, we'll create an abstract Builder class with methods to build each part of the meal.
from abc import ABC, abstractmethod
class Builder(ABC):
@abstractmethod
def prepare_main(self):
pass
@abstractmethod
def add_sides(self):
pass
@abstractmethod
def add_drink(self):
pass
@abstractmethod
def pack(self):
pass
Next, we'll create concrete builder classes BurgerMealBuilder and PizzaMealBuilder that implement these methods.
class BurgerMealBuilder(Builder):
def prepare_main(self):
return "Burger"
def add_sides(self):
return "Fries"
def add_drink(self):
return "Coke"
def pack(self):
return "Packed in paper bag"
class PizzaMealBuilder(Builder):
def prepare_main(self):
return "Pizza"
def add_sides(self):
return "Garlic Bread"
def add_drink(self):
return "Pepsi"
def pack(self):
return "Packed in pizza box"
Next, we'll create concrete builder classes BurgerMealBuilder and PizzaMealBuilder that implement these methods.
class BurgerMealBuilder(Builder):
def prepare_main(self):
return "Burger"
def add_sides(self):
return "Fries"
def add_drink(self):
return "Coke"
def pack(self):
return "Packed in paper bag"
class PizzaMealBuilder(Builder):
def prepare_main(self):
return "Pizza"
def add_sides(self):
return "Garlic Bread"
def add_drink(self):
return "Pepsi"
def pack(self):
return "Packed in pizza box"
Then, we'll create a Director class that takes a Builder instance and uses it to create a meal.
class Director:
def __init__(self, builder):
self.builder = builder
def make_meal(self):
return {
"Main": self.builder.prepare_main(),
"Sides": self.builder.add_sides(),
"Drink": self.builder.add_drink(),
"Packaging": self.builder.pack(),
}
Finally, we can use the Director to create meals.
director = Director(BurgerMealBuilder())
meal = director.make_meal()
print(meal) # Outputs: {'Main': 'Burger', 'Sides': 'Fries', 'Drink': 'Coke', 'Packaging': 'Packed in paper bag'}
director = Director(PizzaMealBuilder())
meal = director.make_meal()
print(meal) # Outputs: {'Main': 'Pizza', 'Sides': 'Garlic Bread', 'Drink': 'Pepsi', 'Packaging': 'Packed in pizza box'}
Structural Patterns:
Example of decorator design pattern:
First, we'll create an abstract Coffee class with a method cost().
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self):
pass
Next, we'll create a BasicCoffee class that implements the cost() method.
class BasicCoffee(Coffee):
def cost(self):
return 1.00 # Basic coffee costs $1.00
Then, we'll create a CoffeeDecorator class that also implements the Coffee interface. This class will be the base class for all concrete decorators.
class CoffeeDecorator(Coffee):
def __init__(self, coffee):
self.coffee = coffee
def cost(self):
return self.coffee.cost()
Next, we'll create concrete decorator classes MilkDecorator, SugarDecorator, and CreamDecorator that add to the cost of the coffee.
class MilkDecorator(CoffeeDecorator):
def cost(self):
return self.coffee.cost() + 0.50 # Adding milk costs $0.50
class SugarDecorator(CoffeeDecorator):
def cost(self):
return self.coffee.cost() + 0.20 # Adding sugar costs $0.20
class CreamDecorator(CoffeeDecorator):
def cost(self):
return self.coffee.cost() + 0.75 # Adding cream costs $0.75
Finally, we can create a coffee and add ingredients to it.
coffee = BasicCoffee()
print(coffee.cost()) # Outputs: 1.00
coffee = MilkDecorator(coffee)
print(coffee.cost()) # Outputs: 1.50
coffee = SugarDecorator(coffee)
print(coffee.cost()) # Outputs: 1.70
coffee = CreamDecorator(coffee)
print(coffee.cost()) # Outputs: 2.45
Example of Adapter design pattern:
First, we'll create a Target class, which is the interface that our Client class understands.
class Target:
def request(self):
return "Target: The default target's behavior."
Next, we'll create an Adaptee class, which has a different interface.
领英推荐
class Adaptee:
def specific_request(self):
return ".eetpadA eht fo roivaheb laicepS"
Then, we'll create an Adapter class that can match the Target interface with the Adaptee interface.
class Adapter(Target, Adaptee):
def request(self):
return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"
Finally, we can use the Adapter to make the Adaptee work with the Client that understands the Target interface.
def client_code(target: "Target"):
print(target.request(), end="")
target = Target()
client_code(target)
adaptee = Adaptee()
print(f"\nAdaptee: {adaptee.specific_request()}")
adapter = Adapter()
client_code(adapter)
Behavioral Patterns:
Example of Iterator design pattern:
First, we'll create an Iterator abstract class with methods iter() and next().
from abc import ABC, abstractmethod
class Iterator(ABC):
@abstractmethod
def __iter__(self):
pass
@abstractmethod
def __next__(self):
pass
Next, we'll create a Playlist class that implements the Iterator interface. This class will hold the songs and provide a way to iterate over them.
class Playlist(Iterator):
def __init__(self):
self.songs = []
self.index = 0
def add_song(self, song):
self.songs.append(song)
def __iter__(self):
return self
def __next__(self):
try:
song = self.songs[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return song
Finally, we can create a playlist and iterate over the songs.
playlist = Playlist()
playlist.add_song("Song 1")
playlist.add_song("Song 2")
playlist.add_song("Song 3")
for song in playlist:
print(song) # Outputs: Song 1, Song 2, Song 3
Example of Observer design pattern:
First, we'll create an abstract Observer class and Subject class.
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, subject):
pass
class Subject(ABC):
@abstractmethod
def attach(self, observer: Observer):
pass
@abstractmethod
def detach(self, observer: Observer):
pass
@abstractmethod
def notify(self):
pass
Next, we'll create a WeatherStation class that implements the Subject interface. This class will hold the weather data and notify the observers whenever the data changes.
class WeatherStation(Subject):
def __init__(self):
self._observers = []
self._temperature = 0
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self):
for observer in self._observers:
observer.update(self)
def set_temperature(self, value):
self._temperature = value
self.notify()
def get_temperature(self):
return self._temperature
Then, we'll create DisplayUnit classes that implement the Observer interface. These classes will update their display whenever they receive a notification from the WeatherStation.
class DisplayUnitA(Observer):
def update(self, subject: WeatherStation):
print(f"Display Unit A: {subject.get_temperature()}")
class DisplayUnitB(Observer):
def update(self, subject: WeatherStation):
print(f"Display Unit B: {subject.get_temperature()}")
Finally, we can create a weather station, attach display units to it, and update the weather data.
weather_station = WeatherStation()
display_unit_a = DisplayUnitA()
display_unit_b = DisplayUnitB()
weather_station.attach(display_unit_a)
weather_station.attach(display_unit_b)
weather_station.set_temperature(25) # Outputs: Display Unit A: 25, Display Unit B: 25
Other Considerations:
Example of Singleton Design Pattern:
First, we'll create a Singleton class. This class uses a class variable _instance to keep track of whether an instance of the class has been created. If an instance has been created, it returns that instance. If not, it creates a new instance and returns it.
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
Now, we can create instances of the Singleton class.
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1 is singleton2) # Outputs: True
Example of dependency injection pattern:
First, we'll create a Car class. This class depends on an Engine, Tires, and Doors. Instead of creating these dependencies inside the Car class, they are passed in as parameters to the init method.
class Car:
def __init__(self, engine, tires, doors):
self.engine = engine
self.tires = tires
self.doors = doors
def start(self):
return self.engine.start()
def roll(self):
return self.tires.roll()
def open(self):
return self.doors.open()
Next, we'll create the Engine, Tires, and Doors classes. These are the dependencies of the Car class.
class Engine:
def start(self):
return "Engine started"
class Tires:
def roll(self):
return "Tires rolling"
class Doors:
def open(self):
return "Doors opened"
Finally, we can create the dependencies and inject them into the Car.
engine = Engine()
tires = Tires()
doors = Doors()
car = Car(engine, tires, doors)
print(car.start()) # Outputs: Engine started
print(car.roll()) # Outputs: Tires rolling
print(car.open()) # Outputs: Doors opened
Design patterns are reusable solutions to common problems that occur in software design. They represent the best practices used by experienced software developers. Here's a brief conclusion on the Python design patterns you mentioned:
Factory Method: This pattern provides a way to delegate the instantiation logic to child classes. It is used when a class cannot anticipate the type of objects it needs to create.
Builder: This pattern is used to construct a complex object step by step. It separates the construction of an object from its representation so that the same construction process can create different representations.
Decorator: This pattern allows a user to add new functionality to an existing object without altering its structure. It is a structural pattern that involves a set of decorator classes that are used to wrap concrete components.
Adapter: This pattern converts the interface of a class into another interface that a client expects. It allows classes to work together that couldn't otherwise because of incompatible interfaces.
Iterator: This pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Observer: This pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Singleton: This pattern ensures that a class has only one instance and provides a global point of access to it.
Dependency Injection: This pattern is a technique in which an object receives other objects that it depends on. It allows program designs to be loosely coupled and to follow the dependency inversion and single responsibility principles.
Each of these patterns has its own pros and cons, and they are used in different situations. Understanding these patterns and knowing when to apply them can make you a more effective Python programmer.