Adapter pattern: the most common pattern in software maintenance … and in the real world.

Adapter pattern: the most common pattern in software maintenance … and in the real world.

In this article we continue to talk about structural design patterns: this time we will look at the?"Adapter?Pattern"?(.1).

Sometimes, there could be a scenario where two objects don't fit together like they should in order to work together. This situation could arise when we are trying to integrate legacy code with new code or when changing a third-party API in our code. This is due to incompatible interfaces of the two objects that do not fit together.

The Adapter pattern lets us adapt what an object or a class exposes to what another object or class expects. It converts the interface of a class into another interface the client expects. It allows to fix the interface between the objects and the classes without modifying the objects and the classes directly.

We can think of an Adapter Pattern as a real-world adapter which is used to connect two different pieces of equipment that cannot be connected directly. An adapter is placed between these equipment, it gets the flow from one equipment and provides it to the other equipment in the form it wants, which is impossible to get otherwise, due to their incompatible interfaces.

An adapter uses composition to store the object it is supposed to adapt and when the adapter methods are called, it translates those calls into something the adapted object can understand and passes the calls on to the adapted object. We will see later that multiple inheritance can be used instead of composition to adapt one interface to another (unfortunately there are few languages that have multiple inheritance). In both of these two cases the code that calls the adapter never needs to know that it’s not dealing with the kind of object it thinks it is, but an adapted object instead.

This pattern is easy to understand as the real world is full of adapters. For example, imagine this: you are an European worker on a business trip to the USA. When you arrive at a hotel in New York and suppose you have to charge your laptop but there is a problem !! The power plug and sockets standards are different, you have a two-round pin charger which don’t fit a US socket.

The power plug and sockets standards are different in different countries. That’s why your European (German) plug won’t fit a US socket.

Okay, Let’s see how the adapter design pattern solves the problem of two incompatible interface

At this instance, we will introduce an adapter interface (charger) that allows you to change the two-pin interface to a square-pin interface

So, the functionality of the adapter (charger) is that it allows two incompatible interfaces to work together.

Finally, you can charge the mobile phone by using the?adapter pretty effectively, right?

let’s see the words that we use during the adapter design process

the?"client"?is your two-pin charger.

the?"adapter"?is a charger that converts the two-pin interface to square pin interface.

the?"adaptee"?is the square-pin socket.

And below the structure of this solution

Non è stato fornito nessun testo alternativo per questa immagine
figure 1 : structure of laptop power plug adapter.


Let's go back to the software world and give a simple example to clarify the scenario.

We consider having to maintain a CAD source. This CAD uses a framework where the draw method of the rectangle wants the coordinates of the upper left vertex and the coordinates of the lower right vertex as call parameters.

However, the part of the CAD that deals with object rotations cannot use the same framework but is forced, for performance reasons, to use a different library (because this library is highly optimized on vector calculus). However, this library requires that the rectangles be drawn passing as parameters the coordinates of the top left vertex, the width and the height. We want to rearrange the code so that we call the draw function of the rectangle in the top left and bottom right vertex mode in this part of the program as well.

Below, a legacy Rectangle component?draw()?method expects to receive "x, y, w, h" parameters. But the client wants to pass "upper left x and y" and "lower right x and y". This incongruity can be reconciled by adding an additional level of indirection, an Adapter object.

Non è stato fornito nessun testo alternativo per questa immagine
figure 2 : adapter for legacy rectangle : class diagram.

Following is a conceptual example of the source code in Kotlin of the adapter pattern applied to solve the problem of the legacy call to the draw method of the Rectangle class.

Non è stato fornito nessun testo alternativo per questa immagine
figure 3 : adapter for legacy rectangle : class diagram : conceptual source code.

The unit test "TestAdapter" proves the validity of this implementation of the adapter pattern

Now let me tell you about my first experience with the pattern adapter.

The first time I consciously used the pattern adapter was more than twenty-five years ago. Probably before that I had used similar structures but I wasn't aware of it, that is I hadn't read the "Gang Of Four" book yet (1.).

It was the software of a circuit of machines for strength exercise, very advanced machines for those times. Just think that both the selection of the weight and the settings of seat adjustments were managed automatically by stepper motors. Their settings could have been done either by the means of the buttons present in the machine consoles, or by the means of a portable RF memory (immersed in a key-shaped mold and usable also as a key ring, it can be seen in figure 4) which was also read by a RF reader placed in the console(this was a proprietary technology since NFC technology did not exist at the time).

