What is the Adapter Design Pattern?
The Adapter design pattern is a structural pattern that allows two incompatible interfaces to work together. It creates a wrapper class that converts the interface of one class into the interface expected by the client. This allows classes that couldn’t otherwise work together due to incompatible interfaces to work together by wrapping an adapter class around the class with the incompatible interface. The adapter pattern is often used in software development to make existing classes work with others without modifying their source code.
Imagine you have a toy robot that only speaks Chinese, but you want it to speak English. The robot and you don’t understand each other because you speak different languages. An adapter is like a translator that helps you and the robot understand each other. The translator (adapter) speaks Chinese to the robot and English to you, so you can communicate and play with the robot together.
Another example could be that you have a new video game console and your old TV doesn’t have the right port to connect it to. The adapter is like a special cord that you can use to connect the new console to the old TV, so you can play the games on the old TV even though it isn’t designed to work with the new console.
It is a way to connect things that are not originally designed to connect.
Here is an example of the Adapter pattern in C#:
// The 'Target' interface
interface ITarget
{
void Request();
}
// The 'Adaptee' class
class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("Specific request.");
}
}
// The 'Adapter' class
class Adapter : ITarget
{
private Adaptee _adaptee;
public Adapter(Adaptee adaptee)
{
this._adaptee = adaptee;
}
public void Request()
{
_adaptee.SpecificRequest();
}
}
Here we have an ITarget interface with a Request() method, and an Adaptee class with a SpecificRequest() method. The Adaptee class represents an existing class that we want to use, but it has an incompatible interface. We create an Adapter class that implements the ITarget interface and takes an instance of the Adaptee class in its constructor. The Adapter class’s Request() method simply calls the SpecificRequest() method of the Adaptee class, allowing it to work with the ITarget interface.
class Program
{
static void Main(string[] args)
{
Adaptee adaptee = new Adaptee();
ITarget target = new Adapter(adaptee);
target.Request();
}
}
In the Main method, we create an instance of the Adaptee class and an instance of the Adapter class. We pass the Adaptee instance to the Adapter’s constructor, and then call the Request() method on the Adapter instance. This causes the SpecificRequest() method of the Adaptee class to be called, allowing us to use the Adaptee class through the ITarget interface.
This output to the console: “Specific request.”
This is a simple example for demonstration purposes, but the Adapter pattern can be applied to more complex situations where classes have multiple methods with different signatures, and more complex logic to make the incompatible interfaces to work together.
Let’s say you’re building a financial application that needs to integrate with different banking systems. Each bank has its own API for accessing account information and performing transactions. The problem is that each bank’s API has a different interface and uses different data formats.
To solve this problem, you can use the Adapter pattern by creating a set of Adapter classes, one for each bank. Each Adapter class will implement a common interface (e.g. IBankAdapter) that your application can use to interact with the different bank APIs. Each Adapter class will act as a bridge between your application and the specific bank API, converting the data and method calls to the format that the bank API expects, and translating the responses back to the format that your application understands.
interface IBankAdapter
{
Account GetAccountInformation(string accountNumber);
bool TransferFunds(string fromAccount, string toAccount, decimal amount);
}
class BankAAdapter : IBankAdapter
{
// Implementation of methods using the Bank A API
}
class BankBAdapter : IBankAdapter
{
// Implementation of methods using the Bank B API
}
class BankCAdapter : IBankAdapter
{
// Implementation of methods using the Bank C API
}
In this scenario, the Adapter pattern allows your application to work with multiple banking systems, even though they have different interfaces and data formats, by creating a common interface that your application can use and a set of Adapter classes that handle the communication with each bank’s API.
This pattern can be very useful in situations where you need to integrate with legacy systems or third-party APIs that have different interfaces and data formats. By creating Adapter classes, you can isolate the rest of your application from the details of the underlying systems, making it more flexible and easier to maintain.
In my previous examples, I used a class named “Adaptee” as an example of a class that has an incompatible interface. However, it is not a mandatory class to have in the Adapter pattern. The Adaptee class can be any class that the adapter needs to adapt to. The main idea of the Adapter pattern is to create a class (the adapter) that will be able to work with the client using the interface that the client is expecting and internally it will use the methods of the adaptee.
The adaptee can be an existing class in your codebase, a third-party library, or an external system. The important thing is that the adaptee has a different interface than what the client needs. The adapter class will implement the required interface and internally it will use the adaptee methods to fulfill the client’s request.
In summary, the Adapter pattern helps in situations where you have an existing class with a specific interface, but you need to use it in a different way that it was not designed for. The Adapter class will allow you to interact with the adaptee using the interface that you need, without modifying the adaptee code.
Here’s an example of the Adapter pattern in C#, where we have a class named “LegacyRectangle” that has an incompatible interface with the one we need:
// The 'Adaptee' class
class LegacyRectangle
{
public int X1 { get; set; }
public int Y1 { get; set; }
public int X2 { get; set; }
public int Y2 { get; set; }
public void Draw()
{
Console.WriteLine("Drawing a rectangle from ({0},{1}) to ({2},{3})", X1, Y1, X2, Y2);
}
}
// The 'ITarget' interface
interface IRectangle
{
int Width { get; }
int Height { get; }
void Draw();
}
// The 'Adapter' class
class RectangleAdapter : LegacyRectangle, IRectangle
{
public int Width
{
get { return X2 - X1; }
}
public int Height
{
get { return Y2 - Y1; }
}
public void Draw()
{
base.Draw();
}
}
Here we have an interface “IRectangle” that defines the interface that we want to use, and a class “LegacyRectangle” that has the incompatible interface. The “LegacyRectangle” class has X1, Y1, X2 and Y2 properties and a Draw method that draw a rectangle using these coordinates.
The “RectangleAdapter” class is the adapter class that implements the “IRectangle” interface and inherit from the “LegacyRectangle” class. This class provides the Width and Height properties that the client expect and it will use the base class properties to calculate the width and height. The Draw method is overridden to call the base class Draw method, this way the client can call the Draw method as it expects.
Now we can use the adapter class in the following way:
class Program
{
static void Main(string[] args)
{
IRectangle rectangle = new RectangleAdapter {X1 = 0, Y1 = 0, X2 = 10, Y2 = 5};
rectangle.Draw();
Console.WriteLine("Width: {0}", rectangle.Width);
Console.WriteLine("Height: {0}", rectangle.Height);
}
}
In this example, we create an instance of the adapter class “RectangleAdapter” and assign the values for X1, Y1, X2 and Y2 properties. We can use it as a regular “IRectangle” object and call the Draw method and the Width and Height properties as expected. Internally, the adapter class will use the LegacyRectangle class properties to calculate the width and height, and the Draw method to draw the rectangle.
In this way, the Adapter pattern allows us to use the existing class “LegacyRectangle” with an incompatible interface, in a way that our client expects, without modifying its code.
Conclusion
The Adapter pattern is a design pattern that allows you to use existing classes with incompatible interfaces in a way that is compatible with your application. The pattern consists of creating a new class, called the Adapter, that implements the interface required by the client and uses the methods of the existing class, called the Adaptee, to fulfill the client’s requests.
The Adapter pattern is useful in situations where you need to integrate with legacy systems, third-party APIs, or existing classes that have different interfaces and data formats. By creating an Adapter class, you can isolate the rest of your application from the details of the underlying systems, making it more flexible and easier to maintain.
In summary, the Adapter pattern is a powerful tool that can help you to integrate different systems, classes or APIs with different interfaces, into a seamless and cohesive whole, by creating a common interface that your application can use and a set of Adapter classes that handle the communication with these classes.
Thanks for reading! If you found the article helpful, you can clap and follow. So you will notified of new articles.
# Reference
It was created with the help of ChatGPT AI.