Design Patterns in Python: A Comprehensive Guide with Detailed Examples

Design Patterns in Python: A Comprehensive Guide with Detailed Examples

Design patterns are tried-and-tested solutions to common programming problems. They provide a reusable and efficient way to solve issues that developers face in software design. Python, being an object-oriented and flexible programming language, lends itself well to the implementation of design patterns. This article explores some of the most widely used design patterns in Python with detailed examples.

What Are Design Patterns?

Design patterns are best practices that help developers solve recurring problems in software design. While not code templates, they offer a structural guide that can be adapted to suit specific needs. Design patterns can be classified into three broad categories:

  1. Creational Patterns: Concerned with object creation mechanisms.
  2. Structural Patterns: Deal with object composition or the relationship between entities.
  3. Behavioral Patterns: Address the interaction and responsibility between objects.


1. Singleton Pattern

Category: Creational

Ensures a class has only one instance and provides a global point of access to that instance.

Use Case:

  • Database connections
  • Logger classes

Example:

In Python, the Singleton pattern can be implemented in multiple ways. Here’s a simple example using the __new__method.

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

# Usage
singleton1 = Singleton()
singleton2 = Singleton()

print(singleton1 is singleton2)  # True, both references point to the same instance        

In the example above, the __new__ method ensures that only one instance of the Singleton class is created. When multiple objects are instantiated, they point to the same instance.

2. Singleton Design Pattern — Loading Pre Trained AI Models

To load pre-trained AI models using the Hugging Face Transformers library with the Singleton Design Pattern, we can ensure that only one instance of a model is created and reused across the entire application. This is particularly useful when working with resource-heavy models (e.g., BERT, GPT-3), ensuring that the model is loaded once and reused efficiently.

Below is an implementation of the Singleton pattern to load pre-trained models using Hugging Face Transformers.

Step-by-Step Implementation

  1. Singleton Design Pattern: We will define a ModelLoader class that ensures only one instance of the pre-trained model is created.
  2. Hugging Face Transformers: The Singleton class will use the Hugging Face AutoModel and AutoTokenizer to load the pre-trained model.

Example: Implementing Singleton for Loading a BERT Model

from transformers import AutoTokenizer, AutoModel
import threading

class ModelLoader:
    _instance = None
    _lock = threading.Lock()  # Ensure thread safety for the Singleton
    def __new__(cls, model_name="bert-base-uncased"):
        with cls._lock:
            if cls._instance is None:
                print(f"Loading model: {model_name}")
                cls._instance = super(ModelLoader, cls).__new__(cls)
                cls._instance.model_name = model_name
                cls._instance.tokenizer = AutoTokenizer.from_pretrained(model_name)
                cls._instance.model = AutoModel.from_pretrained(model_name)
        return cls._instance
    def get_tokenizer(self):
        return self._instance.tokenizer
    def get_model(self):
        return self._instance.model
# Usage of the Singleton
def main():
    # First load
    loader1 = ModelLoader()
    tokenizer1 = loader1.get_tokenizer()
    model1 = loader1.get_model()
    # Attempt to create another instance
    loader2 = ModelLoader()
    tokenizer2 = loader2.get_tokenizer()
    model2 = loader2.get_model()
    # Check if both instances are the same (Singleton)
    print(loader1 is loader2)  # True
    print(model1 is model2)    # True
    print(tokenizer1 is tokenizer2)  # True
if __name__ == "__main__":
    main()        

Explanation:

  1. Thread-safe Singleton:

  • We use a threading lock (_lock) to ensure that the Singleton implementation is thread-safe. This prevents race conditions where two threads might attempt to create two instances of the class simultaneously.
  • __new__() ensures that only one instance of ModelLoader is created. If the _instance is None, it creates the object; otherwise, it returns the existing instance.

2. Model and Tokenizer Loading:

  • AutoTokenizer.from_pretrained(model_name) loads the pre-trained tokenizer.
  • AutoModel.from_pretrained(model_name) loads the pre-trained model.

3. Reusing the Singleton Instance:

  • When trying to create a new instance of ModelLoader, the same instance is returned.
  • The model and tokenizer are only loaded once, even if ModelLoader is called multiple times.

Output:

Loading model: bert-base-uncased
True
True
True        

This output confirms that the model and tokenizer are loaded only once, and subsequent calls reuse the same instance.

Why Use Singleton for Model Loading?

  1. Resource Efficiency: Loading large models like BERT or GPT can be computationally expensive and memory-intensive. Using the Singleton pattern ensures the model is loaded only once.
  2. Consistency: A single instance of the model ensures consistent behavior across the application.
  3. Thread Safety: By using locks, we ensure that the model loading process is safe even in multithreaded environments.

Extending to Other Hugging Face Models

The same pattern can be used to load any other Hugging Face model, such as GPT-2 or T5. You can simply pass the desired model name to the ModelLoaderclass.

Example: Loading GPT-2 with Singleton Pattern

class ModelLoader:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls, model_name="gpt2"):
        with cls._lock:
            if cls._instance is None:
                print(f"Loading model: {model_name}")
                cls._instance = super(ModelLoader, cls).__new__(cls)
                cls._instance.model_name = model_name
                cls._instance.tokenizer = AutoTokenizer.from_pretrained(model_name)
                cls._instance.model = AutoModel.from_pretrained(model_name)
        return cls._instance        

Simply replace "bert-base-uncased" with "gpt2" or any other pre-trained model name to load different models.

By implementing the Singleton Design Pattern to load pre-trained models using the Hugging Face Transformers library, you ensure that the model and tokenizer are loaded only once, saving computational resources.

This approach is ideal for applications where the same model is reused multiple times, such as in web services, real-time processing systems, or large-scale NLP pipelines.

3. Factory Pattern

Category: Creational

Defines an interface for creating objects but allows subclasses to alter the type of objects that will be created.

Use Case:

  • When the exact type of object isn’t known until runtime.

Example:

Here’s an implementation of the Factory Pattern for creating different types of animals.

from abc import ABC, abstractmethod

# Abstract Product
class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass
# Concrete Products
class Dog(Animal):
    def speak(self):
        return "Woof!"
class Cat(Animal):
    def speak(self):
        return "Meow!"
