Python Design Patterns [ Factory Method, Builder, Decorator, Adapter, Iterator, Observer, Singleton, Dependency Injection ]

Python Design Patterns [ Factory Method, Builder, Decorator, Adapter, Iterator, Observer, Singleton, Dependency Injection ]

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:

  • Reusable Code: Instead of reinventing the wheel every time, design patterns provide proven approaches that can be adapted and reused across different projects. This saves development time and effort.
  • Maintainable Code: Well-designed patterns often lead to cleaner and more organized code. This makes it easier for other developers to understand, modify, and maintain the codebase in the future.
  • Efficient Communication: Design patterns establish a common language among developers. By using terms like "Singleton" or "Observer," programmers can quickly convey complex design ideas without needing lengthy explanations.
  • Proven Solutions: Design patterns represent best practices developed and refined by experienced programmers over time. They help avoid common pitfalls and ensure a more robust and reliable codebase.


Here's a breakdown of some common categories and popular choices within them:

Creational Patterns:

  • Factory Method: Provides an interface for creating objects, letting subclasses decide the type of object to create. This is useful for ensuring flexibility when creating complex objects.

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!        

  • Builder: Separates object construction from its representation, allowing for building complex objects step by step. This is helpful when you have many optional parameters for object creation.

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:

  • Decorator: Adds new functionality to an object dynamically without modifying its subclass. This is a very Pythonic pattern for extending functionality without inheritance.

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        

  • Adapter: Allows incompatible interfaces to work together. This is useful for integrating with third-party libraries or legacy code.

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:

  • Iterator: Provides a way to access elements of a collection without exposing its underlying implementation. Iterators are built-in to Python and a fundamental concept for working with collections.

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        

  • Observer: Defines a one-to-many relationship between objects, where one object (subject) notifies many other objects (observers) about changes. This is useful for implementing event-driven systems.

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:

  • Singleton: While a popular pattern in other languages, use Singletons with caution in Python. It can introduce tight coupling and make testing harder.

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        

  • Dependency Injection: This is a more general principle than a specific pattern, but it's a recommended approach for making your code more modular and easier to test. It involves providing objects with their dependencies rather than having them create them themselves.

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.

要查看或添加评论,请登录

Ariful Islam的更多文章

社区洞察

其他会员也浏览了