Adapter in C++, the Right Way

Ant Wang
7 min readAug 26, 2023

--

Photo by Call Me Fred on Unsplash

Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.

Refactoring Guru

While the core definition of Adapter Design Pattern is relatively universal, the implementation of this design pattern can be interpreted in many ways. Generally speaking, anything that acts as a translator or converter that resolves the incompatibility of interfaces is considered an Adapter design. That would technically include a helper class or a helper functions that translates data types from one form.

For example, if you’re writing a diagnostics module that listens to data packets on a data distribution system (DDS) — a node that subscribes to a diagnostic topic, you would probably want to have a library implementation of the message types and use it internally within the module. To convert the external DDS representation to the internal library representation, you need some kind of translator function that could be as simple as copying the fields one by one from the DDS representation to your internal structure.

While the purpose of the translator achieves the purpose of the adapter design, it’s merely a utlity function and shouldn’t be considered a “design pattern”. The purpose of this article is to strictly implement and define the Adapter Design Pattern based on the learnings from Refactoring Guru.

I think to fully realize a well designed Adapter Design Pattern, not only does it need to handle the interface incompatibility, but there are two key elements that need to be fulfilled.

  • The adapter serves as the middle man to call external service for the client code: It’s main duty is simply relaying the service request and pass in the result, but it doesn’t do any of the heavy lifting. Think of a translator translating a conversation between an English speaking and a Chinese speaking person. The translator is relaying the messages back and forth between the two people and not adding any thought of his/her own. Otherwise, if the translator’s bias is introduced, the neutrality of the conversation is compromised.
  • The client code is should not feel the presence of the adapter: It shouldn’t matter for the client code whether an adapter is used or not. The client operation should remain the same and making calls to the incompatible service as if it’s making a regular call.

Before diving deeper into the example, let’s take a moment to look at the UML diagram from Refactoring Guru.

UML Diagram for the Adapter Design

The Client contains the business logic and depends on the interface which defines how external classes must follow in order to collaborate with the Client. The Service, usually an external or 3rd party library, that the client wants to use but has incompatible interface. The Adapter inherits the Client Interface and also contains a instance of the Service that the client wants to use. It might seem confusing as to why the Adapter must implement the client and contains a instance of Service, but it will all make sense after the example.

Photo by Lc on Unsplash

Since we are talking about Adapter Design Pattern, let’s try to implement a software version of an actual adapter we use in real life, a USB adapter. The purpose of the USB adapter is for incompatible types of storage devices to work with a USB reader, which is designed to only read USB devices.

Our USB reader in this case is our client code, and the USB device is the interface the client code implements. The client’s core function is to read information from the storage device, and for external devices to work with the client code, it must implement the interface, USB.

If we have a SD Card, the USB reader is not able to read the content of the SD Card directly unless we use a SDCard to USB adapter. In this case our external service, the SD Card, provides functionality that is not directly compatible with the Client Interface, and hence, the adapter will help us handle the incompatibility.

Let’s write this example in code:

In the spirit of OOP design, we will design the interfaces USB and SDCard both as types of DataStorage Class

#include <iostream>

class DataStorage {
public:
int data;
};

class USB : DataStorage {
public:
USB(int data) {
data_ = num;
}

int USB_Transmit() {
std::cout << "Transmit data from USB Device" << std::endl;
return data_;
}
};

class SDCard: DataStorage {
public:
SDCard(int data) {
data_ = data;
}

int SDCard_Transmit() {
std::cout << "Transmit data from SDCard Device" << std::endl;
return data_;
}
};

Our client, DataReader can be implemented like this. Notice how for the client to function, incoming data must be of type USB.

class DataReader{
public:
void ReadDataFromStorage(USB* usb_device){
int data = usb_device->USB_Transmit();
std::cout << "Received data " << data << std::endl;
}
};

And to make calls to our client, in our main function we can try to read both from the USB Device, and also from the SDCard Device.

int main() {
DataReader reader;

USB* usb = new USB(5);
reader.ReadDataFromStorage(usb);

SDCard* sdcard = new SDCard(10);
reader.ReadDataFromStorage(sdcard); // error due to incompatible Interface

return 0;
}