# Factory
class AnimalFactory:
    @staticmethod
    def get_animal(animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        else:
            return None
# Usage
factory = AnimalFactory()
dog = factory.get_animal("dog")
cat = factory.get_animal("cat")
print(dog.speak())  # Output: Woof!
print(cat.speak())  # Output: Meow!        

In this example, AnimalFactory decides which object to instantiate based on the input parameter. This pattern abstracts object creation and hides the details of object creation from the client.

3. Observer Pattern

Category: Behavioral

Defines a one-to-many relationship between objects such that when one object changes state, all its dependents are notified and updated automatically.

Use Case:

  • Implementing event-driven systems (like GUIs or stock price monitoring).

Example:

# Subject
class Subject:
    def __init__(self):
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

# Observer
class Observer:
    def update(self, subject):
        pass

# Concrete Observers
class ConcreteObserverA(Observer):
    def update(self, subject):
        print("Observer A: Reacted to the event")

class ConcreteObserverB(Observer):
    def update(self, subject):
        print("Observer B: Reacted to the event")

# Usage
subject = Subject()

observer_a = ConcreteObserverA()
observer_b = ConcreteObserverB()

subject.attach(observer_a)
subject.attach(observer_b)

subject.notify()        

In the Observer pattern, the Subject class manages a list of observers and notifies them when a change occurs. The observers (in this case ConcreteObserverA and ConcreteObserverB) receive updates and react to the changes.

4. Strategy Pattern

Category: Behavioral

Defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from clients that use it.

Use Case:

  • When you have multiple algorithms for a specific task and want to swap them easily.

Example:

# Base Component
class Coffee:
    def cost(self):
        return 5

# Decorator
class MilkDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 2

class SugarDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 1

# Usage
coffee = Coffee()
print(f"Basic coffee cost: {coffee.cost()}")  # Output: 5

# Add milk
coffee_with_milk = MilkDecorator(coffee)
print(f"Coffee with milk cost: {coffee_with_milk.cost()}")  # Output: 7

# Add sugar
coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)
print(f"Coffee with milk and sugar cost: {coffee_with_milk_and_sugar.cost()}")  # Output: 8        

In this example, the Context class uses a strategy object to perform a task. By changing the strategy at runtime, you can alter the behavior of the context without modifying its implementation.

5. Decorator Pattern

Category: Structural

Allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class.

Use Case:

  • Extending the functionality of objects without changing the class structure.

Example:

# Base Component
class Coffee:
    def cost(self):
        return 5

# Decorator
class MilkDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 2

class SugarDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 1

# Usage
coffee = Coffee()
print(f"Basic coffee cost: {coffee.cost()}")  # Output: 5

# Add milk
coffee_with_milk = MilkDecorator(coffee)
print(f"Coffee with milk cost: {coffee_with_milk.cost()}")  # Output: 7

# Add sugar
coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)
print(f"Coffee with milk and sugar cost: {coffee_with_milk_and_sugar.cost()}")  # Output: 8        

The Decorator pattern allows the dynamic addition of behavior. In this case, we are decorating a coffee object with additional features like milk and sugar without modifying the original Coffee class.

6. Command Pattern

Category: Behavioral

Encapsulates a request as an object, allowing for parameterization of clients with different requests, queuing of requests, and logging them.

Use Case:

  • When you need to issue requests to objects without knowing about the operation being requested or the receiver of the request.

Example:

# Command Interface
class Command:
    def execute(self):
        pass

# Concrete Commands
class TurnOnLightCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        self.light.turn_on()

class TurnOffLightCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        self.light.turn_off()

# Receiver
class Light:
    def turn_on(self):
        print("Light is On")

    def turn_off(self):
        print("Light is Off")

# Invoker
class RemoteControl:
    def __init__(self):
        self._commands = []

    def add_command(self, command):
        self._commands.append(command)

    def execute_commands(self):
        for command in self._commands:
            command.execute()

# Usage
light = Light()
turn_on = TurnOnLightCommand(light)
turn_off = TurnOffLightCommand(light)

remote = RemoteControl()
remote.add_command(turn_on)
remote.add_command(turn_off)

remote.execute_commands()        

The Command pattern encapsulates requests as objects, allowing for various operations (turning the light on or off) to be treated as commands that can be executed in sequence or stored for later execution.

7. Chain of Responsibility Pattern

Category: Behavioral

Allow multiple objects to handle a request by passing it along the chain until an object handles it. This decouples the sender of the request from its receivers.

Scenario: A Customer Support System

In a typical customer support system, different types of requests (like a complaint, technical support request, or billing inquiry) are handled by different departments. The Chain of Responsibility design pattern fits well here because each department will either process the request or pass it along the chain if it’s outside their scope.

Chain of Responsibility allows several handlers to process a request without explicitly linking the sender of the request to the receiver. It creates a chain where each handler decides whether to process the request or pass it on.

Example: Handling Customer Support Requests

In this example, we’ll have the following handlers:

  1. ComplaintHandler: Handles complaints.
  2. TechnicalSupportHandler: Handles technical support requests.
  3. BillingHandler: Handles billing inquiries.

Each handler will attempt to process a request. If it cannot, it passes the request to the next handler in the chain.

Step-by-Step Code Implementation:

1. Abstract Handler

This class will define a base structure for all handlers, with methods to handle requests and pass them along the chain.

2. Concrete Handlers

Each handler class will attempt to process the request if it matches its responsibility; otherwise, it will pass the request to the next handler

# Abstract Handle
class Handler:
    """Abstract handler class that defines the interface for handling requests."""
    def __init__(self):
        self._next_handler = None

    def set_next(self, handler):
        """Sets the next handler in the chain."""
        self._next_handler = handler
        return handler

    def handle(self, request):
        """Handles the request or passes it to the next handler."""
        if self._next_handler:
            return self._next_handler.handle(request)
        return None

# Concrete Handlers
class ComplaintHandler(Handler):
    """Handler for processing complaints."""
    def handle(self, request):
        if request["type"] == "complaint":
            return f"ComplaintHandler: Processing complaint -> {request['message']}"
        else:
            return super().handle(request)


