Flutter Design Patterns: 2 — Adapter
An overview of Adapter design pattern and its implementation in Dart and Flutter
In the last article, I have analysed the first design pattern in the series — Singleton, provided some general thoughts about its structure, applicability, pros and cons, implemented it in several different ways. This time, I would like to analyse and implement a design pattern which belongs to the category of structural design patterns — Adapter.
Table of Contents
- What is the Adapter design pattern?
- Other articles in this series
- Your Contribution
What is the Adapter design pattern?
Adapter is a structural design pattern, also known as wrapper. It is one of the most common and most useful design patterns available to us as software developers. The intention of this design pattern is described in the GoF book:
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
Let’s say you want to use some kind of a third-party library which has the functionality you need. Also, you have a class that needs to utilize a particular interface, but interfaces between your code and the library are incompatible. In this case, you can use the library’s code by creating an adapter class that “sits” between your code and the code you want to use from that library. Moreover, the pattern is useful when you want to ensure that some particular piece of code in your software could be reused by wrapping it with an adapter and exposing an interface to it. This lets the future implementations in your code to depend on Adapter interfaces, rather than concrete classes directly. Sounds good already? Well, let’s move to the analysis to understand how this pattern works and how it could be implemented.
The class diagram below shows the general structure of the Adapter design pattern.
To be more specific, there are two general implementations of Adapter with a different structure — Object and Class adapters (we will talk about the differences between these two next). Despite different structure between object and class adapters, they share the same idea of participants (elements or classes used to implement the pattern):
- Target or ITarget defines the domain-specific interface that Client uses;
- Client collaborates with objects conforming to the Target interface;
- Adaptee defines an existing interface that needs adapting (e.g. third-party library);
- Adapter adapts the interface of Adaptee to the Target interface. This is the main node in the pattern, which connects the Client code with the code which wants to be used (Adaptee).
Object adapter vs Class adapter
To begin with, both object and class adapters are valid implementations of the Adapter design pattern, but their structure is different as well as their advantages and trade-offs. Object adapter relies on object composition to implement a Target interface in terms of (by delegating to) an Adaptee object. That is, adapter implements the Target operation by calling a concrete operation on the property or instance of Adaptee class. The class adapter uses inheritance to implement a Target interface in terms of (by inheriting from) an Adaptee class. This way, the concrete operation from the Adaptee class could be called directly from the implementation of Target operation. The question is, which one to use?
My personal choice between these two possible implementations is object adapter. Here are the reasons why:
- In order to implement the Adapter design pattern using the class adapter method, the programming language of your choice must support multiple inheritance. In Dart, multiple inheritance is not supported.
- One of the advantages of class adapter is that you can easily override the behaviour of the adaptee class — you extend the adaptee class, right? However, the object adapter method is more flexible since it commits to an Adaptee class at run-time (client and adaptee code are loosely-coupled). It means that you can create a single adapter which could use multiple different adaptees as long as their interfaces (types) matches the one adapter requires.
- I prefer composition over inheritance. What I bear in my mind, if you try to reuse the code by inhering from a class, you make the subclass dependent on the parent class. When using composition, you decouple your code by providing interfaces which implementations can be easily replaced (in this case, the implementation of Adaptee could be replaced inside the Adapter class at run-time). This is only a glimpse of the Liskov substitution principle (the letter L in SOLID principles), which is pretty difficult to understand and apply, but it is worth the effort.
The adapter design pattern could (and should!) be used when the interface of the third-party library (or any other code you want to use) does not match the one you are currently using in your application. This rule could also be applied when calling external data sources, APIs and you want to wrap and separate the data conversion code from the primary business logic in your program. The pattern is useful when you want to use multiple implementations (adaptees) that have similar functionality but use different APIs. In this case, all the “hard work” (implementation) could be done in the Adapter classes, whereas the domain-layer code will use the same interface of the adapters. Also, this code abstraction makes the unit testing of the domain-layer code a little bit easier.
Let’s say, in the Flutter application we want to collect our contacts from two different sources. Unfortunately, these two sources provide the contacts in two different formats — JSON and XML. Also, we want to create a Flutter widget which represents this information in a list. However, to make the widget universal, it cannot be tied to a specific data format (JSON or XML), so it accepts these contacts as a list of Contact objects and does not know anything about how to parse JSON or XML strings to the required data structure. So we have two incompatible interfaces — our UI component (widget), which expects a list of Contact objects, and two APIs, which return contacts’ information in two different formats. As you could have guessed, we will use the Adapter design pattern to solve this problem.
The class diagram below shows the implementation of the Adapter design pattern using the object adapter method.
First of all, there are two APIs: JsonContactsApi and XmlContactsApi. These two APIs have different methods to return contacts information in two different formats — JSON and XML. Hence, two different adapters should be created to convert the specific contacts’ representation to the required format which is needed in the ContactsSection component (widget) — list of Contact objects. To unify the contract (interface) of adapters, IContactsAdapter abstract class is created which requires implementing (override) the getContacts() method in all the implementations of this abstract class. JsonContactsAdapter implements the IContactsAdapter, uses the JsonContactsApi to retrieve contacts information as a JSON string, then parses it to a list of Contact objects and returns it via getContacts() method. Accordingly, XmlContactsAdapter is implemented in the same manner, but it receives the data from XmlContactsApi in XML format.
Contact is a plain Dart class (as some people from Java background would call it — POJO) to store the contact’s information.
This class is used inside the UI widget ContactsSection and both of the adapters to return the parsed data from APIs in an acceptable format for the UI.
A fake API which returns contacts’ information as a JSON string.
A fake API which returns contacts’ information as an XML string.
A contract (interface) which unifies adapters and requires them to implement the method getContacts().
Dart language does not support the interface as a class type, so we define an interface by creating an abstract class and providing a method header (name, return type, parameters) without the default implementation.
An adapter, which implements the getContacts() method. Inside the method, contacts’ information is retrieved from JsonContactsApi as a JSON string and parsed to the required return type (a list of Contact objects).
An adapter, which implements the getContacts() method. Inside the method, contacts’ information is retrieved from XmlContactsApi as an XML string and parsed to the required return type (a list of Contact objects).
First of all, a markdown file is prepared and provided as a pattern’s description:
The example itself uses ContactsSection component which requires a specific adapter of type IContactsAdapter to be injected via constructor.
ContactsSection widget uses the injected adapter of type IContactsAdapter. The widget only cares about the adapter’s type (interface), but not its specific implementation. Hence, we can provide different adapters of type IContactsAdapter which load the contacts’ information from different data sources without making any changes to the UI.
The final result of the Adapter’s implementation looks like this:
All of the code changes for the Adapter design pattern and its example implementation could be found here.
Other articles in this series
👏 Press the clap button below to show your support and motivate me to write better!
💬 Leave a response to this article by providing your insights, comments or wishes for the series.
📢 Share this article with your friends, colleagues in social media.
➕ Follow me on Medium.
⭐ Star the Github repository.