Imagine you’re building an application that uses several third-party APIs, you will often find yourself in a situation where the interfaces exposed by those APIs are not compatible with interfaces defined within your application. To solve this problem, you need to create an intermediary class that acts as a bridge between your code and third-party APIs. This intermediary class acts as an adapter between two incompatible classes. In this tutorial, I will show you how to implement an adapter design pattern in the ASP.NET Core web application to facilitate the communication between two incompatible objects.
Table of Contents
What is Adapter Design Pattern?
The Adapter design pattern is one of the most common and useful structural design patterns. It allows objects with incompatible interfaces to collaborate with each other. This design pattern involves a single class called adapter which is responsible for communication between two independent or incompatible interfaces.
In the above image, on the right-hand side, we have a third-party Analytics library that consumes data in JSON format, and on the left-hand side, we have a client application that generates stock data in XML format. We can easily see that these two systems are incompatible and that’s why we have an XML to JSON Adapter in the middle that can facilitate the communication between these two systems.
The Adapter Design Pattern is composed of the following components:
- Client: A class that wants to consume an incompatible third-party application.
- Target: An interface that needs to be implemented by the Adapter and used by the client. The client class can see only this interface and don’t directly communicate with third party application.
- Adapter: A class that makes two incompatible systems work together. The Adapter class implements the Target interface also has the reference to the Adaptee object.
- Adaptee: A class that contains the functionality requires by the client but it’s not compatible with the existing client code. This class needs to be adopted by the adapter before the client can use it. In simple words, the client will call the Adapter and the Adapter will do the conversion if required and then it will make a call to the Adaptee.
Pros and Cons of Adapter Pattern
Pros | Cons |
---|---|
It encourages the Single Responsibility Principal because we can separate the conversion code from the primary business logic of the program. | Too many adapters make the system messy and difficult to understand and navigate as a whole |
It increases the transparency and reusability of the class because the specific implementation is encapsulated in the adapter class which is transparent to the client class. | |
We can introduce new types of adapters without breaking the existing client code. |
Adapter Pattern Implementation Techniques
The Adapter Design Pattern can be implemented in two ways
- Object Adapter
- Class Adapter
In this post, I will show you how to implement the Adapter pattern using both Object and Class adapter patterns with a real-world example.
Implementing Adapter Pattern using Object Adapter
Using this technique, the Adapter class implements the Target interface but doesn’t inherit the Adaptee class. The Adaptee object is passed into the constructor of the Adapter class. The Adapter class then perform all the conversions and used the Adaptee object to delegate all requests to Adaptee. The main advantage of implementing an adapter pattern using the Object Adapter technique is that you can implement (adapt) multiple adapters to the same target.
Let’s learn more about the Object Adapter pattern by implementing a complete example. In this example, I will create two separate classes which are not compatible with each other, and then we will introduce an adapter in the middle of these two classes to facilitate communication.
Step 1: Implementing Client
Create a new ASP.NET Core 5 MVC web application and add the following Employee class in the Models folder.
Employee.cs
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Salary { get; set; }
public Employee(int id, string name, decimal salary)
{
Id = id;
Name = name;
Salary = salary;
}
}
Create an interface and a service that will return the list of employees to our existing system.
IEmployeeService.cs
public interface IEmployeeService
{
List<Employee> GetEmployees();
}
For the demo purpose, let’s return a fake list of employees from the service.
EmployeeService.cs
public class EmployeeService : IEmployeeService
{
public List<Employee> GetEmployees()
{
return new List<Employee>()
{
new Employee(1, "James", 20000),
new Employee(2, "Peter", 30000),
new Employee(3, "David", 40000),
new Employee(4, "George", 50000)
};
}
}
Register the service with .NET Core DI Container by adding the following line in the ConfigureServices method of Startup.cs file.
services.AddScoped<IEmployeeService, EmployeeService>();
Inject the IEmployeeService in the constructor of the HomeController.cs file and call the GetEmployees method in the Index action method.
HomeController.cs
public class HomeController : Controller
{
private readonly IEmployeeService _employeesService;
public HomeController(IEmployeeService employeesService)
{
_employeesService = employeesService;
}
public IActionResult Index()
{
var employees = _employeesService.GetEmployees();
return View(employees);
}
}
Display the list of employees in the Index.cshtml razor view using the following HTML code snippet. Notice the HTML form in the code below that submits the employees to an MVC action SubmitEmployees. We will see this action method in action shortly.
Index.cshtml
@model IEnumerable<AdapterDesignPatternDemo.Models.Employee>
@{
ViewData["Title"] = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="col">
<h1>Employees</h1>
</div>
<div class="col text-right">
<form asp-action="SubmitEmployees" method="post">
<div class="form-group">
<input type="submit" value="Submit Employees" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<br/>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Id)
</th>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Salary)
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Id)
</td>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Salary)
</td>
</tr>
}
</tbody>
</table>
Run the project and you should a simple list of employees displayed on the page.
Step 2: Implementing Adaptee Object
Let’s assume that you need to submit the above list of employees to a third-party analytical service that wants to run generate some analytical reports using the employee’s data. Create the following interface and service that will act as an Adaptee to whom we want to send employees data from our application.
IAnalyticsService.cs
public interface IAnalyticsService
{
void GenerateReport(string json);
}
AnalyticsService.cs
public class AnalyticsService : IAnalyticsService
{
public void GenerateReport(string json)
{
// Process json to generate report.
}
}
You can immediately see the problem in the above service. The GenerateReport method of this so-called third-party analytical service needs employee data as JSON string but in your application, the employees data is available as a List<Employee>.
Register the service in Startup.cs file as follows:
services.AddScoped<IAnalyticsService, AnalyticsService>();
Let’s try to use the AnalyticsService in our HomeController by injecting the service into the constructor and then calling the GenerateReport method.
HomeController.cs
public class HomeController : Controller
{
private readonly IEmployeeService _employeesService;
private readonly IAnalyticsService _analyticsService;
public HomeController(IEmployeeService employeesService, IAnalyticsService analyticsService)
{
_employeesService = employeesService;
_analyticsService = analyticsService;
}
public IActionResult Index()
{
var employees = _employeesService.GetEmployees();
return View(employees);
}
[HttpPost]
public IActionResult SubmitEmployees()
{
var employees = _employeesService.GetEmployees();
_analyticsService.GenerateReport(employees);
return RedirectToAction("Index");
}
}
We should see the following error in the above code because clearly, we are passing the List<Employee> to GeneateReport method that expects employees as a JSON string.
We now have a real-world example of two incompatible classes which can’t communicate with each other. We are now ready to implement our Target interface and an intermediary adapter class to solve the above problem.
Step 3: Implementing Target Interface
Create the following IAnalyticsAdapter interface that will be implemented by the Adapter.
IAnalyticsAdapter.cs
public interface IAnalyticsAdapter
{
void ProcessEmployees(List<Employee> employees);
}
Step 4: Implementing Adapter
Create the following AnalyticsAdapter class that will implement the IAnalyticsAdapter interface defined above. This class also has a reference to our fictitious third-party service IAnalyticsService object. The ProcessEmploees method receives the employees as a List<Employee> and then serializes the list to a JSON string. Finally, it calls the same GenerateReport method on the IAnalyticsService object that we were calling from our HomeController (Client) above.
AnalyticsAdapter.cs
public class AnalyticsAdapter : IAnalyticsAdapter
{
private readonly IAnalyticsService _analyticsService;
public AnalyticsAdapter(IAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
public void ProcessEmployees(List<Employee> employees)
{
var json = System.Text.Json.JsonSerializer.Serialize(employees);
_analyticsService.GenerateReport(json);
}
}
Register the Adapter in the Startup.cs file using the following code.
services.AddScoped<IAnalyticsAdapter, AnalyticsAdapter>();
We need to modify the code of our HomeController to start using our adapter. We will not communicate with the incompatible IAnalyticsService directly that’s why we need to replace the IAnalyticsService with the IAnalyticsAdapter object as shown in the code below. We will pass the list of employees to the adapter ProcessEmployees method and this method will call the GenerateReport method of the IAnalyticsService internally.
HomeController.cs
public class HomeController : Controller
{
private readonly IEmployeeService _employeesService;
private readonly IAnalyticsAdapter _analyticsAdapter;
public HomeController(IEmployeeService employeesService, IAnalyticsAdapter analyticsAdapter)
{
_employeesService = employeesService;
_analyticsAdapter = analyticsAdapter;
}
public IActionResult Index()
{
var employees = _employeesService.GetEmployees();
return View(employees);
}
[HttpPost]
public IActionResult SubmitEmployees()
{
var employees = _employeesService.GetEmployees();
_analyticsAdapter.ProcessEmployees(employees);
return RedirectToAction("Index");
}
}
Implementing Adapter Pattern using Class Adapter
Using this technique, the Adapter class not only implements the Target interface but also inherits the Adaptee class. Since the adapter class becomes the subclass of Adaptee, there is no need to pass the Adaptee object into the constructor of the Adapter class. The Adapter can call any method of the Adaptee object directly. The one disadvantage of using this technique is that we can only inherit one Adaptee object at a time and this could be a problem if we are using the programming languages that do not support multiple inheritance such as Java or C#.
Let’s change our AnalyticsAdapter slightly to implement the adapter pattern using the Class Adapter technique.
AnalyticsAdapter.cs
public class AnalyticsAdapter : AnalyticsService, IAnalyticsAdapter
{
public void ProcessEmployees(List<Employee> employees)
{
var json = System.Text.Json.JsonSerializer.Serialize(employees);
GenerateReport(json);
}
}
In the above code, the AnalyticsAdapter is inheriting the AnalyticsService (Adaptee) directly. The GenerateReport method is also called directly because this method is defined in the parent AnalyticsService class. Everything else we implemented above will remain the same and there is no need to change any code in HomeController (Client).
You can download the complete source code of this post using the Download Source Code button shown above at the start of this post.
Conclusion
In the modern world of cloud computing and distributed applications, the Adapter pattern is a very commonly used pattern by C# and Java developers. We can use this pattern whenever we want to communicate with third-party libraries or systems which are not compatible with our system. I hope you have found this post useful. If you have any comments or suggestions, please leave your comments below. Don’t forget to share this tutorial with your friends or community.
What are Design Patterns in Java?
Design patterns are proven solutions for solving certain problems faced during the development of the software. It represents best practices used by software developers. Design patterns have been obtained by trial and error that software developers did over an abundant period.