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
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.
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.
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).
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++.
.
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
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.
This structure is composed of the following participants (the names of participants are those used by the Gang of Four (1.)):
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++.
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:
Instead an object adapter:
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:
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.
New the new vendor interface, called SmartPay, looks like this:
领英推荐
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.
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.
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.
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
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.
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:
And before concluding let recall the connection between the pattern adapter and the SOLID Principles:
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/".?