class TechnicalSupportHandler(Handler):
    """Handler for processing technical support requests."""
    def handle(self, request):
        if request["type"] == "technical":
            return f"TechnicalSupportHandler: Handling technical support request -> {request['message']}"
        else:
            return super().handle(request)


class BillingHandler(Handler):
    """Handler for processing billing inquiries."""
    def handle(self, request):
        if request["type"] == "billing":
            return f"BillingHandler: Processing billing request -> {request['message']}"
        else:
            return super().handle(request)

# Client code to set up the chain
def main():
    # Create handlers
    complaint_handler = ComplaintHandler()
    technical_handler = TechnicalSupportHandler()
    billing_handler = BillingHandler()

    # Set up the chain of responsibility
    complaint_handler.set_next(technical_handler).set_next(billing_handler)

    # Create requests
    requests = [
        {"type": "complaint", "message": "The product arrived damaged."},
        {"type": "technical", "message": "The website is not loading."},
        {"type": "billing", "message": "I was overcharged on my last bill."},
        {"type": "other", "message": "I have a general question about your service."}
    ]

    # Process each request through the chain
    for req in requests:
        response = complaint_handler.handle(req)
        if response:
            print(response)
        else:
            print(f"No handler found for request type: {req['type']}")

if __name__ == "__main__":
    main()        

Explanation:

  1. Abstract Handler (Handler):

  • This is the base class with a handle() method that tries to handle a request or pass it along to the next handler.
  • The set_next() method sets up the chain of handlers.

2. Concrete Handlers:

  • ComplaintHandler: Handles complaints by checking if the request’s typeis "complaint". If it can’t handle the request, it passes it to the next handler.
  • TechnicalSupportHandler: Handles technical support requests.
  • BillingHandler: Handles billing-related requests.

3. Chain Setup: The ComplaintHandler is the first in the chain, followed by the TechnicalSupportHandler, and finally the BillingHandler.

4. Client Code:

  • Several requests are created with different type values ("complaint", "technical", "billing", and "other").
  • Each request is passed to the first handler in the chain, and it either gets processed or passed down the chain.

Output:

ComplaintHandler: Processing complaint -> The product arrived damaged.
TechnicalSupportHandler: Handling technical support request -> The website is not loading.
BillingHandler: Processing billing request -> I was overcharged on my last bill.
No handler found for request type: other        

Breakdown:

  1. ComplaintHandler successfully processes the "complaint" request.
  2. TechnicalSupportHandler handles the "technical" support request.
  3. BillingHandler handles the "billing" inquiry.
  4. When the request type is "other", none of the handlers in the chain can handle it, so a message saying "No handler found for request type: other" is printed.

Advantages of Using Chain of Responsibility Pattern:

  1. Decoupled Request Handling: Each handler is only concerned with processing the requests it knows about. Handlers are easily added, removed, or reordered without affecting others.
  2. Flexible and Extendable: If new request types (e.g., “Refund” requests) need to be handled, we can create new handlers and chain them as needed.
  3. Simplified Logic: Each handler has a specific responsibility, making the codebase easier to maintain.

In this example, the Chain of Responsibility pattern allows us to efficiently handle different types of customer service requests without hardcoding conditional logic. The pattern creates a clear and modular solution, making it easier to add new types of requests or modify the order of processing.

8. Flyweight Pattern

Category: Structural

The Flyweight Pattern is a structural design pattern that allows programs to support large numbers of objects efficiently by sharing as much data as possible between them. The pattern reduces memory usage when you have many similar objects, and the shared objects are called flyweights.

The Flyweight Pattern aims to:

  1. Minimize memory usage by sharing common parts of the state between objects.
  2. Manage the intrinsic (shared) and extrinsic (unique) state of objects efficiently.

Example: Flyweight Pattern for Character Objects in a Text Editor

Let’s imagine a text editor that needs to display thousands of characters. Instead of creating a new object for each character, which would take up a lot of memory, we can use the Flyweight Pattern to share common data like the font style and size between all characters, while each character will only store unique data like its position and the specific character it represents.

Step-by-Step Implementation

  1. Flyweight Class: This will represent the shared data (e.g., font, size).
  2. Flyweight Factory: The factory will ensure that we create a new flyweight object only when one doesn’t already exist for a given font.
  3. Client: The client will use the flyweight objects and supply the unique state (e.g., character position and value).

# Flyweight class representing shared intrinsic state (font, size)
class CharacterFlyweight:
    def __init__(self, font, size):
        self.font = font
        self.size = size

    def display(self, character, position):
        """Displays the character with its unique (extrinsic) state."""
        print(f"Character: {character} | Font: {self.font} | Size: {self.size} | Position: {position}")


# Flyweight Factory to manage shared flyweight objects
class CharacterFlyweightFactory:
    def __init__(self):
        self._flyweights = {}

    def get_flyweight(self, font, size):
        """Return a flyweight (shared object) if it exists, else create a new one."""
        key = (font, size)
        if key not in self._flyweights:
            print(f"Creating new flyweight for Font: {font}, Size: {size}")
            self._flyweights[key] = CharacterFlyweight(font, size)
        return self._flyweights[key]

    def get_flyweight_count(self):
        """Returns the number of shared flyweights."""
        return len(self._flyweights)


# Client class that uses the flyweights
class TextEditor:
    def __init__(self):
        self.factory = CharacterFlyweightFactory()
        self.characters = []

    def add_character(self, character, font, size, position):
        """Adds a character with its unique (extrinsic) state."""
        flyweight = self.factory.get_flyweight(font, size)
        self.characters.append((flyweight, character, position))

    def display_text(self):
        """Displays the text by invoking each flyweight's display method."""
        for flyweight, character, position in self.characters:
            flyweight.display(character, position)


# Client code to test Flyweight pattern
def main():
    editor = TextEditor()

    # Add characters with shared font and size, but different positions
    editor.add_character("H", "Arial", 12, (0, 0))
    editor.add_character("e", "Arial", 12, (1, 0))
    editor.add_character("l", "Arial", 12, (2, 0))
    editor.add_character("l", "Arial", 12, (3, 0))
    editor.add_character("o", "Arial", 12, (4, 0))

    # Add characters with a different font and size
    editor.add_character("W", "Times New Roman", 14, (0, 1))
    editor.add_character("o", "Times New Roman", 14, (1, 1))
    editor.add_character("r", "Times New Roman", 14, (2, 1))
    editor.add_character("l", "Times New Roman", 14, (3, 1))
    editor.add_character("d", "Times New Roman", 14, (4, 1))

    # Display all characters in the editor
    editor.display_text()

    # Display number of flyweight objects created
    print(f"Flyweights created: {editor.factory.get_flyweight_count()}")

