Composition Layer
Let’s say you have two services: a remote service that fetches data from a server, and a local service that retrieves data from local storage. In this scenario, the goal is to ensure that users see the latest data if there is an internet connection. However, if there isn’t, they can still access the data stored locally on their device. To achieve this, we need to come up with composition layers, focusing on the separation of concerns and achieving the desired behavior:
- Remote Service(RemoteItemsLoader): This layer encapsulates the functionality to fetch data from a remote service. It handles network requests, communicates with the remote API, and retrieves the most updated data. It provides a method, such as
fetchItems(completion:)
, which retrieves data from the remote service asynchronously. - Local Service(LocalItemsLoader): This layer handles the retrieval of cached data from the local storage. It abstracts the details of accessing the local cache and provides a method, such as
fetchCachedItems(completion:)
, to retrieve the cached data asynchronously. - Data Fetcher(RemoteItemsLoaderWithLocalFallbackService): This composition layer coordinates the remote and local services to provide the required behavior. It is responsible for deciding whether to fetch data from the remote service or use the local cached data based on the availability of a network connection.
- When data is requested, the Data Fetcher first checks if a network connection is available.
- If a network connection is available, the Data Fetcher calls the
fetchItems(completion:)
method on the Remote Service to retrieve the most updated data from the remote source. - If there is no network connection, the Data Fetcher falls back to the
fetchCachedItems(completion:)
method on the Local Service to retrieve the cached data from the local storage. - The Data Fetcher then returns the fetched items (either from the remote service or the local cache) or any errors to the caller through completion handlers or appropriate result types.
By separating the responsibilities into composition layers, you achieve a clear separation of concerns:
- The Remote Service focuses on fetching data from the remote source, handling network communication, and retrieving the most updated data.
- The Local Service focuses on retrieving data from the local cache, abstracting the details of local storage access.
- The Data Fetcher acts as the orchestrator, deciding whether to use the remote service or the local cache based on network availability.
This composition-based approach allows for easy extensibility and modularity. You can easily swap out different implementations of the Remote Service and Local Service without affecting the Data Fetcher or other parts of the code. This adheres to the principles of separation of concerns, modularity, and encapsulation.
class RemoteItemsLoader {
// Remote items loader implementation
func fetchItems(completion:)
}
class LocalItemsLoader {
// Local items loader implementation
func fetchCachedItems(completion:)
}
class RemoteItemsLoaderWithLocalFallbackService {
private let remoteLoader: RemoteItemsLoader
private let localLoader: LocalItemsLoader
init(remoteLoader: RemoteItemsLoader, localLoader: LocalItemsLoader) {
self.remoteLoader = remoteLoader
self.localLoader = localLoader
}
func loadItems() -> [Item] {
if isNetworkAvailable() {
return remoteLoader.fetchItems()
} else {
return localLoader.fetchCachedItems()
}
}
private func isNetworkAvailable() -> Bool {
// Check if network is available
// Implement your network availability check logic here
return true
}
}
In the above code, we have the RemoteItemsLoader
and LocalItemsLoader
classes, which represent the remote and local item loaders. The RemoteItemsLoaderWithLocalFallbackService
class composes these two loaders and provides a loadItems()
method to retrieve the items.
During initialization, you inject instances of RemoteItemsLoader
and LocalItemsLoader
into the RemoteItemsLoaderWithLocalFallbackService
. This allows you to easily switch between different implementations without modifying the code that uses RemoteItemsLoaderWithLocalFallbackService
, adhering to the Open/Closed principle.
In the loadItems()
method, the network availability is checked using the isNetworkAvailable()
method. If the network is available, it calls the fetchItems()
method on the remoteLoader
instance to retrieve the items. Otherwise, it calls the fetchCachedItems()
method on the localLoader
instance to get the items.
With this composition-based approach, you can easily swap out different implementations of RemoteItemsLoader
and LocalItemsLoader
without modifying the code that uses RemoteItemsLoaderWithLocalFallbackService
. This promotes the Open-Closed principle by allowing extension and customization of behavior without directly modifying existing code.
Links:
- https://cs.nyu.edu/~jcf/classes/g22.2440-001_sp06/slides/session8/g22_2440_001_c82.pdf
- http://www.javier8a.com/itc/bd1/articulo.pdf
I would like to thank Caio & Mark and the entire EssentialDeveloper community. 🎉