Non è stato fornito nessun testo alternativo per questa immagine
figure 4: Equipments for strength exercises and portable memory to automatically manage loads and adjustment on equipments.

The implementation of the adapter pattern made it possible to avoid a code full of if, below we can see the class-diagram of the solution written in C++.

.

Non è stato fornito nessun testo alternativo per questa immagine
figure 5: adapter pattern structure for multi-stepper-motor features.

Target is an interface that declares all those methods that can be invoked for stepper-motor management and is the only class used by the rest of the software (Client).

These methods are translated by the Adapter into actions on the concrete Motor. The concrete motor is set by the Adapter based on the analysis of the serial code of the machine stored in EEPROM.

Below is a conceptual implementation of the software

Non è stato fornito nessun testo alternativo per questa immagine
figure 6 : conceptual software implementation for multi-stepper-motor features.

At this point, after having seen a couple of examples of adapter applications, we can continue the analysis of the pattern and see its general structure.

There are two approaches to implementing the adapter pattern, the "Object Adapter" and the "Class Adapter". However, both these approaches produce same result.

Let's start by analyzing the Object Adapter implementation.

This implementation uses the object composition principle: the adapter implements the interface of one object and wraps the other one. It can be implemented in all popular programming languages. Figure 7 shows the Class Adapter structure.

Non è stato fornito nessun testo alternativo per questa immagine
figure 7 : object adapter structure

This structure is composed of the following participants (the names of participants are those used by the Gang of Four (1.)):

  1. The?Client?is a class that contains the existing business logic of the program.
  2. The?Target (I prefer the name Client Interface for it)?describes a protocol that other classes must follow to be able to collaborate with the client code.
  3. The?Adaptee ( I prefer the name Service?for it) is some useful class (usually 3rd-party or legacy). The client can’t use this class directly because it has an incompatible interface.
  4. The?Adapter?is a class that’s able to work with both the client and the Adaptee: it implements the client interface, while wrapping the adaptee object. The adapter receives calls from the client via the adapter interface and translates them into calls to the adaptee object in a format it can understand.

The client code doesn’t get coupled to the concrete adapter class as long as it works with the adapter via the Target interface (better call it Client Interface). Thanks to this, we can introduce new types of adapters into the program without breaking the existing client code. This can be useful when the interface of the Adaptee class (better call it Service) gets changed or replaced: we can just create a new adapter class without changing the client code

And now let's see the Class Adapter implementation.

This implementation uses inheritance: the adapter inherits interfaces from both objects at the same time. Note that this approach can only be implemented in programming languages that support multiple inheritance, such as C++.

Non è stato fornito nessun testo alternativo per questa immagine
figure 8 : class adapter structure

The classes present in this structure are the same as those of the Object Adapter, but the?Class Adapter?doesn’t need to wrap any objects because it inherits behaviors from both the Target and the Adaptee. The adaptation happens within the overridden methods. The resulting adapter can be used in place of an existing client class.

In both implementation types (Object Adapter or Class Adapter) collaboration between classes occurs in the same way, that is the client calls operations on an Adapter instance. So, the adapter calls Adaptee operations that perform the request.

But Class and Object adapters have different trade-offs.

A class adapter adapts Adaptee to Target in a concrete Adapter class. As a consequence:

  • a class adapter won't work when we want to adapt a class and all its subclasses.
  • it allows Adapter override some of Adaptee's behavior, since Adapter is a subclass of Adaptee.
  • and also introduces only one object, and no additional pointer indirection is needed to get to the adaptee.

Instead an object adapter:

  • lets a single Adapter work with many Adaptees that is, the Adaptee itself and all of its subclasses (if any). The Adapter can also add functionality to all Adaptees at once.
  • makes it harder to override Adaptee behavior. It will require subclassing Adaptee and making Adapter refer to the subclass rather than the Adaptee itself.


Let's now continue with another example where it is appropriate to apply the adapter pattern.

Again, let's think about a situation that could be real for software developers like us. Let's assume we have to work on an e-commerce website. The website allows users to shop and pay online. The site is integrated with a third-party payment gateway, through which users can pay bills using credit card. At one point the director tells us that is planning to change payment gateway vendor and that we need to implement this in our code.

