‘Adapter’ Pattern in Swift

Romain Brunie
Level Up Coding
Published in
6 min readJan 1, 2021

Definition

‘Adapter’ pattern is a structural design pattern that is useful for composing classes and objects into a larger system.

The ‘Adapter’ pattern allows two objects, with related functionalities, to work together, even when they have incompatible interfaces.

When should we use this pattern?

To let objects, with incompatible interfaces, work together

This pattern should be used when we need two objects to work together, even when those objects have different interfaces. An object does not know what method to use from the other object interface when it is different from the one expected. The ‘Adapter’ pattern fixes this problem by exposing an object that conforms to the needed interface with the existing code that needs adapting. Moving the complexity of conversion to an interface simplifies the use of its functionalities. This object, called an adapter, hides the complexity of conversion.

To integrate components with similar functionalities

This pattern should be used when two components, with different interfaces, but similar functionalities, have to work together. If the components’ functionalities are different, it will add, remove or alter their behaviors which is not the goal of the ‘Adapter’ pattern. Its role is to convert one interface into another, so it makes interfaces interoperable. We do not want to force the integration of a component that does not provide the functionality intended by the interface for which it is being adapted.

⚠️ If it is not a one to one conversion, then it does not relate to the ‘Adapter’ pattern, but the ‘Facade’ pattern.

To decouple an object from the implemented interface

This pattern should be used when we have two unrelated interfaces that should communicate with each other and when the target/needed interface changes over time. To decouple implementation details from the client code, reduce dependencies, and protect our code from API changes, we use a middleman. It is an object that converts requests from the existing interface with the one needed. It encapsulates code changes so the client does not have to be modified. Without modifying the implementation of the code, we extend the behavior. It makes our client open for extension but closed for modification (Open/closed Principle).

How should we use this pattern?

When two objects with different interfaces need to communicate, we have two options. We could change existing code to work with the new interface. However, it is not a valid solution if the interface changes again in the future because it would be painful for our codebase. It would imply a complex set of changes which is error-prone. The code using the new interface might not even be accessible (3rd party library), making this approach impossible. This last common problem brings us to the second approach when we cannot modify the source code in our application. It consists of creating adapters that convert the old interface to the new one.

Composition over Inheritance

There are 2 kinds of adapters: class adapters and object adapters.

  • Class adapters use inheritance:
simage from Design Patterns — Elements of Reusable Object-Oriented Software by the Gang of Four

Since Swift does not support multiple inheritance, this kind of adapter cannot be implemented.

  • Object adapters use composition:
image from Design Patterns — Elements of Reusable Object-Oriented Software by the Gang of Four

Since Swift supports conformance to multiple protocols, we can implement this adapter. In this UML diagram, the client has a reference to an object that conforms to the Target interface. However, the client cannot communicates with the Adaptee object because it has a different interface than the one the client expects. The client uses the request method and the adaptee object uses the specificRequest method. The interfaces do not match and the adapter pattern helps to make them compatible.

To do so, we create an Adapter, also called a wrapper, in between these two interfaces. The Adapter conforms to the Target interface and has a reference to the Adaptee object. The client can keep using its request method and the Adapter object delegates all requests to the Adaptee. The specificRequest method is used without the client knowing it.

Concrete example

Let’s say we have a stock market monitoring app. Our app displays charts and diagrams of stock data which are downloaded from a Stock Data Provider in XML format. We want to improve our app by using a library that displays charts from collated and analyzed data. However, this library expects stock data in the JSON format.

image from https://refactoring.guru/design-patterns/adapter

We want to integrate a new component (the Analytics Library) that has a similar function than the Application (display charts) but with a different interface (JSON inputs), so the adapter pattern is the right one to use. Our object adapter will conform to a Core Class interface from the Application that display charts. It will have a reference to the Analytic library. Our object will then convert data from XML to JSON format and use its method. The use of the Analytics Library will become transparent to the Application.

image from https://refactoring.guru/design-patterns/adapter

Implementation

Let’s start with the implementation of the Stock Data Provider which return XML data.

class StockDataProvider {
func downloadStockData() -> XML {
return "XML data"
}
}

We then implement the Chart Core class which is part of the Application. It conforms to the Chart protocol in order to display charts. It has no reference to the Analytics Library because it cannot be used yet.

protocol Chart {
func displayCharts(data: XML)
}
class ChartCoreClass: Chart {
func displayCharts(data: XML) {
print("display charts with \(data)")
}
}

Let’s then introduce the Analytics Library that takes JSON inputs.

class AnalyticsLibrary {
func displayAnalyzedCharts(data: JSON){
print("display charts with \(data) analyzed")
}
}

Finally, we can create our object adapter that conforms to the Chart interface. We also add a reference to the Analytics Library because the object will convert data in order to use the library API. In this example, we assume that the library is instantiatable, so we use dependency injection through the constructor.

class XMLtoJSONAdapater: Chart {
let analytics: AnalyticsLibrary
init(analytics: AnalyticsLibrary) {
self.analytics = analytics
}
func displayCharts(data: XML) {
// Data conversion
let XMLtoJSONData = "converted \(data) to JSON data"
analytics.displayAnalyzedCharts(data: XMLtoJSONData)
}
}

Run code in a Playground

Here is an Online Swift Playground so an Xcode Playground does not have to be created in order to test this implementation of the ‘Adapter’ pattern. Then, copy the code below that corresponds with the full implementation of the ‘Adapter’ pattern for our stock market monitoring app.

typealias XML = String
typealias JSON = String
typealias AnalyzedData = String
class StockDataProvider {
func downloadStockData() -> XML {
return "XML data"
}
}
protocol Chart {
func displayCharts(data: XML)
}
class ChartCoreClass: Chart {
func displayCharts(data: XML) {
print("display charts with \(data)")
}
}
class AnalyticsLibrary {
func displayAnalyzedCharts(data: JSON){
print("display charts with \(data) analyzed")
}
}
class XMLtoJSONAdapater: Chart {
let analytics: AnalyticsLibrary
init(analytics: AnalyticsLibrary) {
self.analytics = analytics
}
func displayCharts(data: XML) {
// Data conversion
let XMLtoJSONData = "converted \(data) to JSON data"
analytics.displayAnalyzedCharts(data: XMLtoJSONData)
}
}
// Client (Application)
let provider = StockDataProvider()
let XMLData = provider.downloadStockData()
print("--- Client without adapter ---")
let client = ChartCoreClass()
client.displayCharts(data: XMLData)
print("--- Client with adapter ---")
let analyticsLib = AnalyticsLibrary()
let adaptedClient = XMLtoJSONAdapater(analytics: analyticsLib)
adaptedClient.displayCharts(data: XMLData)

Finally, paste and run the code.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Written by Romain Brunie

Passionate about Clean Code and Software Craftsmanship @AVIV

No responses yet

What are your thoughts?