Adapter design pattern
Amit Nadiger
Polyglot(Rust??, Move, C++, C, Kotlin, Java) Blockchain, Polkadot, UTXO, Substrate, Sui, Aptos, Solana, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Cas, Engineering management.
The Adapter design pattern is a structural pattern that solves the problem of making incompatible interfaces work together. It allows classes with incompatible interfaces to work together by creating a middleman class that acts as a translator or bridge between the two. The adapter class takes the interface of one class and adapts it to the interface that the client expects.
The adapter pattern is useful when you want to use an existing class that is not compatible with your system. Rather than modifying the original class, you can create an adapter that bridges the gap between the two systems. This pattern is used when you need to connect two systems that are incompatible.
Implementation of Adapter Pattern:
The Adapter pattern consists of three main components: the target interface, the adaptee, and the adapter. The target interface is the interface that the client code interacts with, and the adaptee is the class that needs to be adapted to work with the target interface. The adapter is the class that bridges the gap between the two.
Let’s take an example to understand how the adapter pattern works in C++. Suppose we have a legacy code that reads a file in a specific format, and we want to use this code to read a file in a different format. Instead of modifying the existing code, we can create an adapter that adapts the new file format to the format that the legacy code understands.
Here’s how we can implement the adapter pattern in C++:
The target interface is the interface that the client code interacts with. It defines the operations that the client code needs to perform. In our example, the target interface would define the operations to read a file.
class FileReader {
public:
virtual std::string read() = 0;
};
2. Create the Adaptee
The adaptee is the class that needs to be adapted to work with the target interface. In our example, the adaptee would be the class that reads the file in the new format.
class NewFileReader {
public:
std::string readNewFile() {
// read file in new format
return "contents of new file";
}
};
3. Create the Adapter
The adapter is the class that bridges the gap between the target interface and the adaptee. It implements the target interface and uses the adaptee to perform the required operations. In our example, the adapter would adapt the new file format to the format that the legacy code understands.
class NewFileAdapter : public FileReader {
public:
NewFileAdapter(NewFileReader* reader) : reader(reader) {
}
std::string read() {
std::string contents = reader->readNewFile();
// convert contents to old format
return contents + " (converted to old format)";
}
private:
NewFileReader* reader;
};
4. Use the Adapter
Now that we have created the adapter, we can use it to read the new file format using the legacy code that reads files in the old format.
int main() {
NewFileReader newReader;
NewFileAdapter adapter(&newReader);
std::string contents = adapter.read();
// use contents in old format
return 0;
}
In the above example, we have created a new file reader class that reads files in the new format. We have then created an adapter that adapts the new file format to the old file format. Finally, we have used the adapter to read the new file format using the legacy code that reads files in the old format.
Example1:
Let's say you have a legacy class that provides a method specificRequest(), but your client code expects a different interface with a method request(). You can create an adapter class that wraps the legacy class and provides the required interface:
// Legacy class with specific interface
class LegacyClass {
public:
? ? void specificRequest() {
? ? ? ? // Do something specific
? ? }
};
// Target interface
class TargetInterface {
public:
? ? virtual void request() = 0;
};
// Adapter class
class Adapter : public TargetInterface {
private:
? ? LegacyClass* m_legacy;
public:
? ? Adapter(LegacyClass* legacy) : m_legacy(legacy) {}
? ? void request() override {
? ? ? ? m_legacy->specificRequest();
? ? }
};
// Client code
int main() {
? ? LegacyClass legacy;
? ? Adapter adapter(&legacy);
? ? // Client code expects a TargetInterface
? ? TargetInterface* target = &adapter;
? ? target->request();
? ? return 0;
}
In this example, the LegacyClass provides a specific interface that cannot be used directly by the client code. The Adapter class wraps the LegacyClass and implements the TargetInterface expected by the client code. When the client code calls request() on the Adapter, the specificRequest() method of the LegacyClass is called internally.
领英推荐
Example 2;
Suppose you have a legacy codebase that uses an interface called OldInterface to interact with an external library, but you want to update the codebase to use a more modern interface called NewInterface. Unfortunately, the NewInterface has a different method signature than the OldInterface, so the codebase needs to be modified to use the new interface.
To do this, we can create an adapter class Adapter that implements the OldInterface and internally uses the NewInterface. The adapter class Adapter will translate the method calls from the OldInterface to the corresponding method calls on the NewInterface.
class OldInterface {
public:
? ? virtual void foo(int x) = 0;
};
class NewInterface {
public:
? ? virtual void bar(double y) = 0;
};
class NewClass : public NewInterface {
public:
? ? void bar(double y) override {
? ? ? ? std::cout << "NewClass::bar(" << y << ")" << std::endl;
? ? }
};
class Adapter : public OldInterface {
public:
? ? Adapter(std::shared_ptr<NewInterface> new_interface)
? ? ? ? : new_interface_(new_interface) {}
? ??
? ? void foo(int x) override {
? ? ? ? // call the bar method on the NewInterface
? ? ? ? double y = static_cast<double>(x);
? ? ? ? new_interface_->bar(y);
? ? }
? ??
private:
? ? std::shared_ptr<NewInterface> new_interface_;
};
int main() {
? ? auto new_class = std::make_shared<NewClass>();
? ? auto adapter = std::make_shared<Adapter>(new_class);
? ? // call foo method on the adapter, which internally calls the bar method on the NewInterface
? ? adapter->foo(42);
? ??
? ? return 0;
}
In this example, the NewClass implements the NewInterface and the Adapter implements the OldInterface. The Adapter internally uses the NewInterface to perform the necessary translation of method calls.
This adapter design pattern helps to decouple the legacy codebase from the external library, and allows the codebase to be updated to use the more modern interface without requiring significant changes to the existing code.
Example 3:
In below example, we have an Adaptee class FahrenheitSensor that provides temperature readings in Fahrenheit. We want to use this class in our system, but it requires temperatures to be in Celsius. Instead of modifying the FahrenheitSensor class, we can create an adapter class FahrenheitToCelsiusAdapter that implements the TemperatureSensor interface that our system uses. This adapter class takes a std::shared_ptr to an instance of the FahrenheitSensor class and converts the Fahrenheit temperature readings to Celsius temperature readings by implementing the getTemperature function.
We create an instance of the FahrenheitSensor class and pass it to the FahrenheitToCelsiusAdapter constructor to create an instance of the TemperatureSensor interface that our system uses. We then call the getTemperature function on the TemperatureSensor instance to get the temperature in Celsius.
This example shows how the adapter design pattern can be used to seamlessly integrate existing code into a new system without having to modify the existing code.
#include <iostream>
#include <memory>
#include <string>
// Adaptee class
class FahrenheitSensor {
public:
? float getFahrenheitTemp() {
? ? float t = 32.0;
? ? // read temperature from hardware
? ? return t;
? }
};
// Target interface
class TemperatureSensor {
public:
? virtual float getTemperature() = 0;
};
// Adapter class
class FahrenheitToCelsiusAdapter : public TemperatureSensor {
public:
? FahrenheitToCelsiusAdapter(std::shared_ptr<FahrenheitSensor> sensor)?
? ? ? : fahrenheit_sensor_(sensor) {
}
? float getTemperature() override {
? ? float fahrenheit_temp = fahrenheit_sensor_->getFahrenheitTemp();
? ? float celsius_temp = (fahrenheit_temp - 32.0) * 5.0 / 9.0;
? ? return celsius_temp;
? }
private:
? std::shared_ptr<FahrenheitSensor> fahrenheit_sensor_;
};
int main() {
? // create an instance of the Adaptee class
? std::shared_ptr<FahrenheitSensor> fahrenheit_sensor = std::make_shared<FahrenheitSensor>();
? // create an instance of the Adapter class and pass in the Adaptee instance
? std::shared_ptr<TemperatureSensor> temperature_sensor = std::make_shared<FahrenheitToCelsiusAdapter>(fahrenheit_sensor);
? // use the Adapter's interface to get the temperature
? float temperature = temperature_sensor->getTemperature();
? std::cout << "Temperature is: " << temperature << " degrees Celsius" << std::endl;
? return 0;
}
/*
Op =>
Temperature is: 0 degrees Celsius
*/
Kotlin version of above code:
import kotlin.math.*
// Adaptee class
class FahrenheitSensor {
? ? fun getFahrenheitTemp(): Float {
? ? ? ? var t = 32.0F
? ? ? ? // read temperature from hardware
? ? ? ? return t
? ? }
}
// Target interface
interface TemperatureSensor {
? ? fun getTemperature(): Float
}
// Adapter class
class FahrenheitToCelsiusAdapter(private val fahrenheitSensor: FahrenheitSensor) : TemperatureSensor {
? ? override fun getTemperature(): Float {
? ? ? ? val fahrenheitTemp = fahrenheitSensor.getFahrenheitTemp()
? ? ? ? val celsiusTemp = (fahrenheitTemp - 32.0) * 5.0 / 9.0
? ? ? ? return celsiusTemp.toFloat()
? ? }
}
fun main() {
? ? // create an instance of the Adaptee class
? ? val fahrenheitSensor = FahrenheitSensor()
? ? // create an instance of the Adapter class and pass in the Adaptee instance
? ? val temperatureSensor: TemperatureSensor = FahrenheitToCelsiusAdapter(fahrenheitSensor)
? ? // use the Adapter's interface to get the temperature
? ? val temperature = temperatureSensor.getTemperature()
? ? println("Temperature is: $temperature degrees Celsius")
}
/*
Op =>
Temperature is: 0.0 degrees Celsius
*/
The main advantage of the adapter design pattern is that it allows for the reuse of existing code. Instead of modifying existing code to make it compatible with a new system, an adapter class can be created to translate between the two. This can save time and reduce the risk of introducing new bugs into the codebase.
One disadvantage of the adapter design pattern is that it can introduce some overhead, as the adapter class needs to perform additional operations to translate between the two interfaces. Additionally, the adapter class can add complexity to the codebase, as it introduces another layer of abstraction.
Advantages:
Disadvantages:
Example scenarios:
The adapter design pattern is useful in situations where existing code needs to be reused with a new system or library that has a different interface. For example, if an application is designed to work with a specific database library, but a new library with a different interface is introduced, an adapter class can be used to translate between the two interfaces without having to modify the existing application code.
Another suitable scenario to use the adapter design pattern is when working with legacy code that cannot be easily modified. An adapter class can be created to adapt the legacy code to a new system without having to modify the existing codebase.
Conclusion
The Adapter pattern is a powerful pattern that can be used to connect two incompatible interfaces. It is useful when you want to use an existing class that is not compatible with your system. Rather than modifying the original class, you can create.