‘Adapter’ Pattern in Swift
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:
Since Swift does not support multiple inheritance, this kind of adapter cannot be implemented.
- Object adapters use composition:
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.
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.
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 = Stringclass 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.