Introduction

The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by converting the interface of a class into another interface that a client expects.

Key Concepts

  • Client: The class that interacts with the adapter.
  • Adapter: The class that converts the interface of the adaptee into an interface the client expects.
  • Adaptee: The class that needs to be adapted.

When to Use the Adapter Pattern

  • When you want to use an existing class, but its interface does not match the one you need.
  • When you want to create a reusable class that cooperates with unrelated or unforeseen classes, that is, classes that don't necessarily have compatible interfaces.
  • When you need to use several existing subclasses, but it's impractical to adapt their interface by subclassing every one.

Types of Adapter Patterns

  1. Class Adapter: Uses multiple inheritance to adapt one interface to another.
  2. Object Adapter: Uses composition to adapt one interface to another.

Example Scenario

Imagine you have a RoundHole class and a SquarePeg class. The RoundHole class expects objects of type RoundPeg, but you have a SquarePeg that you need to fit into the RoundHole.

Class Diagram

+----------------+        +----------------+        +----------------+
|   RoundHole    |        |    Adapter     |        |   SquarePeg    |
+----------------+        +----------------+        +----------------+
| +fits(peg)     |<-------| +fits(peg)     |<-------| +getWidth()    |
+----------------+        +----------------+        +----------------+

Implementation

Step-by-Step Implementation

  1. Define the Target Interface: The interface that the client expects.
  2. Create the Adaptee Class: The existing class that needs to be adapted.
  3. Create the Adapter Class: The class that implements the target interface and translates the requests to the adaptee.

Code Example

Step 1: Define the Target Interface

class RoundPeg:
    def __init__(self, radius):
        self.radius = radius

    def get_radius(self):
        return self.radius

Step 2: Create the Adaptee Class

class SquarePeg:
    def __init__(self, width):
        self.width = width

    def get_width(self):
        return self.width

Step 3: Create the Adapter Class

class SquarePegAdapter(RoundPeg):
    def __init__(self, square_peg):
        self.square_peg = square_peg

    def get_radius(self):
        # Calculate a radius that fits the square peg
        return self.square_peg.get_width() * (2 ** 0.5) / 2

Step 4: Client Code

def main():
    round_hole = RoundPeg(5)
    small_square_peg = SquarePeg(5)
    large_square_peg = SquarePeg(10)

    small_square_peg_adapter = SquarePegAdapter(small_square_peg)
    large_square_peg_adapter = SquarePegAdapter(large_square_peg)

    print(f"Small square peg fits in round hole: {round_hole.get_radius() >= small_square_peg_adapter.get_radius()}")
    print(f"Large square peg fits in round hole: {round_hole.get_radius() >= large_square_peg_adapter.get_radius()}")

if __name__ == "__main__":
    main()

Explanation

  • RoundPeg: The target interface that the client expects.
  • SquarePeg: The adaptee class that needs to be adapted.
  • SquarePegAdapter: The adapter class that translates the interface of SquarePeg to RoundPeg.

Practical Exercises

Exercise 1: Implement a Class Adapter

Task: Implement a class adapter for the SquarePeg class using multiple inheritance.

Solution:

class SquarePegAdapter(RoundPeg, SquarePeg):
    def __init__(self, width):
        SquarePeg.__init__(self, width)

    def get_radius(self):
        return self.get_width() * (2 ** 0.5) / 2

def main():
    round_hole = RoundPeg(5)
    small_square_peg_adapter = SquarePegAdapter(5)
    large_square_peg_adapter = SquarePegAdapter(10)

    print(f"Small square peg fits in round hole: {round_hole.get_radius() >= small_square_peg_adapter.get_radius()}")
    print(f"Large square peg fits in round hole: {round_hole.get_radius() >= large_square_peg_adapter.get_radius()}")

if __name__ == "__main__":
    main()

Exercise 2: Adapter for Different Libraries

Task: Create an adapter that allows a logging library with a log_message method to be used with a client expecting a write_log method.

Solution:

class OldLogger:
    def log_message(self, message):
        print(f"Logging message: {message}")

class LoggerAdapter:
    def __init__(self, old_logger):
        self.old_logger = old_logger

    def write_log(self, message):
        self.old_logger.log_message(message)

def main():
    old_logger = OldLogger()
    logger_adapter = LoggerAdapter(old_logger)

    logger_adapter.write_log("This is a log message.")

if __name__ == "__main__":
    main()

Common Mistakes and Tips

  • Mistake: Forgetting to implement all methods of the target interface in the adapter.
    • Tip: Ensure that the adapter class implements all methods required by the target interface.
  • Mistake: Overcomplicating the adapter logic.
    • Tip: Keep the adapter logic simple and focused on translating the interface.

Conclusion

The Adapter pattern is a powerful tool for making incompatible interfaces work together. By understanding and implementing this pattern, you can create flexible and reusable code that can integrate with various existing systems and libraries. This pattern is especially useful when dealing with legacy code or third-party libraries that cannot be modified.

In the next section, we will explore the Bridge pattern, another structural pattern that helps decouple an abstraction from its implementation.

© Copyright 2024. All rights reserved