Skip to content
Shop

CommunityJoin Our PatreonDonate

Sponsored Ads

Sponsored Ads

Structural Patterns

  • Focus: How classes and objects are composed to form larger structures.
  • Goal: Build flexible and complex structures from simpler components.
  • Examples:
    • Adapter: Allows incompatible interfaces to work together.
    • Composite: Treats a group of objects as a single object.
    • Facade: Provides a simplified interface to a complex system.

Adapter

The adapter pattern, a structural pattern in software design, aims to make incompatible interfaces work together. It acts as a bridge between two systems or classes with different interfaces, allowing them to interact seamlessly.

Imagine: You have a round peg and a square hole. Normally, they wouldn't fit together. But with an adapter, you can modify the round peg to fit the square hole, or vice versa.

Here's how the adapter pattern works:

  1. Target Interface: This is the interface that the client code expects to use.
  2. Adaptee: This is the existing class or system with the incompatible interface.
  3. Adapter: This is the mediator class that sits between the target interface and the adaptee. It implements the target interface and translates calls to it into calls understandable by the adaptee.

Types of Adapter Patterns:

  • Class Adapter: Inherits from the adaptee class and implements the target interface.
  • Object Adapter: Composes an instance of the adaptee class and implements the target interface.
  • Interface Adapter: Uses multiple inheritance to implement both the target and adaptee interfaces.

Benefits of using the Adapter Pattern:

  • Improved reusability: You can reuse existing code with incompatible interfaces without modifying it.
  • Increased flexibility: You can easily switch between different implementations of the adaptee without affecting the client code.
  • Enhanced maintainability: Your code becomes more modular and easier to understand and maintain.

When to use the Adapter Pattern:

  • When you need to integrate legacy code with a new system.
  • When you want to use a third-party library that has an incompatible interface.
  • When you want to create a generic interface that can work with different types of objects.

Remember:

  • The adapter pattern adds an extra layer of complexity to your code.
  • Use it judiciously and only when the benefits outweigh the drawbacks.

Examples:

  • Using a USB adapter to connect a printer with a different connector type to your computer.
  • Implementing a custom data access layer that adapts to different database systems.
  • Creating a generic logger that can write logs to different destinations (e.g., console, file).