if __name__ == "__main__":
    main()        

Explanation:

  1. Flyweight Class (CharacterFlyweight):

  • This class contains shared (intrinsic) data, which in our case is the font and size of the text characters.
  • The display() method uses both shared and unique (extrinsic) data like the specific character and its position to render it.

2. Flyweight Factory (CharacterFlyweightFactory):

  • This class manages the creation and retrieval of flyweight objects.
  • If a flyweight with the requested font and size already exists, it returns the existing object; otherwise, it creates a new one.

3. Client (TextEditor):

  • This class uses the flyweight factory to get shared objects and adds characters with both intrinsic (shared) and extrinsic (unique) data.
  • The display_text() method calls each flyweight’s display() method to render the characters along with their positions.

Output:

Creating new flyweight for Font: Arial, Size: 12
Creating new flyweight for Font: Times New Roman, Size: 14
Character: H | Font: Arial | Size: 12 | Position: (0, 0)
Character: e | Font: Arial | Size: 12 | Position: (1, 0)
Character: l | Font: Arial | Size: 12 | Position: (2, 0)
Character: l | Font: Arial | Size: 12 | Position: (3, 0)
Character: o | Font: Arial | Size: 12 | Position: (4, 0)
Character: W | Font: Times New Roman | Size: 14 | Position: (0, 1)
Character: o | Font: Times New Roman | Size: 14 | Position: (1, 1)
Character: r | Font: Times New Roman | Size: 14 | Position: (2, 1)
Character: l | Font: Times New Roman | Size: 14 | Position: (3, 1)
Character: d | Font: Times New Roman | Size: 14 | Position: (4, 1)
Flyweights created: 2        

Breakdown:

  1. Flyweight Sharing: Only two flyweight objects are created: one for "Arial" with size 12 and one for "Times New Roman" with size 14.

  • These flyweights are shared by multiple characters with the same font and size.
  • The memory usage is reduced because we avoid creating separate objects for each character’s font and size.

2. Extrinsic Data: Each character has unique data (position and the character itself), which is stored separately from the shared flyweight data.

3. Memory Optimization: Instead of creating a new object for every character in the text, we create flyweight objects for each distinct font and size, reusing them across characters with the same properties.

Advantages of Flyweight Pattern:

  1. Memory Efficiency: By sharing common data between multiple objects, memory usage is significantly reduced, especially when handling a large number of similar objects.
  2. Performance: By reducing the memory footprint, the system can perform faster when handling a large volume of objects.
  3. Flexibility: The pattern allows you to manage intrinsic (shared) and extrinsic (unique) states separately, giving you flexibility in how you manage object data.

The Flyweight Pattern helps in scenarios where many similar objects are needed, and memory usage becomes a concern. By sharing intrinsic data and separating out the extrinsic state, this pattern ensures that the system remains lightweight and efficient. In this example, we successfully applied the Flyweight Pattern in a text editor context, where character formatting (font, size) is shared among multiple characters, and each character maintains its unique position and value.

9. Builder Pattern

The Builder Pattern is a creational design pattern that provides a way to construct a complex object step by step. It separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

The Builder Pattern is used to:

  1. Construct a complex object step by step.
  2. Separate the construction process from the final representation of the object.
  3. Provide flexibility in how complex objects are created.

When to Use the Builder Pattern

  • When an object needs to be created with many optional components or configurations.
  • When the construction process involves multiple steps or needs to be controlled.

Components of the Builder Pattern

  1. Builder: An interface or abstract class that defines the steps for building the product.
  2. ConcreteBuilder: Implements the Builder interface and provides specific implementations of the building steps.
  3. Product: The complex object being built.
  4. Director: Responsible for managing the construction process, typically using a Builder instance.

Example: Building a Custom Computer

We’ll use the Builder Pattern to create a custom computer with various configurations, such as different types of processors, RAM, storage, and other components.

Step-by-Step Implementation

  1. Define the Product (Computer):

  • This is the complex object being built.

2. Create the Builder Interface:

  • Specifies methods for setting different parts of the product.

3. Implement Concrete Builders:

  • Provide specific implementations for building different types of computers.

4. Create the Director:

  • Manages the construction process using the builder.

5. Client Code:

  • Uses the director to construct the computer.

Code Implementation

1. Product

class Computer:
    def __init__(self):
        self.processor = None
        self.ram = None
        self.storage = None
        self.graphics_card = None

    def __str__(self):
        return (f"Computer with:\n"
                f"Processor: {self.processor}\n"
                f"RAM: {self.ram}\n"
                f"Storage: {self.storage}\n"
                f"Graphics Card: {self.graphics_card}\n")        

2. Builder Interface

class ComputerBuilder:
    def set_processor(self, processor):
        raise NotImplementedError

    def set_ram(self, ram):
        raise NotImplementedError

    def set_storage(self, storage):
        raise NotImplementedError

    def set_graphics_card(self, graphics_card):
        raise NotImplementedError

    def get_computer(self):
        raise NotImplementedError        

3. Concrete Builder

class GamingComputerBuilder(ComputerBuilder):
    def __init__(self):
        self.computer = Computer()

    def set_processor(self, processor):
        self.computer.processor = processor

    def set_ram(self, ram):
        self.computer.ram = ram

    def set_storage(self, storage):
        self.computer.storage = storage

    def set_graphics_card(self, graphics_card):
        self.computer.graphics_card = graphics_card

    def get_computer(self):
        return self.computer        
class OfficeComputerBuilder(ComputerBuilder):
    def __init__(self):
        self.computer = Computer()

    def set_processor(self, processor):
        self.computer.processor = processor

    def set_ram(self, ram):
        self.computer.ram = ram

    def set_storage(self, storage):
        self.computer.storage = storage

    def set_graphics_card(self, graphics_card):
        self.computer.graphics_card = graphics_card

    def get_computer(self):
        return self.computer        

