Design Patterns: The Adapter Pattern
The adapter pattern is a simple and very common pattern that is often used in object-oriented designs. The intent behind using the adapter pattern is to convert the interface of a class into another interface that the client expects, without having the vary the client. The need to do this often arises when one is utilizing a third party library that contains the desired functionality, but it does not implement the desired interface. This pattern makes it possible to achieve this by creating an adapter that sits between the class and the third party library. This is also common when dealing with legacy enterprise code that one does not desire to modify due to high risk of introducing defects in production, especially when the unit test coverage of legacy code is not that great. Adapter classes can be thought of as wrappers around the legacy code.
Real Life Example
Using the adapter pattern is analogous to using a power outlet adapter when on an electronic device in Japan or Europe. Both the power outlet and the device do not change. However, there is a new ‘adapter’ piece that sits in between that allows us to connect the two together.
Common Use With Legacy Applications and Unit Testing
A very powerful and common use of the adapter pattern appears often when interfacing with Legacy applications. In modern architectures, there is heavy emphasis on unit tests and code coverage, something that was not considered when many legacy applications were written. For that reason, lots of architects nowadays design their applications in away so that all new code is covered with unit tests. However, in order to avoid any dependencies on poorly unit tested legacy code, a series of adapter interfaces is configured between legacy code and modern code. This way, all newly introduce code features reside in their own libraries that expose interface to legacy code. Concrete implementations of the adapters sits between the two.
Example
In order to demonstrate the adapter pattern, the example below provide a simple library that performs two mathematical operations, addition and subtraction. However, these two functions are ‘void’ and return no values. Instead, they can be passed in an ‘out’ parameter where they store in the results of the calculations. For the sake of clarity, this class library is called ‘LegacyLibrary’. Below is an example of how the two functions are called and the calculations are performed in the Main function:
LegacyLibrary.cs
public class LegacyLibrary
{
public void Add(int x, int y, out int sum)
{
sum = x + y;
} public void Subtract(int x, int y, out int diff)
{
diff = x - y;
}
}
Program Main
public static void Main(string[] args)
{
int sum, diff, x = 4, y = 3;
var legacyLibrary = new LegacyLibrary();
legacyLibrary.Add(x, y, out sum);
Console.WriteLine("x + y = " + sum); legacyLibrary.Subtract(x, y, out diff);
Console.WriteLine("x - y = " + diff);
}
Assume a new requirement comes in to construct a new more modern library that exposes a different interface. This new interface takes in the two input parameters, but instead it returns the results of the calculation. This new library is to be called ‘NewLibrary’ and it implements the new required interface INewLibrary.cs. Since the pre-existing LegacyLibrary above already contains all the logic desired to perform the calculations, it should be utilized in order to avoid having to re-implement the Add and the Subtract functions. A naive solution to this requirement can be approached by directly creating an instance of LegacyLibrary within the NewLibrary to invoke it’s calculations.
INewLibrary.cs
public interface INewLibrary
{
int Add(int x, int y);
int Subtract(int x, int y);
}
NewLibrary.cs
public class NewLibrary : INewLibrary.cs
{
public LegacyLibrary LegacyLibrary { get; set; } public NewLibrary(LegacyLibrary legacyLibrary)
{
LegacyLibrary = legacyLibrary;
} public int Add(int x, int y)
{
int sum;
LegacyLibrary.Add(x, y, out sum); return sum;
} public int Subtract(int x, int y)
{
int diff;
LegacyLibrary.Subtract(x, y, out diff); return diff;
}
}
In the approach above, the newly constructed library seems to work as required. However, with this new approach, both LegacyLibrary and NewLibrary are tightly couple with one another. This creates a dependency between the NewLibrary and the legacy code. Any changes in legacy code or it’s interface could easily break the new library.
The Adapter Pattern to the Rescue
A better approach would be to create a new ‘intermediate’ adapter class that bridges the differences between LegacyLibrary and NewLibrary. The NewLibrary class can reside in the new assembly, while all concrete adapters are placed either in the same class as the LegacyLibrary if possible, or in a separate intermediate assembly. This way, if the interface of LegacyLibrary changes, our new assembly and it’s unit test are safe from breaking. All we need to do is update the adapter class with the new changes of the LegacyLibrary class. Multiple implementations of adapters can be defined. The following is the implementation of a ‘default’ adapter.
ILibraryAdapter.cs
public interface ILibraryAdapter
{
int Add(int x, int y);
int Subtract(int x, int y);
}
DefaultLibraryAdapter.cs
public class DefaultLibraryAdapter : ILibraryAdapter
{
public LegacyLibrary LegacyLibrary { get; set; } public DefaultLibraryAdapter(LegacyLibrary legacyLibrary)
{
LegacyLibrary = legacyLibrary;
} public int Add(int x, int y)
{
int sum;
LegacyLibrary.Add(x, y, out sum);
return sum;
} public int Subtract(int x, int y)
{
int diff;
LegacyLibrary.Subtract(x, y, out diff);
return diff;
}
}
In the implementation above, the interaction with LegacyLibrary is abstracted and it’s responsibility is moved to the adapter. This promotes a SOLID approach as the NewLibrary class is not concerned with the implementation of the adapter. All it’s concerns is to interact with the defined ILibraryAdapter interface. This approach helps with unit testing as the new adapter interface can be easily mocked. The following is the updated NewLibrary class.
NewLibrary.cs
public class NewLibrary
{
public ILibraryAdapter AdapterLibrary { get; set; } public NewLibrary(ILibraryAdapter adapterLibrary)
{
AdapterLibrary = adapterLibrary;
} public int Add(int x, int y)
{
return AdapterLibrary.Add(x, y);
} public int Subtract(int x, int y)
{
return AdapterLibrary.Subtract(x, y);
}
}
Program Main
public static void Main(string[] args)
{
int sum, diff, x = 4, y = 3;
var legacyLibrary = new LegacyLibrary();
var defaultLibraryAdapter = new DefaultLibraryAdapter(legacyLibrary);
var newLibrary = new NewLibrary(defaultLibraryAdapter);
sum = newLibrary.Add(x, y);
Console.WriteLine("x + y = " + sum); diff = newLibrary.Subtract(x, y);
Console.WriteLine("x - y = " + diff);
}
Adapters and Inversion of Control
It is worth mentioning that with the construction of several adapters that ‘glue’ the various components of the design, the creation of a new instance could become more complex and it could require several steps. This issue can be addressed by relying on an IoC container to separate the concern of the creation of an instance from the implementation.