본문 바로가기
Practical Python Design Patterns

Adapter Pattern

by 자동매매 2023. 3. 28.

CHAPTER 7 Adapter Pattern

It is not the strongest of the species that survives, nor the most intelligent. It is the one that is most adaptable to change.

Charles Darwin

Sometimes you do not have the interface you would like to connect to. There could be many reasons for this, but in this chapter, we will concern ourselves with what we can do to change what we are given into something that is closer to what we would like to have.

Let s say you want to build your own marketing software. You begin by writing some code to send emails. In the larger system, you might make a call like this:

send_email.py

import smtplib

from email.mime.text import MIMEText

def send_email(sender, recipients, subject, message):

msg = MIMEText(message)

msg[’Subject’] = subject

msg[’From’] = sender

msg[’To’] = ",".join(recipients)

mail_sender = smtplib.SMTP(’localhost’) mail_sender.send_message(msg) mail_sender.quit()

if __name__ == "__main__":

response = send_email(

’me@example.com’,

["peter@example.com", "paul@example.com", "john@example.com"], *"This is your message"*, "Have a good day"

)

91

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_6
CHAPTER 7 ADAPTER PATTERN

Next, you implement an email sender as a class, since by now you suspect that if you are going to build larger systems you will need to work in an object-oriented way. The EmailSender class handles the details of sending a message to a user. Users are stored in a comma-separated values (CSV) file, which you load as needed.

users.csv

name, surname, email

peter, potter, peter@example.comma ...

Here is what your email-sending class might look like on a first pass: mail_sender.py

import csv

import smtplib

from email.mime.text import MIMEText

class Mailer(object):

def send(sender, recipients, subject, message):

msg = MIMEText(message) msg[’Subject’] = subject

msg[’From’] = sender

msg[’To’] = [recipients]

s = smtplib.SMTP(’localhost’) s.send_message(recipient) s.quit()

if __name__ == "__main__":

with open(’users.csv’, ’r’) as csv_file:

reader = csv.DictReader(csv_file) users = [x for row in reader]

mailer = Mailer()

mailer.send(

’me@example.com’,

[x["email"] for x in users],

*"This is your message"*, "Have a good day" )

92
CHAPTER 7 ADAPTER PATTERN

For this to work, you might need some sort of email service set up on your operating system. Postfix is a good free option should you need it. Before we continue with the example, I would like to introduce two design principles that you will see many times in this book. These principles provide a clear guide to writing better code.

Don’t Repeat Yourself (DRY)

Design principles like DRY are guiding lights in the forest of problems and challenges you need to overcome in any software project. They are little voices in the back of your head, guiding you toward a clean, clear, and maintainable solution. You will be tempted to skip over them, but doing so should leave you feeling uncomfortable. When you have dark corners in your code where no one likes to go, it is probably because the original developer did not follow these guidelines. I will not lie to you; in the beginning, it is hard. It often takes a little longer to do things the right way, but in the long run, it is worth it.

In the Gang of Four book (the original tome on object-oriented design patterns),

the two main principles proposed by the authors for solving common problems are as follows:

Program to an interface, not an implementation Favor object composition over inheritance

Keeping the first principle in mind, we would like to change the way we get user data so that we no longer deal with the implementation of data fetching inside the EmailSender class. We want to move this part of the code out into a separate class

in such a way that we are no longer concerned with how the data is retrieved. This will allow us to swap out the file system for a database of some sort, or even a remote microservice, should that be required in the future.

Separation of Concern

Programming to an interface allows us to create better separation between the code that gets user details and the code that sends the message. The more clearly you separate your system into distinct units, each with its own concern or task, and the more independent of one another these sections are, the easier it becomes to maintain and extend the system. As long as you keep the interface fixed, you are able to switch to newer and better ways of doing things without requiring a complete overall of your

93
CHAPTER 7 ADAPTER PATTERN

existing system. In the real world, this can mean the difference between a system that is stuck with a COBOL base and magnetic tape and one that moves with the times. It also has the added bonus of allowing you to course correct more easily should you find that one or more design decisions you made early on were sub-optimal.

This design principle is called Separation of Concern (SoC), and sooner or later you will either be very thankful you adhered to it or very sorry that you did not. You have been warned.

Let s follow our own advice and separate the retrieval of user information from the EmailSender class.

user_ fetcher.py

class UserFetcher(object): def __init__(source):