4. Director

class ComputerDirector:
    def __init__(self, builder):
        self.builder = builder

    def build_gaming_computer(self):
        self.builder.set_processor("Intel i9")
        self.builder.set_ram("32GB")
        self.builder.set_storage("1TB SSD")
        self.builder.set_graphics_card("NVIDIA RTX 3080")

    def build_office_computer(self):
        self.builder.set_processor("Intel i5")
        self.builder.set_ram("16GB")
        self.builder.set_storage("512GB SSD")
        self.builder.set_graphics_card("Integrated Graphics")        

6. Client Code

def main():
    # Building a Gaming Computer
    gaming_builder = GamingComputerBuilder()
    director = ComputerDirector(gaming_builder)
    director.build_gaming_computer()
    gaming_computer = gaming_builder.get_computer()
    print("Gaming Computer Built:")
    print(gaming_computer)

    # Building an Office Computer
    office_builder = OfficeComputerBuilder()
    director = ComputerDirector(office_builder)
    director.build_office_computer()
    office_computer = office_builder.get_computer()
    print("Office Computer Built:")
    print(office_computer)

if __name__ == "__main__":
    main()        

Explanation:

  1. Product (Computer):

  • The complex object being constructed, with various attributes like processor, RAM, storage, and graphics card.

2. Builder Interface (ComputerBuilder):

  • Specifies methods for setting parts of the product and retrieving the final product.

3. Concrete Builders (GamingComputerBuilder, OfficeComputerBuilder):

  • Implement the Builder interface and define how to set different attributes of the computer.

4. Director (ComputerDirector):

  • Manages the construction process using a builder instance. It provides methods to build different types of computers by setting appropriate configurations.

5. Client Code:

  • Uses the director to build specific types of computers. The client interacts with the director to get the desired configuration without worrying about the internal details of the construction process.

Output:

Gaming Computer Built:
Computer with:
Processor: Intel i9
RAM: 32GB
Storage: 1TB SSD
Graphics Card: NVIDIA RTX 3080

Office Computer Built:
Computer with:
Processor: Intel i5
RAM: 16GB
Storage: 512GB SSD
Graphics Card: Integrated Graphics        

Advantages of Builder Pattern:

  1. Separation of Construction and Representation: The construction process is separated from the final representation of the product, allowing different representations to be created from the same construction process.
  2. Flexibility: Different builders can create different types of products using the same construction process.
  3. Immutability: The constructed object is immutable once created, reducing complexity and potential errors.

The Builder Pattern is ideal for creating complex objects with many optional components or configurations. It simplifies the construction process by separating it from the final representation of the object, making it easier to manage and extend. In this example, we built various types of computers using the Builder Pattern, demonstrating its flexibility and effectiveness in managing complex object creation.

10. Iterator Pattern

The Iterator Pattern is a behavioral design pattern that allows sequential access to elements in a collection without exposing the underlying representation of the collection. It provides a way to access the elements of an aggregate object one at a time without exposing its internal structure.

The Iterator Pattern aims to:

  1. Provide a standard way to access elements of a collection.
  2. Encapsulate the iteration logic and provide a way to traverse the collection.
  3. Allow iteration over different types of collections in a uniform way.

Components of the Iterator Pattern

  1. Iterator: Defines the interface for accessing and iterating through elements.
  2. ConcreteIterator: Implements the Iterator interface and keeps track of the current position.
  3. Aggregate: Defines the interface for creating an iterator.
  4. ConcreteAggregate: Implements the Aggregate interface and provides the iterator.

Example: Implementing an Iterator for a Custom Collection

Let’s create an example with a custom collection called BookCollection. This collection will store a list of books, and we’ll implement an iterator to traverse the collection.

1. Iterator Interface

The Iterator interface defines methods for accessing elements and checking if there are more elements.

from abc import ABC, abstractmethod

class Iterator(ABC):
    @abstractmethod
    def has_next(self):
        """Check if there are more elements to iterate."""
        pass

    @abstractmethod
    def next(self):
        """Return the next element in the collection."""
        pass        

2. Concrete Iterator

The BookIterator implements the Iterator interface and provides the actual iteration logic.

class BookIterator(Iterator):
    def __init__(self, books):
        self._books = books
        self._index = 0

    def has_next(self):
        """Check if there are more books to iterate."""
        return self._index < len(self._books)

    def next(self):
        """Return the next book in the collection."""
        if self.has_next():
            book = self._books[self._index]
            self._index += 1
            return book
        raise StopIteration("No more elements to iterate.")        

3. Aggregate Interface

The Aggregate interface defines a method for creating an iterator.

class Aggregate(ABC):
    @abstractmethod
    def create_iterator(self):
        """Create an iterator for the aggregate collection."""
        pass        

4. Concrete Aggregate

The BookCollection implements the Aggregate interface and provides the iterator for the collection.

class BookCollection(Aggregate):
    def __init__(self):
        self._books = []

    def add_book(self, book):
        """Add a book to the collection."""
        self._books.append(book)

    def create_iterator(self):
        """Create an iterator for the book collection."""
        return BookIterator(self._books)        

5. Client Code

The client code uses the BookCollection and BookIterator to access and iterate over the collection of books.

def main():
    # Create a book collection and add books
    collection = BookCollection()
    collection.add_book("The Great Gatsby")
    collection.add_book("To Kill a Mockingbird")
    collection.add_book("1984")
    collection.add_book("Pride and Prejudice")

    # Create an iterator for the book collection
    iterator = collection.create_iterator()

    # Iterate over the collection and print the books
    while iterator.has_next():
        book = iterator.next()
        print(f"Book: {book}")

if __name__ == "__main__":
    main()        

Explanation:

  1. Iterator Interface (Iterator):

  • Defines methods has_next() to check if there are more elements and next() to return the next element.

2. Concrete Iterator (BookIterator):

  • Implements the iteration logic, tracking the current position in the collection and providing the next element.

3. Aggregate Interface (Aggregate):

  • Defines a method create_iterator() for creating an iterator.

