Table of Contents
Explanation
The Adapter design pattern is a Structural design pattern. You can use the Adapter design pattern to make incompatible types compatible. This can be the case with objects that implement different interfaces, but also with two different classes. For example: converting JSON to XML or vice versa, supporting different database types, different kind of electrical sockets and so on.
The use cases and pitfalls down below are somewhat abstract, but I will use a specific example.
Use cases:
- You want to adapt an interface or object to something else.
- You want attributes to be interpreted differently.
- You want to make different objects/interfaces compatible.
Pitfalls:
- Unsuitable adaptations: of course you could use this pattern to adapt any object to another somehow, but it doesn’t mean it is the best solution.
- Added complexity to the code.
- The role of the Adapter must be clear.
C# Example
I see a lot of complex examples of this pattern, so I try to approach this as simple as possible.
An Electric Oven for the European market
Imagine you have a product for the European market that uses the metric system. In this example I’ve chosen an electric oven, that has a width, height and a maximum temperature.
// Oven.cs
/// <summary>
/// Objects that implement this follow the metric system:
/// (centimeters, celsius)
/// </summary>
public interface IMetricSystem {
public float GetHeightCentimeter();
public float GetWidthCentimeter();
public float GetMaxTempCelsius();
}
internal abstract class Oven
{
public float Height { get; set; }
public float Width { get; set; }
public float MaxTemp { get; set; }
public Oven()
{
if (this is not IMetricSystem)
{
throw new InvalidOperationException("Oven must implement IMetricSystem");
}
}
}
internal class EUOven : Oven, IMetricSystem
{
public EUOven(float width, float height, float mTemp)
{
Height = height;
Width = width;
MaxTemp = mTemp;
}
public float GetHeightCentimeter()
{
return Height;
}
public float GetWidthCentimeter()
{
return Width;
}
public float GetMaxTempCelsius()
{
return MaxTemp;
}
}
C#In the code sample above, we have an EUOven class that extends Oven and we defined an interface IMetricSystem to label that our EUOven class uses the metric system. Meanwhile, interface IMetricSystem, does nothing on itself, we use it to remind ourselves that it follows the metric system. This means we interpret the Height and Width attributes as centimeters and the MaxTemp as degrees Celsius.
EU Oven implementation
Let’s call the Oven that is sold to the European market “Nice Oven”, niceOven for a variable name.
// Program.cs
using System;
internal class Program
{
static void Main(string[] args)
{
try
{
EUOven niceOven = new EUOven(80f, 60f, 230f);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
C#A “Nice Oven” for the US market
Imagine that the “Nice Oven” product sells good on the European market and now we want to sell it on the US market. Instead of creating a new product, we want to adapt the “Nice Oven” to be in the units more easily interpretable for customers more acquainted with the imperial system. Here’s where the Adapter design pattern can help out.
The Height and Width need to be converted from centimeters to inches, and the MaxTemp from Celsius to Fahrenheit.
Adaptation
Instead of creating a USOven object that is factually the same oven but only uses a different unit system, we use a metric to imperial adapter class:
// MetricToImperialAdapter.cs
public interface IImperialSystem
{
public float GetHeightInch();
public float GetWidthInch();
public float GetMaxTempFahrenheit();
}
class MetricToImperialAdapter : IImperialSystem
{
IMetricSystem metricObject;
public MetricToImperialAdapter(IMetricSystem metricObject)
{
this.metricObject = metricObject;
}
public float GetHeightInch()
{
return CentimeterToInch(this.metricObject.GetHeightCentimeter());
}
public float GetWidthInch()
{
return CentimeterToInch(this.metricObject.GetWidthCentimeter());
}
public float GetMaxTempFahrenheit()
{
return CelsiusToFahrenheit(this.metricObject.GetMaxTempCelsius());
}
private float CentimeterToInch(float value)
{
return value * 0.393701f;
}
private float CelsiusToFahrenheit(float value)
{
return value * 9 / 5 + 32f;
}
public override string ToString()
{
return $"Width (inch): {GetWidthInch()}, " +
$"Height (inch): {GetHeightInch()}, " +
$"MaxTemp (Fahrenheit): {GetMaxTempFahrenheit()}";
}
}
C#The MetricToImperialAdapter class can convert any type that implements the IMetricSystem to a IImperialSystem. The original object stays untouched by the Adapter class.
The Adapter class implements the interface (IImperialSystem) that it wants to adapt to, and uses the interface as datatype (IMetricSystem) where it wants to adapt from.
We can alter Program.cs to the following:
// Program.cs altered to:
using System;
internal class Program
{
static void Main(string[] args)
{
try
{
EUOven niceOven = new EUOven(80f, 60f, 230f);
MetricToImperialAdapter adapter = new MetricToImperialAdapter(niceOven);
Console.WriteLine(adapter.ToString());
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
C## Output:
Width (inch): 31,496078, Height (inch): 23,622059, MaxTemp (Fahrenheit): 446
Conclusion
The Adapter design pattern uses an adapter class to adapt the adaptee to a target interface.
- Target Interface: IImperialSystem is the target interface, where the client/customer expects to work with.
- Adaptee: IMetricSystem is the existing interface that needed to be adapted. The EUOven implements the IMetricSystem interface and can therefore be seen as the adaptee.
- Adapter: The MetricToImperialAdapter class adapts the adaptee to the target interface.
Class diagram
References
Freeman, E., Bates, B., & Sierra, K. (2004). Head first design patterns.
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software.
Wikipedia contributors. (2024, 10 september). Software design pattern. Wikipedia. https://en.wikipedia.org/wiki/Software_design_pattern