self.source = source

def fetch_users():

with open(self.source, ’r’) as csv_file:

reader = csv.DictReader(csv_file) users = [x for row in reader]

return rows mail_sender.py

import csv

import smtplib

from email.mime.text import MIMEText

class Mailer(object):

def send(sender, recipients, subject, message):

msg = MIMEText(message) msg[’Subject’] = subject

msg[’From’] = sender

msg[’To’] = [recipients]

s = smtplib.SMTP(’localhost’) s.send_message(recipient) s.quit()

94
CHAPTER ADAPTER PATTERN

if __name__ == "__main__":

user_fetcher = UserFetcher(’users.csv’) mailer = Mailer()

mailer.send(

’me@example.com’,

[x["email"] for x in user_fetcher.fetch_users()], *"This is your message"*, "Have a good day"

)

Can you see how it might take a bit of effort to get this right, but how much easier it will be when we want to drop in Postgres, Redis, or some external API without changing the code that does the actual sending of the email?

Now that you have this beautiful piece of code that sends emails to users, you might get a request for your code to be able to send status updates to a social media platform like Facebook or Twitter. You do not want to change your working email-sending code, and you found a package or program online that can handle the other side of the integration for you. How could you create some sort of messaging interface that would allow you to send messages to many different platforms without changing the code

doing the sending to fit each target, or reinventing the wheel to fit your needs?

The more general problem we want to solve is: The interface I have is not the interface I want!

As with most common software problems, there is a design pattern for that. Let s

work on a sample problem first so we can develop a bit of intuition about the solution before we implement it for the problem we have at hand.

Let s begin by oversimplifying the problem. We want to plug one piece of code into another, but the plug does not fit the socket. When trying to charge your laptop when traveling to another country, you need to get some sort of adapter to adapt your charger plug to the wall socket. In our everyday lives, we have adapters for switching between two-pronged plugs and three-pronged sockets, or for using VGA screens with HDMI outputs or other physical devices. These are all physical solutions for the problem of The interface I have is not the interface I want!

When you are building everything from the ground up, you will not face this

problem, because you get to decide the level of abstraction you are going to use in your system and how the different parts of the system will interact with one another. In the

real world, you rarely get to build everything from scratch. You have to use the plugin

95
CHAPTER ADAPTER PATTERN

packages and tools available to you. You need to extend code you wrote a year ago in a way that you never considered, without the luxury to go back and redo everything you did before. Every few weeks or months, you will realize that you are no longer the programmer you were before you became better but you still need to support the projects (and mistakes) you previously made. You will need interfaces you do not have. You will need adapters.

Sample Problem

We said that we want an adapter, whenever really we want a specific interface that we do not have.

class WhatIHave(object):

def provided_function_1(self): pass def provided_function_2(self): pass

class WhatIWant(object):

def required_function(): pass

The first solution that comes to mind is simply changing what we want to fit what we have. When you only need to interface with a single object, you are able to do this, but let s imagine we want to also use a second service, and now we have a second interface. We could begin by using an if statement to direct the execution to the right interface:

class Client(object)

def __init__(some_object):

self.some_object = some_object

def do_something():

if self.some_object.__class__ == WhatIHave: self.some_object.provided_function_2() self.some_object.provided_function_1()

else if self.some_object.__class__ == WhatIWant:

self.some_object.required_function()

else:

print("Class of self.some_object not recognized")

96
CHAPTER ADAPTER PATTERN

In a system where we have only two interfaces, this is not a bad solution, but by now you know that very rarely will it stay just the two systems. What also tends to happen

is that different parts of your program might make use of the same service. Suddenly, you have multiple files that need to be altered to fit every new interface you introduce. In the previous chapter, you realized that adding to a system by creating multiple, often telescoping, if statements all over the place is rarely the way to go.

You want to create a piece of code that goes between the library or interface you have and makes it look like the interface you want. Adding another similar service will then simply entail adding another interface to that service, and thus your calling object becomes decoupled from the provided service.

Class Adapter

One way of attacking this problem is to create several interfaces that use polymorphism to inherit both the expected and the provided interfaces. So, the target interface can be created as a pure interface class.

In the example that follows, we import a third-party library that contains a class called WhatIHave. This class contains two methods, provided_function_1 and provided_function_2, which we use to build the function required by the client object.

from third_party import WhatIHave

class WhatIWant:

def required_function(self): pass