4. Concrete Aggregate (BookCollection):

  • Implements the aggregate interface and provides an iterator for its collection of books.

5. Client Code:

  • Uses the BookCollection and BookIterator to add books to the collection and iterate over them, demonstrating how the iterator pattern provides a standard way to access elements.

Advantages of the Iterator Pattern:

  1. Encapsulation: The pattern encapsulates the iteration logic and allows access to elements without exposing the internal structure of the collection.
  2. Uniform Access: Provides a uniform way to access elements across different types of collections.
  3. Separation of Concerns: Separates the collection’s iteration logic from its representation, making it easier to manage and extend.

The Iterator Pattern is a powerful tool for managing and traversing collections of objects. By providing a standard interface for iteration, it ensures that clients can access elements in a consistent manner without needing to know the underlying details of the collection’s implementation. In this example, we used the pattern to create a custom book collection and iterator, showcasing how to effectively manage and traverse complex data structures.

11. Composite Pattern

The Composite Pattern is a structural design pattern used to allow clients to treat individual objects and compositions of objects uniformly. It is particularly useful when dealing with tree-like structures, where objects are organized into hierarchies.

The Composite Pattern aims to:

  1. Treat individual objects and compositions of objects uniformly.
  2. Allow clients to interact with both single objects and groups of objects in a similar way.
  3. Simplify client code by using a common interface for all objects.

Components of the Composite Pattern

  1. Component: Defines the common interface for all objects (both leaf and composite).
  2. Leaf: Represents end objects in the composition that do not have sub-objects.
  3. Composite: Represents nodes in the composition that can have children (both leaf and composite).

Example: File System Hierarchy

We’ll use the Composite Pattern to model a file system where both files and directories can be treated uniformly. In this example:

  • Files are leaf objects.
  • Directories are composite objects that can contain both files and other directories.

1. Component Interface

The FileSystemComponent defines common operations for files and directories.

from abc import ABC, abstractmethod

class FileSystemComponent(ABC):
    @abstractmethod
    def get_name(self):
        """Return the name of the component."""
        pass

    @abstractmethod
    def print_structure(self, indent=0):
        """Print the structure of the component."""
        pass        

2. Leaf

The File class represents a file, which is a leaf in the file system hierarchy.

class File(FileSystemComponent):
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def print_structure(self, indent=0):
        print(' ' * indent + self.get_name())        

3. Composite

The Directory class represents a directory, which can contain other files and directories.

class Directory(FileSystemComponent):
    def __init__(self, name):
        self._name = name
        self._children = []

    def add(self, component):
        self._children.append(component)

    def remove(self, component):
        self._children.remove(component)

    def get_name(self):
        return self._name

    def print_structure(self, indent=0):
        print(' ' * indent + self.get_name())
        for child in self._children:
            child.print_structure(indent + 2)        

4. Client Code

The client code demonstrates how to create a file system hierarchy and print its structure.

def main():
    # Create files
    file1 = File("file1.txt")
    file2 = File("file2.txt")
    file3 = File("file3.txt")

    # Create directories
    directory1 = Directory("Directory1")
    directory2 = Directory("Directory2")

    # Add files to directories
    directory1.add(file1)
    directory1.add(file2)

    directory2.add(file3)

    # Create root directory and add subdirectories
    root_directory = Directory("Root")
    root_directory.add(directory1)
    root_directory.add(directory2)

    # Print the file system structure
    root_directory.print_structure()

if __name__ == "__main__":
    main()        

Explanation:

  1. Component Interface (FileSystemComponent):

  • Defines common methods get_name() and print_structure() for both files and directories.

2. Leaf (File):

  • Represents a file in the file system. Implements the get_name() and print_structure() methods.

3. Composite (Directory):

  • Represents a directory that can contain other files or directories. Manages a list of child components and implements the print_structure() method to display its structure.

4. Client Code:

  • Creates a hierarchy of files and directories.
  • Uses the print_structure() method to display the hierarchical structure of the file system.

Output:

Root
  Directory1
    file1.txt
    file2.txt
  Directory2
    file3.txt        

Advantages of the Composite Pattern:

  1. Uniformity: Clients can treat individual objects and compositions of objects uniformly, simplifying code and reducing the need for special-case handling.
  2. Flexibility: New types of components can be added to the system with minimal changes to existing code.
  3. Ease of Use: Simplifies the client code by allowing it to use a common interface for all components.

The Composite Pattern is a powerful design pattern for managing hierarchical collections of objects. It enables you to treat individual objects and compositions of objects in a uniform way, simplifying code and making it easier to work with complex structures. In this example, we modeled a file system hierarchy, demonstrating how both files and directories can be managed and displayed using the Composite Pattern.

12. Proxy Pattern

The Proxy Pattern is a structural design pattern that provides an object representing another object. The proxy acts as an intermediary or placeholder for the real object and can control access to it, manage its lifecycle, or add additional functionality.

The Proxy Pattern aims to:

  1. Control access to the real object, potentially adding security or access control.
  2. Add additional functionality to the real object without modifying it.
  3. Manage the lifecycle of the real object, such as creating it on-demand or handling resource management.

Components of the Proxy Pattern

  1. Subject: Defines the common interface for both the RealObject and Proxy.
  2. RealObject: The actual object that the proxy represents.
  3. Proxy: Maintains a reference to the RealObject and controls access to it.

Example: Virtual Proxy for Loading Images

Let’s use the Proxy Pattern to manage the loading of large images. The proxy will delay the loading of the image until it’s actually needed (virtual proxy).

1. Subject Interface

The Image interface defines common operations for both the real image and the proxy.

from abc import ABC, abstractmethod

class Image(ABC):
    @abstractmethod
    def display(self):
        """Display the image."""
        pass        

2. RealObject

The RealImage class represents the actual image, which is loaded and displayed.

class RealImage(Image):
    def __init__(self, filename):
        self._filename = filename
        self._load_image()

    def _load_image(self):
        """Simulate loading a large image."""
        print(f"Loading image: {self._filename}")

    def display(self):
        """Display the image."""
        print(f"Displaying image: {self._filename}")        

3. Proxy

The ProxyImage class acts as an intermediary for RealImage. It controls access and can delay the creation of the RealImage until it's needed.

