Swift: Composite Pattern
Composite Pattern is a structural design pattern, which provides a way to treat multiple objects the same way. This pattern is applicable when all the objects within the branching pattern need to be treated identically. This is achieved by using a common protocol that acts as a layer of abstraction So that the consumer does not know about the concrete type.
For Simplicity, Let's assume an example of loading data from API, as well as from In-memory.
Below we had created a class called InMemoryService to demonstrate the data loading from InMemory, inside it, we have a method loadData which returns empty data.
//InMemoryService.swift
class InMemoryService: Service {
func loadData() -> Data? {
return Data()
}
}
For Simplicity, we aren't going ahead with closures(completion handler) in the loadData method.
Next, similar to InMemoryService we had created a class called APIService to demonstrate the data loading from API, inside it, we have a method loadData. For demonstration purpose, in order to make API calls fail every time, we are always returning nil from this method.
//APIService.swift
class APIService {
func loadData() -> Data? {
// For demonstration purpose, inorder to make API Call's fail everytime, we hadn't used the URLSession Class.
return nil
}
}
Although we can invoke this APIService, InMemoryService from any class. For this example, we are creating a ViewController inwhich we would call the service and get the data.
// ViewController.swift
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
// 1
let apiService = APIService()
let inMemoryService = InMemoryService()
// 2
if let data = apiService.loadData() {
print("Data obtained from Remote", data)
} else {
// 3
let inMemoryData = inMemoryService.loadData()
print("Data obtained from InMemory", inMemoryData)
}
}
}
// USAGE
let viewController = ViewController()
viewController.loadViewIfNeeded()
- We had created an instance for both
APIServiceandInMemoryService. - Through the
if let, we are checking whether the data from theAPIServiceis not nil and then print statement "Data obtained from Remote" is executed inside of it.(In order to make sure first it loads from the API and then from InMemory Service, we are mocking the data obtained fromAPIService.loadDataas nil.) - If the data from the
APIServiceis nil, then we are loading the data from theInMemoryServiceand then print statement "Data obtained from InMemory" is executed inside of it. Since we had mockedAPIService.loadDataas nil, In our caseInMemoryServicewould be returning data every time.
As you could see the ViewController, now depends on both the concrete type. We can create the protocol Service with the same method which acts as an abstraction for both APIService & InMemoryService, So that the ViewController can treat both the Service as the same.
//Service.swift
protocol Service {
func loadData() -> Data?
}
From the above code you could see, This Service protocol has a method loadData and returns the value as optional Data. This protocol is then conformed by the APIService and InMemoryService. Below is the code in which both the APIService and InMemoryService conform to the Service protocol.
//InMemoryService.swift
class InMemoryService: Service {
func loadData() -> Data? {
return Data()
}
}
//APIService.swift
class APIService: Service {
func loadData() -> Data? {
// For simplicity purpose, inorder to mock API Call fails everytime, hadn't used the URLSession Class.
return nil
}
}
Next in the ViewController, we are making some changes as the protocol is added.
// ViewController.swift
class ViewController: UIViewController {
// 1
private let apiService: Service
private let inMemoryService: Service
init(apiService: Service, inMemoryService: Service) {
self.apiService = apiService
self.inMemoryService = inMemoryService
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// 2
if let data = apiService.loadData() {
print("Data obtained from Remote", data)
} else {
// 3
let inMemoryData = inMemoryService.loadData()
print("Data obtained from InMemory", inMemoryData)
}
}
}
// 4
// USAGE
let viewController = ViewController(apiService: APIService(), inMemoryService: InMemoryService())
viewController.loadViewIfNeeded()
- Through constructor injection, we are passing the instance of created
APIServiceandInMemoryService. - Through the
if let, we are checking whether the data from theAPIServiceis not nil and then print statement "Data obtained from Remote" is executed inside of it. - If the data from the APIService is nil, then we are loading the data from the
InMemoryServiceand then print statement "Data obtained from InMemory" is executed inside of it. - For the Usage part, As we had provided constructor injection, we need to provide the
APIServiceandinMemoryServicewhile we initialize theViewController. Therefore, a new instance ofAPIServiceandinMemoryServiceis created and provided to theViewController.
You could see that the ViewController even though after a layer of abstraction is added, it still somehow has an idea of APIService and InMemoryService through instance names and the code is rigid. This is where Composite comes in, you could create a FallbackService where this service doesn't need to know about the primary or the fallback. We also make sure that both primary and fallback have the Service Protocol as type so that it doesn't have concrete type dependency.
//FallbackService.swift
class FallbackService: Service {
// 1
private let primary: Service
private let fallback: Service
init(primary: Service, fallback: Service) {
self.primary = primary
self.fallback = fallback
}
// 2
func loadData() -> Data? {
// 3
if let data = primary.loadData() {
return data
} else {
// 4
let inMemoryData = fallback.loadData()
return inMemoryData
}
}
}
- We are getting the primary and secondary service through constructor injection and saving it for future reference.
- Next we are conforming to the
Serviceprotocol by adding the methodloadData. You might be wondering why we are conforming toServiceprotocol, this is because theFallbackServiceshould also be treated in the same way asAPIServiceandInMemoryServiceby the ViewController. This is what the composite pattern says to treat all the objects in the same way. - Through the
if let, we are checking whether the data from theprimaryis not nil and then returning the obtained data. - If the data from the
primaryis nil, then we are loading the data from thesecondaryand then returning the obtained data fromsecondary.loadData.
After adding FallbackService, Here's how the ViewController looks like
//ViewController.swift
class ViewController: UIViewController {
// 1
private let service: Service
init(service: Service) {
self.service = service
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// 2
let data = service.loadData()
print("Data obtained from Service", data)
}
}
// 3
//USAGE
let fallBackService = FallbackService(primary: APIService(), fallback: InMemoryService())
let viewController = ViewController(service: fallBackService)
viewController.loadViewIfNeeded()
- Through constructor injection, we are passing the instance of created Service, it can be of any concrete class which conforms to the Service protocol, either
APIServicealone,InMemoryServicealone (or)FallBackService. - We are loading the data from the
servicewhich is passed through constructor injection and then print statement "Data obtained from Service" is executed. - For the Usage part, As we had provided constructor injection, we need to provide the service for the
ViewController. It can be APIService, InMemoryService or a combination like FallbackService. In this case, we are going with FallbackService, fallback service requiresprimaryandfallbackwhile we initialize theFallbackService. Therefore, a new instance ofAPIServiceandinMemoryServiceis created and provided to theFallbackService.
We can also use the ViewController in the below ways:
//APIServiceAlone
let viewController = ViewController(service: APIService())
viewController.loadViewIfNeeded()
//InMemoryServiceAlone
let viewController = ViewController(service: InMemoryService())
viewController.loadViewIfNeeded()
//FallbackService
let fallBackService = FallbackService(primary: APIService(), fallback: InMemoryService())
let viewController = ViewController(service: fallBackService)
viewController.loadViewIfNeeded()
//Multiple API Fallback Service
let fallBackService = FallbackService(primary: APIService(), fallback: InMemoryService())
let multipleAPIFallbackService = FallbackService(primary: APIService(), fallback: fallBackService)
let viewController = ViewController(service: multipleAPIFallbackService)
viewController.loadViewIfNeeded()
In the Multiple API Fallback Service Scenario(APIService -> APIService -> InMemoryService), First, it would try to get the data from APIService, if it fails then it would try again to get data from APIService. If both of the attempt to load data from APIService fails i.e After failing to load data from APIService twice, it would fallback to InMemoryService.
Similarly, you can compose multiple layers of service. The composite pattern is applicable when all the objects within the branching pattern needs to be treated identically. That’s a wrap for the article. Hope you enjoyed reading it. Happy Coding!!!