As expected, the DataReader can only read data from USB, and if we try to pass SDCard into the client, it will break the client code.

To fix this, we can implement our Adapter. The first step is to create an Adapter class and make it follow the Client Interface (USB) by inheriting it. Let’s start with just an empty shell.

class SDCard_USB_Adapter : public USB {

};

Next, we want to add a field to the adapter class to store a reference to the service object, which is the incompatible service we are trying to access, SDCard. We will pass in the SDCard reference to our Adapter during construction, and because there is no default constructor in the USB class implicitly generated, we will just pass a garbage value into the USB class. The Adapter class now becomes this:

class SDCard_USB_Adapter : public USB {
private:
SDCard* sdcard;
public:
SDCard_USB_Adapter(SDCard* sdcard) : USB(-1) {
this->sdcard = sdcard;
}
};

Finally, we can override the methods of the client interface in the adapter class. To do that, we want to make the methods in our client interface, USB, virtual as well.

class USB : public DataStorage {
public:
USB(int num) {
data = num;
}

virtual int USB_Transmit() {
std::cout << "Transmit data from USB Device" << std::endl;
return data;
}
};

class SDCard_USB_Adapter : public USB {
private:
SDCard* sdcard;
public:
SDCard_USB_Adapter(SDCard* sdcard) : USB(-1) {
this->sdcard = sdcard;
}
int USB_Transmit() override {
return sdcard->SDCard_Transmit();
};
};

The adapter should delegate most of the real work to the service reference stored, and handling only the interface or data format conversion. In the overridden USB_Transmit() method, it’s simply invoking a call to the SDCard method.

We didn’t make changes to the Client, DataReader, but with the USB adapter now implemented, we can let the Client read the incompatible data inside the SDCard.

int main() {
DataReader reader;
SDCard* sdcard = new SDCard(10);
USB* adapter = new SDCard_USB_Adapter(sdcard);
reader.ReadDataFromStorage(adapter);

return 0;
}

// Output:
// Transmit data from SDCard Device
// Received data 10

For the Client, it doesn’t know that the adapter is passed into its read function, ReadDataFromStorage (other than the fact that the adapter is named “adapter”). The read function treats the passed in object just as a normal USB pointer. This is only possible because we had inherited the Client Interface in our Adapter class, so the Client does realize the presence of the adapter and it just works as usual.

Additional, even if the content of the SDCard object, sdcard, changes, because the Adapter holds a reference to the sdcard, the DataReader Client can still read the most up-to-date content inside the sdcard.

int main() {
DataReader reader;
SDCard* sdcard = new SDCard(10);
USB* adapter = new SDCard_USB_Adapter(sdcard);
reader.ReadDataFromStorage(usb2);

// Added a setter function to SDCard class
sdcard->SetData(20);
reader.ReadDataFromStorage(usb2);

return 0;
}

// Output:
// Transmit data from SDCard Device
// Received data 10
// Transmit data from SDCard Device
// Received data 20

Another thing to note here is because we’re working with C++ that supports multiple inheritance, we can also inherit the Service class rather than having an instance of it.

class SDCard_USB_Adapter : public USB, public SDCard {

...

public:
int USB_Transmit() override {
return SDCard_Transmit();
};
};

In the overridden method USB_Transmit, we can directly call the service method SDCard_Transmit() instead of invoking through a sdcard object sdcard->SDCard_Transmit().

Going back to the two key elements that makes a well designed Adapter design pattern:

  • The adapter serves as the middle man to call external service for the client code.
  • The client code is should not feel the presence of the adapter:

To achieve the first key element, we are able to accomplish it by having a reference of or directly inheriting (multiple inheritance only) the incompatible service class. We wrapped the service inside the adapter so when the client call is invoked, the adapter simply calls the service method using the service instance. No actual heavy lifting is being done by the adapter.

To achieve the second key element, we are able to accomplish it by inheriting the client interface and overloading the client method in the adapter implementation. We’re able to leverage run time polymorphism and make the client code compatible with the incompatible service, even without the client realizing the adapter’s presence.

--

--

Ant Wang

Self taught software developer passionate for robotics and industrial automation.