class MyAdapter(WhatIHave, WhatIWant): def __init__(self, what_i_have):

self.what_i_have = what_i_have

def provided_function_1(self):

self.what_i_have.provided_function_1

def provided_function_2(self):

self.what_i_have.profided_function_2

def required_function(self):

self.provided_function_1() self.provided_function_2()

97
CHAPTER ADAPTER PATTERN

class ClientObject():

def __init__(self, what_i_want):

self.what_i_want = what_i_want

def do_something():

self.what_i_want.required_function()

if __name__ == "__main__":

adaptee = WhatIHave()

adapter = MyAdapter(adaptee) client = ClientObject(adapter)

client.do_something()

Adapters occur at all levels of complexity. In Python, the concept of adapters extends beyond classes and their instances. Often, callables are adapted via decorators, closures, and functools.

There is yet another way that you could adapt one interface to another, and this one is even more Pythonic at its core.

Object Adapter Pattern

Instead of using inheritance to wrap a class, we could make use of composition. An adapter could then contain the class it wraps and make calls to the instance of the wrapped object. This approach further reduces the implementation complexity.

class ObjectAdapter(InterfaceSuperClass): def __init__(self, what_i_have):

self.what_i_have = what_i_have

def required_function(self):

return self.what_i_have.provided_function_1()

def __getattr__(self, attr):

  • Everything else is handled by the wrapped object return getattr(self.what_i_have, attr)

Our adapter now only inherits from InterfaceSuperClass and takes an instance as a parameter in its constructor. The instance is then stored in self.what_i_have.

98
CHAPTER 7 ADAPTER PATTERN

It implements a required_function method, which returns the result of calling the provided_function_1() method of its wrapped WhatIHave object. All other calls on the class are passed to its what_i_have instance via the ___getattr__() method.

You still only need to implement the interface you are adapting. ObjectAdapter does not need to inherit the rest of the WhatIHave interface; instead, it supplies __getattr__() to provide the same interface as the WhatIHave class, apart from the methods it implements itself.

The __getattr__() method, much like the __init__() method, is a special method in Python. Often called magic methods, this class of methods is denoted by leading and trailing double underscores, sometimes referred to as double under or dunder. When

the Python interpreter does an attribute lookup on the object but can t find it, the __getattr__() method is called, and the attribute lookup is passed to the self.what_i_ have object.

Since Python uses a concept called duck typing, we can simplify our implementation some more.

Duck Typing

Duck typing says that if an animal walks like a duck and quacks like a duck, it is a duck.

Put in terms we can understand, in Python we only care about whether an object offers the interface elements we need. If it does, we can use it like an instance of the interface without the need to declare a common parent. InterfaceSuperClass just lost its usefulness and can be removed.

Now, consider the ObjectAdapter as it makes full use of duck typing to get rid of the InterfaceSuperClass inheritance.

from third_party import WhatIHave

class ObjectAdapter(object):

def __init__(self, what_i_have):

self.what_i_have = what_i_have

def required_function(self):

return self.what_i_have.provided_function_1()

def __getattr__(self, attr):

  • Everything else is handeled by the wrapped object return getattr(self.what_i_have, attr)

99
CHAPTER ADAPTER PATTERN

The code does not look any different, other than having the default object instead

of InterfaceSuperClass as a parent class of the ObjectAdapter class. This means that we never have to define some super class to adapt one interface to another. We followed the intent and spirit of the adapter pattern without all the run-around caused by less- dynamic languages.

Remember, you are not a slave to the way things were defined in the classic texts of our field. As technology grows and changes, so too must we, the implementers of that technology.

Our new version of the adapter pattern uses composition rather than inheritance to deliver a more Pythonic version of this structural design pattern. Thus, we do not require access to the source code of the interface class we are given. This, in turn, allows us to not violate the open/closed principle proposed by Bertrand Meyer, creator of the Eiffel programming language, and the idea of design by contract.

The open/closed principle states: Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

You want to extend the behavior of an entity without ever modifying its source code.

When you want to learn something new, especially when it is not a trivial bit of information, you need to understand the information on a deep level. An effective strategy for gaining deeper understanding is to deconstruct the big picture into smaller parts and then simplify those into digestible chunks. You then use these simple chunks

to develop an intuitive sense of the problem and its solution. Once you gain confidence in your ability, you let in more of the complexity. Once you have a deep understanding of the parts, you continue to string them back together, and you will find that you have a much clearer sense of the whole. You can use the process we just followed whenever you want to begin exploring a new idea.

