Structural design patterns are concerned with how classes and objects are composed to form larger structures. These patterns help ensure that if one part of a system changes, the entire system doesn't need to change as well. They focus on simplifying the design by identifying a simple way to realize relationships between entities.

Key Concepts

  1. Composition Over Inheritance: Structural patterns often emphasize composition over inheritance. This means that instead of creating a complex class hierarchy, you can achieve the same functionality by composing objects.
  2. Object Relationships: These patterns help manage relationships between objects to ensure that they can work together effectively.
  3. Flexibility and Reusability: By using structural patterns, you can create more flexible and reusable code.

Common Structural Patterns

Here are some of the most commonly used structural design patterns:

Pattern Description
Adapter Allows incompatible interfaces to work together.
Bridge Separates an object’s interface from its implementation.
Composite Composes objects into tree structures to represent part-whole hierarchies.
Decorator Adds additional responsibilities to an object dynamically.
Facade Provides a simplified interface to a complex subsystem.
Flyweight Reduces the cost of creating and manipulating a large number of similar objects.
Proxy Provides a surrogate or placeholder for another object.

Example: Adapter Pattern

The Adapter pattern allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces.

Example Scenario

Imagine you have a MediaPlayer interface that plays audio files and a AdvancedMediaPlayer interface that plays both audio and video files. You want to use AdvancedMediaPlayer in place of MediaPlayer.

Code Example

# MediaPlayer interface
class MediaPlayer:
    def play(self, audio_type, file_name):
        pass

# AdvancedMediaPlayer interface
class AdvancedMediaPlayer:
    def play_vlc(self, file_name):
        pass

    def play_mp4(self, file_name):
        pass

# Concrete implementation of AdvancedMediaPlayer
class VlcPlayer(AdvancedMediaPlayer):
    def play_vlc(self, file_name):
        print(f"Playing vlc file. Name: {file_name}")

    def play_mp4(self, file_name):
        pass

class Mp4Player(AdvancedMediaPlayer):
    def play_vlc(self, file_name):
        pass

    def play_mp4(self, file_name):
        print(f"Playing mp4 file. Name: {file_name}")

# Adapter class implementing MediaPlayer
class MediaAdapter(MediaPlayer):
    def __init__(self, audio_type):
        if audio_type == "vlc":
            self.advanced_music_player = VlcPlayer()
        elif audio_type == "mp4":
            self.advanced_music_player = Mp4Player()

    def play(self, audio_type, file_name):
        if audio_type == "vlc":
            self.advanced_music_player.play_vlc(file_name)
        elif audio_type == "mp4":
            self.advanced_music_player.play_mp4(file_name)

# Concrete implementation of MediaPlayer
class AudioPlayer(MediaPlayer):
    def __init__(self):
        self.media_adapter = None

    def play(self, audio_type, file_name):
        if audio_type == "mp3":
            print(f"Playing mp3 file. Name: {file_name}")
        elif audio_type in ["vlc", "mp4"]:
            self.media_adapter = MediaAdapter(audio_type)
            self.media_adapter.play(audio_type, file_name)
        else:
            print(f"Invalid media. {audio_type} format not supported")

# Client code
if __name__ == "__main__":
    audio_player = AudioPlayer()

    audio_player.play("mp3", "beyond_the_horizon.mp3")
    audio_player.play("mp4", "alone.mp4")
    audio_player.play("vlc", "far_far_away.vlc")
    audio_player.play("avi", "mind_me.avi")

Explanation

  1. Interfaces: MediaPlayer and AdvancedMediaPlayer define the interfaces for playing media files.
  2. Concrete Implementations: VlcPlayer and Mp4Player implement the AdvancedMediaPlayer interface.
  3. Adapter: MediaAdapter implements the MediaPlayer interface and uses an instance of AdvancedMediaPlayer to play the appropriate file format.
  4. Client: AudioPlayer uses MediaAdapter to play different types of media files.

Practical Exercise

Task

Create a Shape interface with a draw method. Implement two concrete classes, Rectangle and Circle, that implement the Shape interface. Then, create a ShapeMaker class that uses the Facade pattern to draw these shapes.

Solution

# Shape interface
class Shape:
    def draw(self):
        pass

# Concrete implementations
class Rectangle(Shape):
    def draw(self):
        print("Drawing a Rectangle")

class Circle(Shape):
    def draw(self):
        print("Drawing a Circle")

# Facade class
class ShapeMaker:
    def __init__(self):
        self.rectangle = Rectangle()
        self.circle = Circle()

    def draw_rectangle(self):
        self.rectangle.draw()

    def draw_circle(self):
        self.circle.draw()

# Client code
if __name__ == "__main__":
    shape_maker = ShapeMaker()

    shape_maker.draw_rectangle()
    shape_maker.draw_circle()

Explanation

  1. Shape Interface: Defines the draw method.
  2. Concrete Implementations: Rectangle and Circle implement the Shape interface.
  3. Facade: ShapeMaker provides a simplified interface to draw shapes.
  4. Client: Uses ShapeMaker to draw shapes without knowing the underlying implementation.

Summary

In this section, we introduced structural design patterns, which focus on how classes and objects are composed to form larger structures. We discussed the importance of composition over inheritance and provided an example of the Adapter pattern. We also included a practical exercise to reinforce the concepts learned. In the next sections, we will delve deeper into individual structural patterns and their applications.

© Copyright 2024. All rights reserved