Adapter Design Pattern in Modern C++
Join the DZone community and get the full member experience.
Join For FreeIn software engineering, Structural Design Patterns deal with the relationship between object and classes i.e. how objects and classes interact or build a relationship in a manner suitable to the situation. The structural design patterns simplify the structure by identifying relationships. In this article of the Structural Design Patterns, we're going to take a look at Adapter Design Pattern in Modern C++ which used to convert the interface of an existing class into another interface that client/API-user expect. Adapter Design Pattern makes classes work together that could not otherwise because of incompatible interfaces.
By the way, If you haven't check out my other articles on Structural Design Patterns, then here is the list:
The code snippets you see throughout this series of articles are simplified not sophisticated. So you often see me not using keywords like override
, final
, public
(while inheritance) just to make code compact & consumable(most of the time) in single standard screen size. I also prefer struct
instead of class
just to save line by not writing "public:
" sometimes and also miss virtual destructor, constructor, copy constructor, prefix std::
, deleting dynamic memory, intentionally. I also consider myself a pragmatic person who wants to convey an idea in the simplest way possible rather than the standard way or using Jargons.
Note: If you stumbled here directly, then I would suggest you go through What is design pattern? first, even if it is trivial. I believe it will encourage you to explore more on this topic. All of this code you encounter in this series of articles are compiled using C++20(though I have used Modern C++ features up to C++17 in most cases). So if you don't have access to the latest compiler you can use https://wandbox.org/ which has preinstalled boost library as well.
Intent
To get the interface you want from the interface you have.
An adapter allows two incompatible classes to work together by converting the interface of one class into an interface expected by the client/API-user without changing them. Basically, adding intermediate class i.e. Adapter.Adapter Design Pattern Examples in C++
Implementing an Adapter Design Pattern is easy, just determine the API you have & the API you need. Create a component which aggregates(has a reference to,...) the adaptee.Classical Adapter
struct Point {
int32_t m_x;
virtual void draw(){ cout<<"Point\n"; }
};
struct Point2D : Point {
int32_t m_y;
void draw(){ cout<<"Point2D\n"; }
};
void draw_point(Point &p) {
p.draw();
}
struct Line {
Point2D m_start;
Point2D m_end;
void draw(){ cout<<"Line\n"; }
};
struct LineAdapter : Point {
Line& m_line;
LineAdapter(Line &line) : m_line(line) {}
void draw(){ m_line.draw(); }
};
int main() {
Line l;
LineAdapter lineAdapter(l);
draw_point(lineAdapter);
return EXIT_SUCCESS;
}
You can also create a generic adapter by leveraging C++ template as follows:
xxxxxxxxxx
template<class T>
struct GenericLineAdapter : Point {
T& m_line;
GenericLineAdapter(T &line) : m_line(line) {}
void draw(){ m_line.draw(); }
};
The usefulness of the generic approach hopefully becomes more apparent when you consider that when you need to make other things
Point
-like, the non-generic approach becomes quickly very redundant.
Pluggable Adapter Design Pattern using Modern C++
The Adapter should support the adaptees(which are unrelated and have different interfaces) using the same old target interface known to the client/API-user. Below example satisfy this property by using C++11's lambda function & functional header.xxxxxxxxxx
/* Legacy code -------------------------------------------------------------- */
struct Beverage {
virtual void getBeverage() = 0;
};
struct CoffeeMaker : Beverage {
void Brew() { cout << "brewing coffee" << endl;}
void getBeverage() { Brew(); }
};
void make_drink(Beverage &drink){
drink.getBeverage(); // Interface already shipped & known to client
}
/* --------------------------------------------------------------------------- */
struct JuiceMaker { // Introduced later on
void Squeeze() { cout << "making Juice" << endl; }
};
struct Adapter : Beverage { // Making things compatible
function<void()> m_request;
Adapter(CoffeeMaker* cm) { m_request = [cm] ( ) { cm->Brew(); }; }
Adapter(JuiceMaker* jm) { m_request = [jm] ( ) { jm->Squeeze(); }; }
void getBeverage() { m_request(); }
};
int main() {
Adapter adp1(new CoffeeMaker());
make_drink(adp1);
Adapter adp2(new JuiceMaker());
make_drink(adp2);
return EXIT_SUCCESS;
}
The pluggable adapter sorts out which object is being plugged in at the time. Once an object has been plugged in and its methods have been assigned to the delegate objects(i.e.
m_request
in our case), the association lasts until another set of methods is assigned.
What characterizes a pluggable adapter is that it will have constructors for each of the types that it adapts. In each of them, it does the delegate assignments (one, or more than one if there are further methods for rerouting).
Pluggable adapter provides the following two main benefits:
- You can bind an interface(bypassing lambda function in constructor argument), unlike the object we did in the above example.
- This also helps when adapter & adaptee have a different number of the argument.
Benefits of Adapter Design Pattern
- Open-Closed Principle: One advantage of the Adapter Pattern is that you don't need to change the existing class or interface. By introducing a new class, which acts as an adapter between the interface and the class, you avoid any changes to the existing code.
- This also limits the scope of your changes to your software component and avoids any changes and side-effects in other components or applications.
- By above two-point i.e. separate class(i.e. Single Responsibility Principle) for special functionality & fewer side-effects, it's obvious we do requires less maintenance, learning curve & testing.
- AdapterDesing Pattern also adheres to the Dependency Inversion Principle, due to which you can preserve binary compatibility between multiple releases.
Summary by FAQs
When to use the Adapter Design Pattern?
-- Use the Adapter class when you want to use some existing class, but its interface isn't compatible with the rest of your code.
-- When you want to reuse several existing subclasses that lack some common functionality that can't be added to the superclass.
-- For example, let say you have a function which accepts weather object & prints temperature in Celsius. But now you need to print the temperature in Fahrenheit. In this case of an incompatible situation, you can employ the Adapter Design Pattern.
Real-life & practical example of the Adapter Design Pattern?
-- In STL, stack, queue & priority_queue are adaptors from deque & vector. When stack executes stack::push(), the underlying vector does vector::push_back()
.
-- A card reader which acts as an adapter between the memory card and a laptop.
-- Your mobile & laptop charges are kind of adapter which converts standard voltage & current to the required one for your device.
What are the differences between Bridge & Adapter Design Pattern?
-- Adapter is commonly used with an existing app to make some otherwise-incompatible classes work together nicely.
-- Bridge is usually designed up-front, letting you develop parts of an application independently of each other.
What is the difference between Decorator & Adapter Design Pattern?
-- Adapter converts one interface to another, without adding additional functionalities\
-- Decorator adds new functionality into an existing interface.
What is the difference between Proxy & Adapter Design Pattern?
-- Adapter Design Pattern translates the interface for one class into a compatible but different interface.
-- Proxy provides the same but easy interface or some time act as the only wrapper.
Published at DZone with permission of Vishal Chovatiya. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments