본문 바로가기
Mastering Python Design Patterns

The Adapter Pattern

by 자동매매 2023. 3. 22.

4 The Adapter Pattern

In the previous chapters, we have covered creational patterns, object-oriented programming patterns that help us with object creation procedures. The next category of patterns we want to present is structural design patterns.

A structural design pattern proposes a way of composing objects for creating new functionality. The first of these patterns we will cover is the adapter pattern.

The adapter pattern is a structural design pattern that helps us make two incompatible interfaces compatible. What does that really mean? If we have an old component and we want to use it in a new system, or a new component that we want to use in an old system, the two can rarely communicate without requiring any code changes. But, changing the code is not always possible, either because we don't have access to it, or because it is impractical. In such cases, we can write an extra layer that makes all the required modifications for enabling the communication between the two interfaces. This layer is called an adapter.

In general, if you want to use an interface that expects function_a(), but you only have function_b(), you can use an adapter to convert (adapt) function_b() to function_a().

In this chapter, we will discuss the following:

  • Real-world examples
    • Use cases
      • Implementation

Real-world examples

When you are traveling from most European countries to the UK or USA, or the other way around, you need to use a plug adapter for charging your laptop. The same kind of adapter is needed for connecting some devices to your computer: the USB adapter.

The Adapter Pattern Chapter 55*

In the software category, the Zope application server (http:/ / www.zope.org) is known for

its Zope Component Architecture (ZCA), which contributed an implementation of interfaces and adapters used by several big Python web projects. Pyramid, built by former Zope developers, is a Python web framework that took good ideas from Zope to provide a more modular approach for developing web apps. Pyramid uses adapters for making it possible for existing objects to conform to specific APIs without the need to modify them. Another project from the Zope ecosystem, Plone CMS, uses adapters under the hood.

Use cases

Usually, one of the two incompatible interfaces is either foreign or old/legacy. If the interface is foreign, it means that we have no access to the source code. If it is old, it is usually impractical to refactor it.

Using an adapter for making things work after they have been implemented is a good approach because it does not require access to the source code of the foreign interface. It is also often a pragmatic solution if we have to reuse some legacy code.

Implementation

Let's look at a relatively simple application to illustrate adaptation: a club's activities, mainly the need to organize performances and events for the entertainment of its clients, by hiring talented artists.

At the core, we have a Club class that represents the club where hired artists perform some evenings. The organize_performance() method is the main action that the club can perform. The code is as follows:

class Club:

def __init__(self, name): self.name = name

def __str__(self):

return f'the club {self.name}'

def organize_event(self):

return 'hires an artist to perform for the people'

Most of the time, our club hires a DJ to perform, but our application addresses the need to organize a diversity of performances, by a musician or music band, by a dancer, a one-man or one-woman show, and so on.

Via our research to try and reuse existing code, we find an open source contributed library that brings us two interesting classes: Musician and Dancer. In the Musician class, the

main action is performed by the play() method. In the Dancer class, it is performed by the dance() method.

In our example, to indicate that these two classes are external, we place them in a separate module. The code is as follows for the Musician class:

class Musician:

def __init__(self, name): self.name = name

def __str__(self):

return f'the musician {self.name}'

def play(self):

return 'plays music'

Then, the Dancer class is defined as follows:

class Dancer:

def __init__(self, name):

self.name = name

def __str__(self):

return f'the dancer {self.name}' def dance(self):

return 'does a dance performance'

The client code, using these classes, only knows how to call the organize_performance() method (on the Club class); it has no idea about play() or dance() (on the respective classes from the external library).

How can we make the code work without changing the Musician and Dancer classes?

Adapters to the rescue! We create a generic Adapter class that allows us to adapt a number of objects with different interfaces, into one unified interface. The obj argument of the __init__() method is the object that we want to adapt, and adapted_methods is a

dictionary containing key/value pairs matching the method the client calls and the method that should be called.

The code for the Adapter class is as follows:

class Adapter:

def __init__(self, obj, adapted_methods): self.obj = obj self.__dict__.update(adapted_methods)

def __str__(self):

return str(self.obj)

When dealing with the different instances of the classes, we have two cases:

  • The compatible object that belongs to the Club class needs no adaptation. We can treat it as is.
    • The incompatible objects need to be adapted first, using the Adapter class.

The result is that the client code can continue using the known organize_performance() method on all objects without the need to be aware of any interface differences between the used classes. Consider the following code:

def main():

objects = [Club('Jazz Cafe'), Musician('Roy Ayers'), Dancer('Shane Sparks')]

for obj in objects:

if hasattr(obj, 'play') or hasattr(obj, 'dance'):

if hasattr(obj, 'play'):

adapted_methods = dict(organize_event=obj.play)

elif hasattr(obj, 'dance'):

adapted_methods = dict(organize_event=obj.dance)

  • referencing the adapted object here

obj = Adapter(obj, adapted_methods)

print(f'{obj} {obj.organize_event()}')

Let's recapitulate the complete code of our adapter pattern implementation:

  1. We define the Musician and Dancer classes (in external.py).
  2. Then, we need to import those classes from the external module (in adapter.py):

from external import Musician, Dance

  1. We then define the Adapter class (in adapter.py).
  2. We add the main() function, as shown earlier, and the usual trick to call it (in adapter.py).

Here is the output when executing the python adapter.py command, as usual:

As you can see, we managed to make the Musician and Dancer classes compatible with the interface expected by the client, without changing their source code.

Summary

This chapter covered the adapter design pattern. We use the adapter pattern for making two (or more) incompatible interfaces compatible. We use adapters every day for interconnecting devices, charging them, and so on.

The adapter makes things work after they have been implemented. The Pyramid web framework, the Plone CMS, and other Zope-based or related frameworks use the adapter pattern for achieving interface compatibility.

In the Implementation section, we saw how to achieve interface conformance using the adapter pattern without modifying the source code of the incompatible model. This is achieved through a generic Adapter class that does the work for us.

In the next chapter, we will cover the decorator pattern.

[ 55 ] )

'Mastering Python Design Patterns' 카테고리의 다른 글

The Bridge Pattern  (0) 2023.03.22
The Decorator Pattern  (0) 2023.03.22
Other Creational Patterns  (0) 2023.03.22
The Builder Pattern  (0) 2023.03.22
The Factory Pattern  (0) 2023.03.22

댓글