The problem is that the site is connected to the SafePay payment gateway which accepts a SafePay object type. The new vendor, SmartPay, only allows the SmartPay object type to allow the process. We don't want to change the entire set of one hundred classes which have reference to an object of type SafePay. This also increases the risk on the project, which is already running on production. The problem occurred due to the incompatible interfaces between the two different parts of the code. In order to get the process work, we need to find a way to make the code compatible with the new vendor-provided API.

What we need here is an adapter that can stand between the code and the vendor API and can allow the process to flow.

Now, let's see how we can solve this problem. This time we will use the Java language.

Currently, the code is exposed to the SafePay interface. The interface looks something like this:

Non è stato fornito nessun testo alternativo per questa immagine
figura 9 : SafePay interface

It contains set of setters and getter method used to get the information about the credit card and customer name. This?SafePay interface is implemented in the code which is used to instantiate an object of this type, and exposes the object to the vendor’s API.

The following class defines the implementation to the?SafePay interface.

Non è stato fornito nessun testo alternativo per questa immagine
figura 10 : SafePayImpl an implementation of SafePay

New the new vendor interface, called SmartPay, looks like this:

Non è stato fornito nessun testo alternativo per questa immagine
figura 11 : SmartPay

As we can see, this interface has a set of different methods which need to be implemented in the code. But the old SafePay is created by most part of the code, it’s really hard and risky to change all this code.

We need a way that can meet the new vendor's requirements to process the payment and also make few or no changes to the current code. The way is provided by the Adapter pattern.

We will create an adapter that will be of type SmartPay, and it wraps an SafePay object, SafePay is the type that needs to be adapted.

Non è stato fornito nessun testo alternativo per questa immagine
figura 12 : adapter from SafePay to SmartPay

In the above code, we created an Adapter (SavePayToSmartPayAdapter). The adapter implements the SmartPay interface, since it needs to imitate a SmartPay object type. The adapter uses object composition to hold the object. The object is passed to the adapter through its constructor.

Now, keep in mind that we have two types of incompatible interfaces, which we need to fit together using an adapter in order to make the code work. These two interfaces have a different set of methods. But the sole purpose of these interfaces is very similar, which is to provide customer and credit card information to their specific vendors.

The setProp() method of the above class is used to set the SafePay properties in the SmartPay object. We set up methods that are similar in work in both interfaces. However, there is only one method in the SmartPay interface to set the credit card month and year, unlike the two methods in the SafePay interface. We merged the result of the two methods of the SafePay object and set it in the setCardExpMonthYear() method of SmartPay.

Let us test the above code and see whether it can solve our problem.

Non è stato fornito nessun testo alternativo per questa immagine
figura 13 : unit test code

In the test class, we first created a SafePay object and set its properties. So, we created an adapter and passed it the safePay object in its constructor and assigned it to the SmartPay interface. The static testSmartPay() method takes a SmartPay type as an argument to execute and print its methods for testing. As we can see from the output in console below the code works fine.

Non è stato fornito nessun testo alternativo per questa immagine
figura 14 : unit test output

So, in our project all we need to implement the new vendor API in code is to pass this adapter to the vendor method to make payment work. You don't need to change anything in your existing code.

Below we can see the class-diagram of our solution

Non è stato fornito nessun testo alternativo per questa immagine
figura 15

Obviously, it is a structure of type object adapter

As Java does not support multiple inheritance, we cannot apply a class adapter solution.


Let's now take a bit more complicated example where the application of the adapter pattern is less trivial. This is still a real case that I had to solve last year.

As I have said on more than one occasion, for some years I have been developing software for physical exercise equipment. In the example I'm talking about, I'm referring in particular to a recent line of products developed on our customization of the Android AOSP platform.

Most of the applications of the exercises have been developed in the Java language. These applications make calls to cloud services (for example to set up the equipment with the loads that a AI Model assigns to the user or even to save the results of the exercises performed). To call these cloud services we use an API auto-generated from the services specifications. This API includes the request and response classes of all services as well as functions for calling the services themselves. As call client this API uses by default an OkHttp client which, however, we can specialize respecting a defined interface..

For technical reasons, the cloud services development team had to change the services architecture to a microservices architecture. Now it was no longer possible to generate a legacy API because the generation toolchain was no longer adequate for this new service technology. A new API was therefore created to call the services, starting from the documentation of the services via Swagger and codegen and setting Retrofit as the call client and kotlin as the generated code. With these settings, codegen generates APIs where all service call functions are declared suspended.

