The Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.
The Adapter attempts to reconcile the differences between two otherwise incompatible interfaces or classes. In doing so, it wraps one of the classes in a layer that allows it to talk to the other.
You can find the example code of this post, on GitHub
Conceptualizing the Problem
Imagine that we're creating a stock market monitoring app. The app downloads the financial data from multiple endpoints in XML format processes them, and then displays them in pretty-looking charts and diagrams to the end user.
At some point, we decided to improve the app by integrating a 3rd-party visualization library. But there's a catch: the visualization library only works with data in JSON format.
We could change the library to work with XML. However, this might break some existing code that relies on the library. Moreover, while the library is open-source, getting a copy and changing the library's code to support XML is an exercise in futility (I have seen this practice more than once though...)
What we can do, is to create an adapter. An adapter is a special object that converts the interface of one object so that another object can communicate with it.
The adapter wraps one of the objects to hide the complexity of conversion happening under the hood. The wrapped object isn't even aware of the adapter. For example, we can wrap an object that operates in grams and celsius degrees with an adapter that converts all of the data to units such as pounds and Fahrenheit degrees.
Adapters can not only convert data into various formats but can also help objects with different interfaces collaborate.
Let's get back to our stock market app. To solve the problem of incompatible formats, we can create an XML-to-JSON adapter for every class of the analytics library that our code works with directly. Then we have to adjust our code to communicate with the library only via these adapters. When an adapter receives a call, it translates the incoming XML data into a JSON object and passes the call to the appropriate methods of a wrapped analytics object.
Structuring the Adapter Pattern
The adapter pattern comes in two flavours: the Object adapter and the Class adapter.
Object adapter
This implementation of the adapter pattern uses the object composition principle: the adapter implements the interface on one object and wraps the other.
In this implementation, the Object Adapter has four participants:
- Client: The Client is a class containing the program's existing business logic. The client code doesn't get coupled to the concrete adapter class as long as it works with the adapter via the client interface. Thanks to this, we can introduce new types of adapters into the program without breaking the existing client code. This can be used when the interface of the service class gets changed or replaced. We can just create a new adapter class without changing the client code.
- Client Interface: The Client Interface describes the protocol that other classes must follow to collaborate with the client code.
- Service: The Service is some class (usually 3rd-party or legacy). The client can't use this class directly because it has an incompatible interface.
- Adapter: The Adapter is a class that works with both the client and the service: it implements the client interface while wrapping the service object. The adapter receives calls from the client via the adapter interface and translates them into calls to the wrapped service object in a format it can understand.
Class adapter
This implementation uses inheritance: the adapter inherits interfaces from both objects at the same time. Note that this approach can only be implemented in languages that support multiple inheritance.
In this implementation, the Class Adapter doesn't wrap any object. It inherits the behaviours from both the client and the service, and the adaptation happens within the overridden methods. The resulting adapter can be used in place of an existing client class.
To demonstrate how the Adapter pattern works, we will create a meat-safe-cooking temperature database.
For this exampe, we have an old, legacy system which stores the temperature data. This legacy system will be represented by the MeatsDatabase
and will be our Service participant. Such a system might look like the following:
namespace Adapter.Legacy
{
public enum TemperatureType
{
Fahrenheit,
Celsius
}
/// <summary>
/// The legacy API must be converted to the new structure
/// </summary>
public class MeatsDatabase
{
public float GetSafeCookingTemperature(string meat)
{
return meat.ToLower() switch
{
"beef" or "pork" => 145f,
"chicken" or "turkey" => 165f,
_ => 165f,
};
}
public int GetCaloriesPerOunce(string meat)
{
return meat.ToLower() switch
{
"beef" => 71,
"pork" => 69,
"chicken" => 66,
"turkey" => 38,
_ => 0,
};
}
public double GetProteinPerOunce(string meat)
{
return meat.ToLower() switch
{
"beef" => 7.33f,
"pork" => 7.67f,
"chicken" => 8.57f,
"turkey" => 8.5f,
_ => 0f,
};
}
}
}
Now let's create a Meats
class
namespace Adapter
{
/// <summary>
/// The new Meats class, which represents details
/// about a particular kind of meat.
/// </summary>
public class Meats
{
protected string MeatName;
protected double SafeCookingTemperatureFahrenheit;
protected double SafeCookingTemperatureCelsius;
protected double CaloriesPerOunce;
protected double CaloriesPerGram;
protected double ProteinPerOunce;
protected double ProteinPerGram;
public Meats(string meatName)
{
this.MeatName = meatName;
}
public virtual void LoadData()
{
Console.WriteLine($"\nMeat: {MeatName} ------");
}
}
}
The problem is that we cannot modify the legacy API, which is the MeatDatabase
class. Here's where our Adapter participant comes into play: we need another class that inherits from Meat
but maintains a reference to the API such that the API's data can be loaded into an instance of the Meat
class:
using Adapter.Legacy;
namespace Adapter
{
/// <summary>
/// The Adapter class, which wraps the Meats class and
/// initializes that class's values.
/// </summary>
public class MeatDetails : Meats
{
private MeatsDatabase meatsDatabase;
public MeatDetails(string name) : base(name)
{
}
public override void LoadData()
{
meatsDatabase = new MeatsDatabase();
SafeCookingTemperatureFahrenheit = meatsDatabase.GetSafeCookingTemperature(MeatName);
SafeCookingTemperatureCelsius = FahrenheitToCelsius(SafeCookingTemperatureFahrenheit);
CaloriesPerOunce = meatsDatabase.GetCaloriesPerOunce(MeatName);
CaloriesPerGram = PoundsToGrams(CaloriesPerOunce);
ProteinPerOunce = meatsDatabase.GetProteinPerOunce(MeatName);
ProteinPerGram = PoundsToGrams(ProteinPerOunce);
base.LoadData();
Console.WriteLine($" Safe Cooking Temperature (Fahrenheit): {SafeCookingTemperatureFahrenheit}");
Console.WriteLine($" Safe Cooking Temperature (Celcius): {SafeCookingTemperatureCelsius}");
Console.WriteLine($" Calories per Ounce: {CaloriesPerOunce}");
Console.WriteLine($" Calories per Gram: {CaloriesPerGram}");
Console.WriteLine($" Protein per Ounce: {ProteinPerOunce}");
Console.WriteLine($" Protein per Gram: {ProteinPerGram}");
}
private double FahrenheitToCelsius(double fahrenheit)
{
return (fahrenheit - 32) * 0.55555;
}
private double PoundsToGrams(double pounds)
{
return pounds * 0.0283 / 1000;
}
}
}
Finally, in our Main()
method, we can now show the difference between using the legacy class by itself and using the Adapter class:
using Adapter;
// Non-adapted
Meats unknown = new Meats("Beef");
unknown.LoadData();
// Adapted
MeatDetails beef = new MeatDetails("Beef");
beef.LoadData();
MeatDetails chicken = new MeatDetails("Chicken");
chicken.LoadData();
MeatDetails turkey = new MeatDetails("Turkey");
turkey.LoadData();
The output of our application will be the following:
Pros and Cons of Adapter Pattern
✔ We can separate the interface or data conversion code from the primary business logic of the program, thus satisfying the Single Responsibility Principle | ❌ The overall complexity of the code increases because we 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. |
✔ We can introduce new types of adapters into the program without breaking the existing client code, as long as they work with the adapters through the client interface, thus satisfying the Open/Closed Principle |
Relations with Other Patterns
- The Bridge is usually designed up-front, letting us develop parts of an application independently of each other. On the other hand, rhe Adapter is commonly used with an existing application to make some otherwise-incompatible classes work together nicely.
- The Adapter changes the interface of an existing object, while the Decorator enhances an object without changing its interface. In addition, rhe Decorator supports recursive composition, which isn't possible when we use the Adapter.
- The Adapter provides a different interface to the wrapped object, the Proxy provides it with the same interface, and the Decorator provides it with an enhanced interface.
- Bridge, State, Strategy and to some degree Adapter have very similar structures. Indeed, all of these patterns are based on composition, which is delegating work to other objects. However, they all solve different problems. A pattern isn't just a recipe for structuring our code in a specific way.
Final Thoughts
In this article, we have discussed what is the Adapter pattern, when to use it and what are the pros and cons of using this design pattern. We then examined what is a class adapter and what is an object adapter and how the Adapter pattern relates to other classic design patterns.
It's worth noting that the Adapter pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (0)