This is the first post in the Structural Design Patterns series and in this blog post, we will discuss the Adapter Design Pattern and its implementation using the C# programming language.
The Adapter pattern is also known as the Wrapper Pattern and helps us to communicate between two incompatible interfaces and falls into the Structural Pattern category as defined by the Gang of Four (GoF). If you are new to design patterns, I would highly recommend you to read the introductory post “Introduction to Design Patterns“
What is Adapter Design Pattern?
The Adapter Design Pattern is a Structural Design Pattern that helps us to build an intermediate class (called adapter) that hides the incompatibility from a class or a service (called adaptee) that a client wants to consume.
In other words, the Adapter pattern helps us to design an intermediate class (adapter) that provides an interface to the client by mapping the incompatibility (parameters, method calls, etc) to a class or a service that the client cannot consume directly.
The pattern is also known as the Wrapper pattern as it wraps the functionality of one incompatible class and invokes it internally by providing an easy to consume interface for the client.
When to consider Adapter Design Pattern
Following are few of the scenarios where the Adapter pattern can be used.
- A class or a service exists but the client cannot consume it directly. Like for example, we have an OpenCV library written in C++, but the client is written using the C# language.
- A class or a service exists but does not have an interface that the client wants to consume. For example, a legacy service exposes only a method that returns a list of employees but the client is interested in getting employee details based only on employee-Id.
Adapter Pattern – UML
The major classes participating in the UML are as follows:
- Adaptee: This is the class that the client wants to use but it contains an incompatible operation that the client cannot consume directly.
- IAdapter: This is the interface that exposes an operation for the client, which its implementor class (Adapter) uses to call the incompatible operation of the Adaptee class. It is also known as the Target interface.
- Adapter: This is a concrete class which implements the IAdapter interface and call the incompatible operation of the Adaptee class
- Client: This is the consumer class of the Adaptee class incompatible operation via the IAdapter interface.
C# Implementation
The adapter design pattern can be implemented in two ways first is via inheritance when the Adapter class implements an interface and resolves the incompatibility. This way implementation is termed as Class Adapter Pattern.
The second way is via composition when the Adapter class creates the instance of the Adaptee class within itself and resolves the incompatibility. This way of implementation is termed as Object Adapter Pattern.
Use case description
Let us assume we have a legacy class called RecordServer with the following functionality:
- Maintains a list of employees
- Have a single function to return all the employees at once.
This class cannot be discarded and need to be reused, whereas the client wants to fetch employee by its employee-id only and it is not interested in fetching all employees at once.
Let us build up the solution using the Object Adapter Design pattern where the Adapter class uses composition to remove the incompatibility serve the required functionality to its client.
The artifacts for the patterns are follows:
- Employee.cs: This class represents an employee of the organization. This is the same data structure the RecordServer uses and the client is also interested in it.
- RecordServer.cs: This is the legacy system that maintains a list of all the employees and has a single functionality to return all employees at once. This class needs to be adapted and represents the adaptee class in the pattern.
- IEmployeeService: This interface represents the IAdapter class and exposes the functionality of querying an employee using the employee-id which the client requires.
- EmployeeService: This class implements the IEmployeeService and represents the adapter in the design pattern. This class creates the instance of RecordServer and serves the client by returning an employee based on the employee-Id.
- Program.cs: This class represents the client, who uses the IEmployeeService and fetches the employee based on the employee Id.
Use case implementation using C#
1. Employee.cs: The data structure that is common to both RecordServer and the client. The ToString() method is overridden to return employee details.
/// <summary> /// This class represents an employee of the organization /// </summary> public class Employee { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Department { get; set; } public override string ToString() { return $"Id #{Id} FirstName# {FirstName} LastName# {LastName} Department# {Department}"; } }
2. RecordServer.cs: The old legacy code, that has the list of employees and contains an incompatible GetEmployees() to return the list of all employees.
/// <summary> /// This class represents the Adaptee class. /// Assuming this is a legacy code and it maintains a list of employee and have only one /// functionality of return all the employees to the caller /// </summary> public class RecordServer { private List<Employee> _employees; public RecordServer() { InitializeEmployees(); } public List<Employee> GetEmployees() { return _employees; } private void InitializeEmployees() { _employees = new List<Employee> { new Employee { Id = 1001, FirstName = "Michael" , LastName = "Lawson", Department = "Management"}, new Employee { Id = 1002, FirstName = "Lindsay" , LastName = "Ferguson", Department = "Developer"}, new Employee { Id = 1003, FirstName = "Tobias" , LastName = "Funke", Department = "IT"}, new Employee { Id = 1004, FirstName = "Byron" , LastName = "Fields", Department = "IT"}, new Employee { Id = 1004, FirstName = "George" , LastName = "Edwards", Department = "Developer"} }; } }
3. IEmployeeService: The interface exposes GetEmployee(int employeeId), as per the clients requirement.
/// <summary> /// Represents the IAdapter /// This interface will allow the client to fetch an employee using employeeId /// </summary> public interface IEmployeeService { Employee GetEmployee(int employeeId); }
4. EmployeeService: The Adapter class, create the RecordServer (adaptee) instance fetches employees list from it and returns employee object based on the employee-Id.
/// <summary> /// Represents the Adapter class. /// This class create the instance of Adaptee class and serves the client via composition /// </summary> public class EmployeeService : IEmployeeService { #region Private member RecordServer recordServer; #endregion #region Constructor public EmployeeService() { recordServer = new RecordServer(); } #endregion #region IAdapter - Implemented Member /// <summary> /// This method fetches the list of employee from the record server /// and return the employee based on the employeeId /// </summary> /// <param name="employeeId"></param> /// <returns></returns> public Employee GetEmployee(int employeeId) { var allEmployees = recordServer.GetEmployees(); return allEmployees.FirstOrDefault(e => e.Id == employeeId); } #endregion }
5. Program.cs: This is the client application that uses the IEmployeeService to query the employee based on the employeeId it passes.
class Program { static void Main(string[] args) { IEmployeeService service = new EmployeeService(); var employee = service.GetEmployee(1001); PrintEmployeeDetails(employee); employee = service.GetEmployee(1004); PrintEmployeeDetails(employee); employee = service.GetEmployee(1020); PrintEmployeeDetails(employee); employee = service.GetEmployee(1002); PrintEmployeeDetails(employee); Console.Read(); } static void PrintEmployeeDetails(Employee employee) { if(employee != null) Console.WriteLine(employee); else Console.WriteLine("Employee not found"); } }
Output of the program
Advantages of Adapter Pattern
Few of the advantages of using the Adapter patterns are as follows:
- The use of inheritance brings flexibility into the code and new Adapter types can be introduced for client consumption.
- The pattern increases the reusability of the code by wrapping the existing code-base for new client consumption.
- The pattern helps to minimize code coupling as the client is aware of the interface (a contract that is agreed upon) and the inner implementation can be changed easily without breaking the client code.
- The pattern promotes the Open-Closed principle.
- The code can be unit tested via mocking.
Disadvantages of Adapter Pattern
Few of the disadvantages of the Adapter patterns are as follows:
- New interfaces and classes are introduced to wrap an exiting functionality, this contributes to the complexity of the code.
- Additional request forwarding is introduced between the client and the Adaptee class.
Conclusion
The Adapter Design pattern is commonly used to introduce intermediate interfaces to reuse existing functionality and making the functionality available to incompatible consumers. The pattern is easy to implement and comes really handy in day to day development.
I hope you find this post helpful, thanks for visiting. Cheers !!!
[Further Readings: How to use Blazor EditForm for Model Validation | How to add a new profile in Windows Terminal | How to easily Customize Global Settings in Windows Terminal | How to add Git Bash to Windows Terminal Application | How to customize Windows Terminal Application | How to customize Windows Terminal Key Bindings | How to Install Windows Terminal in Windows 10 | Important Debugging Shortcuts of Visual Studio 2019 | How to publish a Blazor Server Application to IIS | Top 7 Visual Studio 2019 extensions for Web Projects | The difference in Blazor Server and WebAssembly Application | Exploring Blazor WebAssembly App Project Structure ]