class ProxyImage(Image):
    def __init__(self, filename):
        self._filename = filename
        self._real_image = None

    def display(self):
        """Control access to the real image."""
        if self._real_image is None:
            self._real_image = RealImage(self._filename)
        self._real_image.display()        

4. Client Code

The client code uses the ProxyImage to access and display images.

def main():
    # Create a proxy image
    proxy_image = ProxyImage("large_image.jpg")

    # Display the image
    # The image will be loaded and displayed when needed
    proxy_image.display()

    # Display the image again
    # The image will not be reloaded
    proxy_image.display()

if __name__ == "__main__":
    main()        

Explanation:

  1. Subject Interface (Image):

  • Defines the common interface for both RealImage and ProxyImage.

2. RealObject (RealImage):

  • Represents the actual image. It loads and displays the image, and simulates a time-consuming operation with the _load_image() method.

4. Proxy (ProxyImage):

  • Acts as an intermediary for RealImage. It delays the creation of the real image until it is actually needed, managing access and potentially improving performance by avoiding unnecessary operations.

5. Client Code:

  • Uses the ProxyImage to display the image. The proxy handles the creation and access to the real image, ensuring that the image is loaded only when it is first accessed.

Output:

Loading image: large_image.jpg
Displaying image: large_image.jpg
Displaying image: large_image.jpg        

Advantages of the Proxy Pattern:

  1. Control Access: The proxy can control access to the real object, adding security or managing access permissions.
  2. Lazy Initialization: Delays the creation or loading of an expensive object until it’s actually needed (virtual proxy).
  3. Additional Functionality: Can add extra features or functionality, such as logging or caching, without modifying the real object.

The Proxy Pattern is a versatile design pattern that provides a way to control access to an object and manage its lifecycle. By using a proxy, you can add additional functionality, such as lazy loading or access control, while keeping the real object unchanged. In this example, we demonstrated a virtual proxy for loading and displaying images, showcasing how proxies can be used to manage expensive operations efficiently.

14. Facade Pattern

The Facade Pattern is a structural design pattern that provides a simplified interface to a complex subsystem. It is used to provide a unified and simplified interface to a set of interfaces in a subsystem, making it easier to use the subsystem without dealing with its complexity.

The Facade Pattern aims to:

  1. Simplify interaction with a complex subsystem by providing a unified interface.
  2. Hide the complexities of the subsystem and provide a more understandable interface for clients.
  3. Decouple the client code from the subsystem, reducing dependencies and improving maintainability.

Components of the Facade Pattern

  1. Facade: Provides a simplified interface to the subsystem. It delegates requests to the appropriate components in the subsystem.
  2. Subsystem Classes: Implement the complex logic or functionality that the facade simplifies.

Example: Home Theater System

We’ll use the Facade Pattern to simplify interaction with a home theater system. The system includes various components such as a DVD player, projector, amplifier, and lights. The facade will provide a simple interface for turning on and off the home theater system, without needing to interact with each component directly.

1. Subsystem Classes

These classes represent the various components of the home theater system.

class DVDPlayer:
    def on(self):
        print("DVD Player is now ON.")

    def off(self):
        print("DVD Player is now OFF.")

    def play(self, movie):
        print(f"DVD Player is playing: {movie}")

    def stop(self):
        print("DVD Player stopped.")        
class Projector:
    def on(self):
        print("Projector is now ON.")

    def off(self):
        print("Projector is now OFF.")

    def set_input(self, input):
        print(f"Projector input set to: {input}")

    def wide_screen_mode(self):
        print("Projector in wide screen mode.")        

2. Facade

The HomeTheaterFacade class provides a simplified interface to the complex subsystem.

class HomeTheaterFacade:
    def __init__(self, dvd_player, projector, amplifier, lights):
        self._dvd_player = dvd_player
        self._projector = projector
        self._amplifier = amplifier
        self._lights = lights

    def watch_movie(self, movie):
        print("Get ready to watch a movie...")
        self._lights.dim(10)
        self._projector.on()
        self._projector.set_input("DVD")
        self._projector.wide_screen_mode()
        self._amplifier.on()
        self._amplifier.set_volume(5)
        self._amplifier.set_audio_mode("Surround")
        self._dvd_player.on()
        self._dvd_player.play(movie)

    def end_movie(self):
        print("Shutting down the home theater...")
        self._dvd_player.stop()
        self._dvd_player.off()
        self._projector.off()
        self._amplifier.off()
        self._lights.dim(100)        

3. Client Code

The client code uses the HomeTheaterFacade to interact with the home theater system in a simplified manner.

def main():
    # Create subsystem components
    dvd_player = DVDPlayer()
    projector = Projector()
    amplifier = Amplifier()
    lights = Lights()

    # Create the facade
    home_theater = HomeTheaterFacade(dvd_player, projector, amplifier, lights)

    # Use the facade to watch a movie
    home_theater.watch_movie("Inception")

    # End the movie and shut down the system
    home_theater.end_movie()

if __name__ == "__main__":
    main()        

Explanation:

  1. Subsystem Classes:

  • Represent various components of the home theater system, such as DVDPlayer, Projector, Amplifier, and Lights. Each class has methods to control its functionality.

2. Facade (HomeTheaterFacade):

  • Provides a simplified interface for interacting with the complex subsystem. It handles the coordination between the subsystem components and provides methods watch_movie() and end_movie() to perform common operations.

3. Client Code:

  • Uses the HomeTheaterFacade to watch a movie and end it. The client interacts with the simplified facade rather than dealing with each component directly.

Output:

Get ready to watch a movie...
Lights dimmed to 10%
Projector is now ON.
Projector input set to: DVD
Projector in wide screen mode.
Amplifier is now ON.
Amplifier volume set to: 5
Amplifier audio mode set to: Surround
DVD Player is now ON.
DVD Player is playing: Inception
Shutting down the home theater...
DVD Player stopped.
DVD Player is now OFF.
Projector is now OFF.
Amplifier is now OFF.
Lights dimmed to 100%        