These APIs could therefore only be invoked within a coroutine context. This was fine for those applications already written in Kotlin. Obviously, however, it could not work with those applications written in Java because it is not possible to instantiate a context of coroutines.

In fact, in Java generally operations like HTTP calls are performed inside java worker threads and the results are transmitted via continuations which allows the result to be obtained asynchronously on a callback (see continuation interface in snippet code of figure 16).

It was therefore necessary to find a mechanism that would allow us to adapt calls to services with the new API also in the context of java worker threads.

Both for code clarity and for confidentiality reasons, I won't show the real code. I wrote a unit-test instead.

The Unit Test is written in Java and shows how it is possible to call a cloud service through an API written in Kotlin and declared suspend, that uses Retrofit as call client (see figure 18).

As a service I used a free HTTP-service of the GET type which returns the list of countries of the world.

Snippet 16 instead shows an API to call the service through a call function declared suspend. Similar situation to the API generated by our cloud services team.

Snippet 17 instead shows the adapter created to allow using this API in a Java call context within a worker thread and having the result notified in the callback functions of a continuation object passed as an API call parameter .

So, this is exactly the same problem we had in the Java applications of our exercise equipment.

Below is the code of the API (figure 16), the Adapter (figure 17) and the test program (figure 18).

I also include some links for those unfamiliar with aspects of kotlin, suspend functions and coroutines.

Non è stato fornito nessun testo alternativo per questa immagine
figura 16 : CountryAPI (Retrofit based)


Non è stato fornito nessun testo alternativo per questa immagine
figura 17 : CountriesAdapter : allows to adapt the suspend function of CountriesService to a call made by a java worker thread


Non è stato fornito nessun testo alternativo per questa immagine
figura 18 : Unit Test of CountriesAdapter


After analyzing the structure of the Adapter pattern and after examining some examples in the three languages of Android Studio (Kotlin,?Java?and?C++)?let's remember that it is useful to use the pattern when we want to use some existing class, but its interface isn’t compatible with the rest of our code. The Adapter pattern lets us create a middle-layer class that serves as a translator between our code and a legacy class, a 3rd-party class or any other class with an incompatible interface. Let's summarize the steps to implements it:

  1. Let's make sure we have at least two classes with incompatible interfaces: a useful service class (Adaptee), which we cannot change (often third-party, legacy, or with many existing dependencies) and one or more client classes that would benefit from using the class of service.
  2. We declare the client interface and describe how clients communicate with the service (Adaptee).
  3. Create the adapter class and make it follow the client interface.
  4. Let's add a field to the adapter class to store a reference to the service object.
  5. We implement all client interface methods in the adapter class. The adapter should delegate most of the actual work to the adaptee object, handling only the interface or data format conversion.
  6. Clients must use the adapter through the client interface. This will allow us to modify or extend adapters without affecting client code.

And before concluding let recall the connection between the pattern adapter and the SOLID Principles:

  • In relation to Single Responsibility Principle (.2) we can separate the interface or data conversion code from the primary business logic of the program.
  • Instead in relation to Open/Closed Principle (.3) we can introduce new types of adapters into the program without breaking the existing client code, because it works with adapters through the client interface.


And finally here is the last suggestions on using the Adapter pattern:

Adapter is commonly used with an existing app to make some otherwise-incompatible classes work together nicely. The overall complexity of the code increases because you need to introduce a set of new interfaces and classes. Sometimes it’s simpler just to change the service class so that it matches the rest of your code. So, we must always evaluate whether the implementation of the Adapter pattern is justified.


That's it for the "Adapter Pattern".

?

?I remind you my newsletter?"Sw Design & Clean Architecture":??https://lnkd.in/eUzYBuEX ?where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.

?

?

thanks for reading my article, and I hope you have found the topic useful,

Feel free to leave any feedback.

Your feedback is very appreciated.

?Thanks again.

Stefano

?

?

References:

1.?Gamma, Helm, Johnson, Vlissides, “Design Patterns”, Addison Wesley (2° Edition October 2002). pp 121-130.

2.?S. Santilli: "https://www.dhirubhai.net/pulse/single-responsibility-principle-stefano-santilli/".?

3.?S. Santilli: "https://www.dhirubhai.net/pulse/open-closed-principle-stefano-santilli/".?

要查看或添加评论,请登录

Stefano Santilli的更多文章

社区洞察

其他会员也浏览了