python
class LegacyDatabase:
    def get_users(self):
        # Fetch users from legacy database format
        return [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class UserAdapter:
    def __init__(self, legacy_database):
        self.legacy_database = legacy_database

    def get_users(self):
        legacy_users = self.legacy_database.get_users()
        return [User(user["name"], user["age"]) for user in legacy_users]

# Usage
legacy_db = LegacyDatabase()
adapter = UserAdapter(legacy_db)
users = adapter.get_users()
for user in users:
    print(f"Name: {user.name}, Age: {user.age}")

Bridge

The bridge pattern, another structural pattern in software design, aims to decouple an abstraction from its implementation so that they can vary independently. It achieves this by introducing two separate class hierarchies: one for the abstraction and one for the implementation.

Think of it like this: You have a drawing program that allows users to draw different shapes (abstraction) in different colors (implementation). With the bridge pattern, you can change the shape library (abstraction) without affecting the color palette (implementation), and vice versa.

Here's how the bridge pattern works:

  1. Abstraction: This defines the interface that the client code uses to interact with the functionality. It doesn't have any implementation details itself.
  2. Implementor: This defines the concrete implementation of the functionality. It can have different implementations based on specific needs.
  3. Concrete Abstraction: This inherits from the abstraction and implements it using a specific implementor object. It provides a way to bind the abstraction to a particular implementation.
  4. Refined Abstraction: This extends the abstraction with additional functionality or behavior. It can be used with different implementors as well.

Benefits of using the Bridge Pattern:

  • Increased flexibility: You can easily change the implementation without affecting the client code and vice versa.
  • Improved code reusability: You can reuse both the abstraction and the implementation independently.
  • Enhanced maintainability: The code becomes more modular and easier to understand and maintain.

When to use the Bridge Pattern:

  • When you need to support multiple implementations of the same functionality.
  • When you want to decouple the abstraction from its implementation for easier testing and maintenance.
  • When you want to provide a framework for extending functionality without modifying the existing code.

Remember:

  • The bridge pattern adds an extra layer of complexity to your code.
  • Use it judiciously and only when the benefits outweigh the drawbacks.

Examples:

  • A database driver that allows you to connect to different database systems using the same API.
  • A graphics framework that allows you to use different rendering backends with the same rendering API.
  • A logging framework that allows you to send logs to different destinations (e.g., console, file, network) with the same logging API.
python
class Shape:
    def __init__(self, color):
        self.color = color

class Circle(Shape):
    def draw(self):
        print(f"Drawing {self.color} circle")

class Square(Shape):
    def draw(self):
        print(f"Drawing {self.color} square")

class RasterRenderer:
    def draw_shape(self, shape):
        print(f"Drawing {shape} using raster renderer")

class VectorRenderer:
    def draw_shape(self, shape):
        print(f"Drawing {shape} using vector renderer")

# Usage
circle = Circle("red")
square = Square("blue")

raster_renderer = RasterRenderer()
vector_renderer = VectorRenderer()

raster_renderer.draw_shape(circle)  # Output: Drawing red circle using raster renderer
vector_renderer.draw_shape(square)  # Output: Drawing blue square using vector renderer

Composite

The composite pattern, a structural pattern in software design, lets you treat individual objects and compositions of objects uniformly. It essentially allows you to build hierarchical structures of objects and work with them as if they were all individual objects.

Imagine: You have a file system with folders and files. Each folder can contain other folders and files, creating a hierarchy. The composite pattern lets you treat individual files and folders the same way, regardless of their position in the hierarchy.

Here's how the composite pattern works:

  1. Component: This is the base interface that both individual objects (leaves) and composite objects (containers) implement. It defines operations that can be performed on both types of objects, such as "add child" or "get name."
  2. Leaf: This represents an individual object that cannot have children. It implements the "Component" interface but does not offer methods like "add child."
  3. Composite: This represents a container object that can have child objects. It implements the "Component" interface and offers methods like "add child" and "get children."

Benefits of using the Composite Pattern:

  • Uniform treatment of objects: You can treat individual objects and compositions of objects in the same way, simplifying your code.
  • Recursive operations: You can easily perform operations on entire hierarchies by applying them to the root composite and letting it propagate down the tree.
  • Flexibility: You can easily add or remove objects from the hierarchy without affecting the rest of the code.

When to use the Composite Pattern:

  • When you need to represent hierarchical structures of objects.
  • When you want to treat individual objects and compositions of objects uniformly.
  • When you need to perform recursive operations on entire object hierarchies.

Remember:

  • The composite pattern can add some complexity to your code.
  • Use it judiciously and only when the benefits outweigh the drawbacks.

Examples:

  • A file system explorer that allows you to browse files and folders.
  • A menu system in a graphical user interface that allows you to navigate through nested menus.
  • A tree data structure that allows you to store and manipulate hierarchical data.
python
class Component:
    def __init__(self, name):
        self.name = name

    def operation(self):
        # Base operation
        pass

class Composite(Component):
    def __init__(self, name):
        super().__init__(name)
        self.children = []

    def add_child(self, child):
        self.children.append(child)

    def operation(self):
        # Recursively call operation on children
        for child in self.children:
            child.operation()

class Leaf(Component):
    def operation(self):
        print(f"Performing operation on {self.name}")

# Usage
root = Composite("Root")
branch1 = Composite("Branch 1")
branch2 = Composite("Branch 2")

leaf1 = Leaf("Leaf 1")
leaf2 = Leaf("Leaf 2")
leaf3 = Leaf("Leaf 3")

branch1.add_child(leaf1)
branch2.add_child(leaf2)
branch2.add_child(leaf3)

root.add_child(branch1)
root.add_child(branch2)

root.operation()

Decorator

The decorator pattern, another valuable structural pattern in software design, focuses on dynamically adding new behaviors to existing objects without altering their original structure. It achieves this by wrapping objects in additional layers that each add specific functionalities.

Think of it like this: You have a plain pizza. You can use the decorator pattern to add toppings like cheese, pepperoni, and mushrooms, each layer enhancing the original pizza with additional flavor and features. However, the base pizza remains the same.

Here's how the decorator pattern works:

  1. Component: This defines the interface for the objects that can be decorated. It specifies the methods that decorators can call.
  2. ConcreteComponent: This is the actual object that gets decorated. It implements the "Component" interface and provides the basic functionality.
  3. Decorator: This wraps a "Component" object and adds new functionality to it. It implements the "Component" interface and delegates most methods to the wrapped object, but can also add its own behavior before or after.
  4. ConcreteDecorator: This is a specific type of decorator that adds a particular functionality. It inherits from the "Decorator" and implements its own logic for adding behavior.

Benefits of using the Decorator Pattern:

  • Dynamic behavior extension: You can add new functionalities to objects at runtime without modifying their original code.
  • Flexible composition: You can combine different decorators to create different combinations of behavior.
  • Loose coupling: Decorators are loosely coupled with the concrete components, making the code more modular and maintainable.

When to use the Decorator Pattern:

  • When you need to add optional functionalities to objects without changing their core behavior.
  • When you want to allow users to customize objects with different combinations of features.
  • When you want to avoid subclassing existing classes to add new functionalities.

Remember:

  • The decorator pattern can add some complexity to your code.
  • Use it judiciously and only when the benefits outweigh the drawbacks.
  • Overusing decorators can lead to "decorator hell," making your code difficult to understand and maintain.

Examples:

  • Adding toppings to a pizza.
  • Adding filters to an image.
  • Logging the execution of a method.
  • Adding authorization checks to methods.

Decorator Pattern

python
class Pizza:
    def __init__(self):
        self.description = "Plain Pizza"
        self.cost = 5

class ToppingDecorator(Pizza):
    def __init__(self, decorated_pizza):
        self.decorated_pizza = decorated_pizza

class Cheese(ToppingDecorator):
    def __init__(self, decorated_pizza):
        super().__init__(decorated_pizza)
        self.description = self.decorated_pizza.description + ", Cheese"
        self.cost += 2

class Pepperoni(ToppingDecorator):
    def __init__(self, decorated_pizza):
        super().__init__(decorated_pizza)
        self.description = self.decorated_pizza.description + ", Pepperoni"
        self.cost += 3

pizza = Cheese(Pepperoni(Pizza()))
print(pizza.description, pizza.cost)  # Output: Plain Pizza, Cheese, Pepperoni 10
python
class Beverage:
	def cost()->int:
		pass

class AddonDecorator(Beverage):
	def cost()->int:
		pass

class Espresso(Beverage):
	def cost(self)->int:
		return 1
		
class Caramel(AddonDecorator):
	def __init__(self,b=Beverage):
		self.beverage = b
	def cost(self)->int:
		return self.beverage.cost() + 2

e = Espresso()
print(e.cost())

Facade

The facade pattern, another structural pattern in your design toolkit, aims to simplify the interface of a complex system by providing a single, unified point of access. Think of it like a building facade hiding the intricate internal structure and presenting a clean, accessible exterior.

Here's the key idea:

  • Complex system: Your system might have numerous classes, methods, and functionalities, making it overwhelming to interact with directly.
  • Facade class: This class acts as a simplified interface, hiding the complexity and exposing only the essential features and functionalities.
  • Client code: Users interact with the facade instead of the intricate system directly, simplifying development and usage.

Benefits of using the facade pattern:

  • Improved usability: Provides a user-friendly interface, making it easier for developers and clients to understand and use the system.
  • Reduced complexity: Hides the internal workings, simplifying the mental model needed to interact with the system.
  • Loose coupling: Decouples client code from the underlying implementation, making the system more flexible and maintainable.
  • Controlled access: Can restrict access to certain functionalities or data, enhancing security and control.

When to use the facade pattern:

  • When dealing with a complex system with numerous functionalities and classes.
  • When you want to simplify the interface for ease of use and understanding.
  • When you need to control access to specific functionalities within the system.
  • When you want to decouple client code from the internal implementation details.

Remember:

  • The facade pattern introduces an extra layer of abstraction, which might add some overhead.
  • Use it judiciously, only when the complexity of the system justifies the additional layer.
  • Overusing facades can lead to a situation where the facade itself becomes complex, negating its purpose.

Examples:

  • A library or framework might expose a facade class to simplify its complex API.
  • A system for managing user accounts might use a facade to provide essential functionalities like login and registration.
  • A payment processing system might expose a facade for managing payments and transactions.
python
class Database:
    def __init__(self, host, username, password):
        # Connect to database
        pass

    def get_users(self):
        # Fetch users from database
        pass

class UserFacade:
    def __init__(self, host, username, password):
        self.db = Database(host, username, password)

    def get_active_users(self):
        users = self.db.get_users()
        return [user for user in users if user["active"]]

facade = UserFacade("localhost", "user", "password")
active_users = facade.get_active_users()
print(active_users)  # List of active users

Flyweight

The flyweight pattern is a structural pattern that aims to minimize memory usage by sharing common parts of objects. It's particularly useful when you need to create many objects that have a lot of similar data but differ in only a few pieces.

Imagine: You're building a text editor and need to display thousands of characters on the screen. Each character has properties like font, size, and color. Storing all this information individually for each character would be memory-intensive.

Here's how the flyweight pattern works:

  1. Intrinsic state: This part of the data is shared by multiple objects and doesn't change, like font and size in our character example.
  2. Extrinsic state: This part of the data is unique to each object and changes frequently, like the actual character itself and its position on the screen.
  3. Flyweight object: This object stores the intrinsic state and provides methods to access and manipulate it.
  4. Client code: This code utilizes the flyweight objects to access and manipulate the data, providing the extrinsic state as needed.

Benefits of using the flyweight pattern:

  • Reduced memory usage: By sharing common data, you significantly decrease the amount of memory required to store many objects.
  • Improved performance: Fewer objects to create and manage can lead to faster rendering and processing.
  • Flexibility: You can easily add new flyweight objects with different intrinsic states as needed.

When to use the flyweight pattern:

  • When you need to create a large number of objects with many similarities.
  • When memory usage is a concern, and objects have large amounts of shared data.
  • When performance is critical, and minimizing object creation overhead is necessary.

Remember:

  • The flyweight pattern adds some complexity to your code.
  • Ensure the benefits outweigh the added complexity before applying it.
  • Not all objects are suitable for flyweighting; it works best with mostly shared data.

Examples:

  • Text characters in a text editor
  • Icons in a graphical user interface
  • Shapes in a drawing application
  • Enemies in a video game
python
class Character:
    def __init__(self, font, size, color):
        self.font = font
        self.size = size
        self.color = color

    def draw(self, x, y, text):
        print(f"Drawing '{text}' with font={self.font}, size={self.size}, color={self.color} at ({x}, {y})")

class CharacterFactory:
    characters = {}

    def get_character(self, font, size, color):
        key = (font, size, color)
        if key not in self.characters:
            self.characters[key] = Character(font, size, color)
        return self.characters[key]

factory = CharacterFactory()
char1 = factory.get_character("Arial", 12, "red")
char2 = factory.get_character("Arial", 12, "red")  # Reuses existing character
char3 = factory.get_character("Arial", 16, "blue")

char1.draw(10, 20, "Hello")
char2.draw(50, 20, "World")
char3.draw(100, 50, "Flyweight!")

Proxy

The proxy pattern, another valuable tool in your design toolbox, acts as a surrogate or placeholder for another object, controlling access to it and providing additional functionalities. It essentially sits between your code and the real object, offering various benefits depending on its implementation.

Here's the core idea:

  • Real object: The actual object you want to access, but it might be complex, expensive to create, or have security concerns.
  • Proxy object: This object stands in for the real object, handling interactions and potentially adding additional behavior.
  • Client code: Your code interacts with the proxy instead of the real object, benefiting from the added control and possibilities.

Types of proxy patterns:

  • Virtual Proxy: Delays the creation of the real object until it's actually needed, improving performance and memory usage.
  • Remote Proxy: Handles communication with a real object located on a different machine, simplifying distributed system interactions.
  • Protection Proxy: Controls access to the real object, enforcing security and permissions.
  • Smart Proxy: Adds additional functionalities to the real object, like caching or logging.

Benefits of using the proxy pattern:

  • Improved performance: Can defer object creation or optimize access, leading to faster execution.
  • Reduced complexity: Simplifies interaction with complex or remote objects.
  • Enhanced security: Controls access and enforces permissions on the real object.
  • Additional functionalities: Provides caching, logging, or other features beyond the real object's capabilities.

When to use the proxy pattern:

  • When accessing expensive or complex objects to improve performance or memory usage.
  • When dealing with remote objects to simplify distributed system communication.
  • When needing to enforce security or access control on an object.
  • When adding functionalities like caching or logging to an existing object.

Remember:

  • The proxy pattern introduces an extra layer of abstraction, which might add some overhead.
  • Choose the right type of proxy based on your specific needs and the desired functionality.
  • Overusing proxies can lead to complex code, so use them judiciously.

Examples:

  • Lazy loading images in a web application (virtual proxy)
  • Accessing a remote database service through a local proxy (remote proxy)
  • Implementing an authorization layer for accessing sensitive data (protection proxy)
  • Caching database queries to improve performance (smart proxy)
python
class RealImage:
    def __init__(self, filename):
        self.filename = filename
        self.image = None

    def load_from_disk(self):
        print(f"Loading image from disk: {self.filename}")
        self.image = ...  # Load image data

    def display(self):
        if not self.image:
            self.load_from_disk()
        print(f"Displaying image: {self.filename}")

class ImageProxy:
    def __init__(self, filename):
        self.filename = filename
        self.real_image = None

    def display(self):
        if not self.real_image:
            self.real_image = RealImage(self.filename)
        self.real_image.display()

proxy = ImageProxy("image.jpg")
proxy.display()  # Only loads image on first display
proxy.display()  # Reuses loaded image

Resources

https://refactoring.guru/design-patterns/factory-method

https://github.com/wesdoyle/design-patterns-explained-with-food

Structural Extended

Certainly! Structural design patterns help you organize and manage relationships between objects within your restaurant's operations. Here's how:

Core Concept: Structural patterns focus on how objects work together (communicate and collaborate) to achieve a desired outcome. This is crucial for ensuring smooth operations in your restaurant, as different departments and individuals need to interact seamlessly.

Specific Patterns and Restaurant Analogies

  • Adapter: Imagine having different types of suppliers with various ordering formats. An adapter allows you to convert their format into one your ordering system understands.

    • Example: Adapting a vegetable supplier's spreadsheet format to work with your inventory management software.
  • Bridge: Similar to having a "maitre d'" who connects customers to the right waiters based on their needs. This pattern creates a bridge between two incompatible interfaces.

    • Example: A bridge system connecting online reservation requests with your existing table management system.
  • Composite: This is like grouping similar items for efficient management. Imagine combining appetizer and side dish options under a single menu section with subcategories.

    • Example: A "Starters" menu section with subcategories for "Salads" and "Appetizers."
  • Decorator: Think of adding toppings or modifications to a base dish. This pattern allows you to dynamically add functionalities to an existing object without changing its core structure.

    • Example: Adding a "spicy" or "vegetarian" option to a dish on the menu, without needing a separate menu entry for each variation.
  • Facade: This is like having a central point of contact for customers, like a cashier who handles various tasks (taking orders, receiving payments, etc.). It simplifies the interface and hides the complexity of underlying processes.

    • Example: A self-service kiosk that acts as a facade, allowing customers to order, pay, and customize their orders without interacting with different staff members.

Benefits of Structural Patterns in a Restaurant:

  • Improved communication: Efficient interaction between different aspects of your operation.
  • Flexibility: Easier to add new functionalities or modify existing ones without major overhauls.
  • Increased code maintainability: Cleaner and more organized code makes it easier to understand and update.

Remember, these are just a few examples. Many other structural patterns can be applied to optimize different areas of your restaurant business. Feel free to ask if you'd like to delve deeper into a specific pattern or have a situation you want to explore in this context!

MVC (Model View Controller)

Resources