Advantages of the Facade Pattern:

  1. Simplification: Provides a simplified interface to a complex subsystem, making it easier for clients to use.
  2. Decoupling: Reduces dependencies between the client code and the subsystem, making the code more modular and easier to maintain.
  3. Single Point of Control: Centralizes control over the subsystem, allowing for easier management and changes.

The Facade Pattern is a valuable design pattern for managing complexity in systems with multiple interacting components. By providing a unified and simplified interface, the facade makes it easier for clients to interact with complex subsystems, improving code readability and maintainability. In this example, we demonstrated how a home theater system can be simplified using a facade, allowing clients to control the system with ease.

15. Adapter Pattern

The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two different interfaces, enabling objects with different interfaces to collaborate. The Adapter Pattern is particularly useful when integrating legacy code or systems with new code that uses a different interface.

The Adapter Pattern aims to:

  1. Convert the interface of a class into another interface that clients expect.
  2. Enable compatibility between classes or systems with incompatible interfaces.
  3. Allow reusability of existing classes without modifying their source code.

Components of the Adapter Pattern

  1. Client: The system or object that uses the target interface.
  2. Target: Defines the interface expected by the client.
  3. Adaptee: The existing class that has an incompatible interface but contains the functionality needed by the client.
  4. Adapter: Implements the target interface and translates requests from the client to the adaptee’s interface.

Example: Integrating a Legacy Media Player

Let’s say we have a modern media player interface (MediaPlayer) that expects to play MP4 and VLC files. However, we also have a legacy audio player (AudioPlayer) that only supports MP3 files. We’ll use the Adapter Pattern to integrate the legacy AudioPlayer into the modern media player system.

1. Target Interface

The MediaPlayer interface defines the expected operations for playing media files.

from abc import ABC, abstractmethod

class MediaPlayer(ABC):
    @abstractmethod
    def play(self, audio_type, filename):
        """Play a media file."""
        pass        

2. Adaptee (Legacy Class)

The AudioPlayer class represents a legacy player that only supports playing MP3 files.

class AudioPlayer:
    def play_mp3(self, filename):
        print(f"Playing MP3 file: {filename}")        

3. Adapter

The MediaAdapter class implements the MediaPlayer interface and adapts the AudioPlayer (the legacy class) to be compatible with the new media player interface.

class MediaAdapter(MediaPlayer):
    def __init__(self, audio_type):
        self.audio_type = audio_type
        self.audio_player = AudioPlayer()

    def play(self, audio_type, filename):
        if audio_type == "mp3":
            self.audio_player.play_mp3(filename)
        else:
            print(f"Audio type {audio_type} not supported by AudioPlayer.")        

4. Concrete Client

The AdvancedMediaPlayer class is the modern player that supports playing MP4 and VLC formats. It will use the MediaAdapter to handle MP3 files.

class AdvancedMediaPlayer(MediaPlayer):
    def play(self, audio_type, filename):
        if audio_type == "mp4":
            print(f"Playing MP4 file: {filename}")
        elif audio_type == "vlc":
            print(f"Playing VLC file: {filename}")
        elif audio_type == "mp3":
            # Use the adapter for MP3 files
            adapter = MediaAdapter(audio_type)
            adapter.play(audio_type, filename)
        else:
            print(f"Invalid media type: {audio_type}. Supported types are MP4, VLC, and MP3.")        

5. Client Code

The client code interacts with the AdvancedMediaPlayer, using it to play various types of media files.

def main():
    player = AdvancedMediaPlayer()

    # Play different types of media
    player.play("mp3", "song.mp3")   # MP3 file (using the adapter)
    player.play("mp4", "video.mp4")  # MP4 file (supported directly)
    player.play("vlc", "movie.vlc")  # VLC file (supported directly)
    player.play("avi", "clip.avi")   # Unsupported format

if __name__ == "__main__":
    main()        

Explanation:

  1. Target Interface (MediaPlayer):

  • Defines the interface for playing media files. This is the interface expected by the modern media player.

2. Adaptee (AudioPlayer):

  • Represents the legacy audio player that only supports MP3 files. It cannot be directly used by the modern media player because its interface is incompatible.

3. Adapter (MediaAdapter):

  • Translates the MediaPlayer interface into the AudioPlayer interface, allowing the modern media player to play MP3 files through the adapter.

4. Client Code:

  • Uses the modern media player (AdvancedMediaPlayer) to play various media formats. The adapter is automatically invoked for MP3 files.

Output:

Playing MP3 file: song.mp3
Playing MP4 file: video.mp4
Playing VLC file: movie.vlc
Invalid media type: avi. Supported types are MP4, VLC, and MP3.        

Advantages of the Adapter Pattern:

  1. Compatibility: Enables the integration of legacy code or third-party libraries with a new system by adapting their interfaces.
  2. Reusability: Allows reuse of existing functionality without modifying the original code, reducing development effort.
  3. Flexibility: Can handle multiple types of incompatible interfaces by using different adapters.

The Adapter Pattern is a versatile pattern that enables systems with incompatible interfaces to work together. By introducing an adapter, you can integrate legacy code or third-party libraries with modern systems without modifying the original classes. In this example, we demonstrated how to use the Adapter Pattern to integrate a legacy audio player that only supports MP3 files into a modern media player system, allowing seamless playback of various media formats.

Conclusion

Design patterns provide a reusable blueprint for solving common problems in software design. Python, with its object-oriented capabilities, makes implementing these patterns straightforward. Understanding and using design patterns such as Singleton, Factory, Observer, Strategy, Decorator, and Command can greatly improve the structure, maintainability, and scalability of your code.

Each of these patterns addresses specific challenges and enhances different aspects of software architecture. While it’s important to know when and how to use design patterns, overuse of them can also lead to unnecessary complexity, so it’s essential to apply them judiciously.


If you found them helpful:

  • Show some love with a like
  • Drop a comment.
  • Share your favorite part!

Discover more in my online courses at Appmillers

Connect on LinkedIn: Elshad Karimov

Follow on X (Twitter) : Elshad Karimov

Jalal Mirzayev

If you stop getting better, you've stopped being good.

3 周

Elshad this is quite an overview! A videos series would be great!

回复
Gabriel Gitonga

Software Engineer at Smart Applications International Ltd

1 个月

Good job

回复

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

社区洞察

其他会员也浏览了