Implementing the Adapter Pattern in the Real World

Since you now have a feel for a clear, pythonic, implementation of the adapter pattern, let s apply that intuition to the case we looked at in the beginning of this chapter. We are going to create an adapter that will allow us to write text to the command line in order to show you what the adapter pattern looks like in action. As part of the exercises, you will be challenged to create an adapter to one of the social network libraries for Python and integrate that with the code that follows.

100
CHAPTER 7 ADAPTER PATTERN

import csv

import smtplib

from email.mime.text import MIMEText

class Mailer(object):

def send(sender, recipients, subject, message):

msg = MIMEText(message) msg[’Subject’] = subject

msg[’From’] = sender

msg[’To’] = [recipients]

s = smtplib.SMTP(’localhost’) s.send_message(recipient) s.quit()

class Logger(object):

def output(message):

print("[Logger]".format(message))

class LoggerAdapter(object):

def __init__(self, what_i_have):

self.what_i_have = what_i_have

def send(sender, recipients, subject, message):

log_message = "From: {}\nTo: {}\nSubject: {}\nMessage: {}".format(

sender,

recipients,

subject,

message

)

self.what_i_have.output(log_message)

def __getattr__(self, attr):

return getattr(self.what_i_have, attr)

if __name__ == "__main__":

user_fetcher = UserFetcher(’users.csv’) mailer = Mailer() mailer.send(

’me@example.com’,

101
CHAPTER ADAPTER PATTERN

[x["email"] for x in user_fetcher.fetch_users()], *"This is your message"*, "Have a good day"

)

You may just as well use the Logger class you developed in Chapter 1. In the preceding example, the Logger class is just used as an example.

For every new interface you want to adapt, you need to write a new AdapterObject type class. Each of these is constructed with two arguments in its constructor, one of which is an instance of the adaptee. Each of the adapters also needs to implement the send_message(self, topic, message_body) method with the required parameters so it can be called using the desired interface from anywhere in the code.

If the parameters we pass to each provided_function remain the same as those of the required_function, we could create a more generic adapter that no longer needs to know anything about the adaptee; we simply supply it an object and the provided_ function, and we re done.

This is what the generic implementation of the idea would look like:

class ObjectAdapter(object):

def __init__(self, what_i_have, provided_function):

self.what_i_have = what_i_have self.required_function = provided_function

def __getattr__(self, attr):

return getattr(self.what_i_have, attr)

Implementing the message-sending adapter in this way is also one of the exercises at the end of this chapter. By now you may have realized that you have to do more and more of the implementation yourself. This is by design. The aim is to guide you in the learning process. You should be implementing the code so you gain more intuition about the concepts they address. Once you have a working version, fiddle with it, break it, then improve upon it.

Parting Shots

You saw that adapters are useful when you need to make things work after they are designed. It is a way of providing a different interface to subjects to which the interface are provided from the interface provided.

102
CHAPTER ADAPTER PATTERN

The adapter pattern has the following elements:

Target - Defines the domain-specific interface the client uses Client - Uses objects that conform to the Target interface

Adaptee - e interface you have to alter because the object does not

conform to the Target

Adapter - The code that changes what we have in the Adaptee to what

we want in the Client

To implement the adapter pattern, follow this simple process:

  1. Define what the components are that you want to accommodate.
  2. Identify the interfaces the client requires.
  3. Design and implement the adapters to map the client s required interface to the adaptee s provided interface.

The client is decoupled from the adaptee and coupled to the interface. This gives you extensibility and maintainability.

Exercises

Extend the user-fetcher class to be able to fetch from a CSV file or a

SQLite database.

Pick any modern social network that provides an API to interact

with the network and post messages of some sort. Now, install the Python library for that social network and create a class and an object adapter for said library so the code you have developed in this chapter can be used to send a message to that social network. Once done, you can congratulate yourself on building the basis of many large businesses that offer scheduled social media messages.

Rewrite your code from the previous exercise to make use of the

generic adapter for all of the interfaces you implemented.

103

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

Facade Pattern  (0) 2023.03.28
Decorator Pattern  (0) 2023.03.28
Builder Pattern  (0) 2023.03.28
Factory Pattern  (0) 2023.03.28
The Prototype Pattern  (0) 2023.03.28

댓글