Skip to main content

Command Palette

Search for a command to run...

Swift: Composite Pattern

Published
7 min read

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()
  1. We had created an instance for both APIService and InMemoryService.
  2. Through the if let, we are checking whether the data from the APIService is 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 from APIService.loadData as nil.)
  3. If the data from the APIService is nil, then we are loading the data from the InMemoryService and then print statement "Data obtained from InMemory" is executed inside of it. Since we had mocked APIService.loadData as nil, In our case InMemoryService would 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()
  1. Through constructor injection, we are passing the instance of created APIService and InMemoryService.
  2. Through the if let, we are checking whether the data from the APIService is not nil and then print statement "Data obtained from Remote" is executed inside of it.
  3. If the data from the APIService is nil, then we are loading the data from the InMemoryService and then print statement "Data obtained from InMemory" is executed inside of it.
  4. For the Usage part, As we had provided constructor injection, we need to provide the APIService and inMemoryService while we initialize the ViewController. Therefore, a new instance of APIService and inMemoryService is created and provided to the ViewController.

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
    }
  }
}
  1. We are getting the primary and secondary service through constructor injection and saving it for future reference.
  2. Next we are conforming to the Service protocol by adding the method loadData. You might be wondering why we are conforming to Service protocol, this is because the FallbackService should also be treated in the same way as APIService and InMemoryService by the ViewController. This is what the composite pattern says to treat all the objects in the same way.
  3. Through the if let, we are checking whether the data from the primary is not nil and then returning the obtained data.
  4. If the data from the primary is nil, then we are loading the data from the secondary and then returning the obtained data from secondary.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()
  1. 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 APIService alone, InMemoryService alone (or) FallBackService.
  2. We are loading the data from the service which is passed through constructor injection and then print statement "Data obtained from Service" is executed.
  3. 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 requires primary and fallback while we initialize the FallbackService. Therefore, a new instance of APIService and inMemoryService is created and provided to the FallbackService.

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!!!