‘Composite’ Pattern in Swift

Romain Brunie
Level Up Coding
Published in
5 min readJul 31, 2020

--

Definition

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

The ‘Composite’ pattern defines an interface for dealing with structures of objects uniformly. Structures are in the form of trees that contains individual objects and composition of objects.

When should we use this pattern?

To compose multiple objects through a unified interface

This pattern should be used when we have a collection of objects that we treat uniformly. The client does not know the types of objects it deals with and it implies that all objects should conform to the same interface. It makes the client code simple because the types of objects become transparent to the client.

To treat a composition of objects as one

This pattern should be used when we have a collection of objects with hierarchical relationships. The collection of objects forms a tree like structure with a hierarchy. The structure contains leaves (individual objects) and composites (composition of objects).

image from Design Patterns — Elements of Reusable Object-Oriented Software by the Gang of Four

In this structure, a composite object executes operations from its children. It means leaf objects define behaviors and composite objects delegate the work to their children. At the end, the client thinks it communicates with one object, even though behind the interface we can have multiple objects.

To decouple objects knowledge from the client

This pattern should be used when the client is aware of the types of objects it uses. By sharing an interface, objects do not need to be treated individually and it removes complexity from the application. The client does not need to check if it calls the right method on the right object: it calls the same method over an entire structure of objects. The ‘Composite’ pattern collects and manages objects for the client, which can work with any objects from the structure without any coupling.

How should we use this pattern?

A client communicates with a collection of objects through an interface (Component) representing operations executed by leaves and composites.

image from Design Patterns — Elements of Reusable Object-Oriented Software by the Gang of Four

Concrete example

Let’s say we have a mobile app that load data remotely or locally depending on the internet connection. Here is the UML diagram reflecting the structure above.

We can use any of our leaf objects (LocalLoader or RemoteLoader) to load data for our app. If we make a remote request with a bad internet connection, we want to anticipate any failing requests by loading local data. We create a composite object (CompositeFallbackLoader) in order to provide a fallback when remote data fail to load.

Implementation

protocol LoadData {
func load(completion: @escaping (Result<Data, Error>) -> Void)
}

In our example, we do not define the Add and Remove methods because we want to keep our composite object immutable by injecting individual objects in the constructor. We also do not use the getChild method since we do not have any use of it. Following the Interface Segregation Principle, we do not implement methods that our objects do not need.

If your use case needs those methods, we could throw exceptions as default implementations of our methods in order to force us to implement them. Leaves and composites play different roles, so it makes more sense if we do not write default implementations that do not make sense to an object. However, if the shared interface contains operations for both types of objects, we lose safety since an object might do something meaningless with some methods (for example, the Add method has no point being in a leaf). It is a design decision to separate responsibilities into interfaces. It would make the design safe because we will not call meaningless methods. However, we would lose transparency because the client needs to know the types of objects it communicates with. Depending on your needs, find the right balance between transparency and safety.

Leaf objects

class LoadRemoteData: LoadData {
func load(completion: @escaping (Result<Data, Error>) -> Void) {
completion(.failure(NSError(domain: "any error", code: 0)))
}
}
class LoadLocalData: LoadData {
func load(completion: @escaping (Result<Data, Error>) -> Void) {
completion(.success("'my local data'".data(using: .utf8)!))
}
}

Each of the individual objects conforms to the LoadData interface. In this example, we fake a remote request failure and a successful local data retrieval to show the use of the composite object.

Composite object

class CompositeFallbackLoader: LoadData {
let remote: LoadData
let local: LoadData
init(remote: LoadData, local: LoadData) {
self.remote = remote
self.local = local
}
func load(completion: @escaping (Result<Data, Error>) -> Void) {
remote.load(completion: { [weak self] result in
switch
result {
case .success:
print("fetch \(result) remotely")
case .failure:
self?.retrieveLocalData()
}
})
}
private func retrieveLocalData() {
local.load(completion: { localResult in
switch
localResult {
case let .success(data):
let myData = String(data: data, encoding: .utf8)!
print("fetch \(myData) locally")
case let .failure(error):
print(error)
}
})
}
}

The composite object conforms to the LoadData interface. It implements a fallback so when a remote request fails, data are retrieved locally.

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 ‘Composite’ pattern. Then, copy the code below that corresponds with the full implementation of the ‘Composite’ pattern for our mobile app showing a failing remote request with a fallback on local data.

import Foundationprotocol LoadData {
func load(completion: @escaping (Result<Data, Error>) -> Void)
}
class LoadRemoteData: LoadData {
func load(completion: @escaping (Result<Data, Error>) -> Void) {
completion(.failure(NSError(domain: "any error", code: 0)))
}
}
class LoadLocalData: LoadData {
func load(completion: @escaping (Result<Data, Error>) -> Void) {
completion(.success("'my local data'".data(using: .utf8)!))
}
}
class CompositeFallbackLoader: LoadData {
let remote: LoadData
let local: LoadData
init(remote: LoadData, local: LoadData) {
self.remote = remote
self.local = local
}
func load(completion: @escaping (Result<Data, Error>) -> Void) {
remote.load(completion: { [weak self] result in
switch
result {
case .success:
print("fetch \(result) remotely")
case .failure:
self?.retrieveLocalData()
}
})
}
private func retrieveLocalData() {
local.load(completion: { localResult in
switch
localResult {
case let .success(data):
let myData = String(data: data, encoding: .utf8)!
print("fetch \(myData) locally")
case let .failure(error):
print(error)
}
})
}
}
// Client
let composite = CompositeFallbackLoader(
remote: LoadRemoteData(),
local: LoadLocalData()
)
composite.load(completion: { result in
switch
result {
case let .success(data):
print(data)
case let .failure(error):
print(error)
}
})

Finally, paste and run the code.

--

--