On the previous article, we already learn about how to create a UseCase. In this article, we are going to learn how we can composes use case and create new one that has different functionality, using Composite Reuse Principle.

Let’s visit the previous article example.

The previous example, we have a LoadTransactionsUseCase protocol to get a general picture of a use case. This protocol, does not care wether the transactions is being loaded from Local or Remote data.

protocol LoadTransactionsUseCase {

	func execute(completion: @escaping (Result<[Transaction], Error>) -> Void)

}

Then, comes the concrete type or implementation of this use case, named LoadTransactionsFromCacheUseCase.

class LoadTransactionsFromCacheUseCase: LoadTransactionsUseCase {

	private let store: TransactionCacheStore
	
	init(store: TransactionCacheStore) {

		self.store = store
	}
	
	func execute(completion: @escaping (Result<[Transaction], Error>) -> Void) {

		store.loadTransactions { result in
			switch result {
				case .success(let transactions):
					completion(.success(transactions))
				case .failure(let error):
					completion(.failure(error))
			}
		}
	}
}

This class is an implementation of the LoadTransactionsUseCase protocol, that communicates with the abstract store that we now it is using cache, or we can call it as local data.

What if the user wants to load from a Remote first then load from cache? If we change directly in LoadTransactionsFromCacheUseCase implementation class, we will break the class first, add logic, and retest again. Furthermore, the class’ name needs to change.

Composite Reuse Principle

We can minimize the changes by creating new types to accomodate the requirement without changing the old implementation class.

First, we create a remote implementation for this protocol. This concrete class is talking to the remote service, the network, to ask the transactions data.

class LoadTransactionsFromRemoteUseCase: LoadTransactionsUseCase {

	private let service: TransactionService
	
	init(service: TransactionService) {

		self.service = service
	}
	
	func execute(completion: @escaping (Result<[Transaction], Error>) -> Void) {

		service.loadTransactions { result in
			switch result {
				case .success(let transactions):
					completion(.success(transactions))
				case .failure(let error):
					completion(.failure(error))
			}
		}
	}
}

Then, we can take the benefit of the Composite Reuse Principle, to enable the load from network first then load from local functionality, by creating a new type. Let’s call it LoadTransactionsFromRemoteWithLocalFallbackUseCase

assumed that we have NetworkReachabilityManager protocol that is implemented by a custom class or 3rd party framework.

protocol NetworkReachabilityManager {

	func isOnline() -> Bool

}

Then, we can create a new type and inject all of the dependencies :

class LoadTransactionsFromRemoteWithLocalFallbackUseCase: LoadTransactionsUseCase {

	private let remoteUseCase: LoadTransactionsUseCase
	private let localUseCase: LoadTransactionsUseCase
	private let networkReachailityManager: NetworkReachabilityManager
	
	init(remoteUseCase: LoadTransactionsUseCase, localUseCase: LoadTransactionsUseCase, networkReachabilityManager: NetworkReachabilityManager) {

		self.remoteUseCase = remoteUseCase
		self.localUseCase = localUseCase
		self.networkReachabilityManager = NetworkReachabilityManager
	}
	
	func execute(completion: @escaping (Result<[Transaction], Error>) -> Void) {

		let isOnline = networkReachabilityManager.isOnline()
		
		if isOnline {
			// using `remoteUseCase` when online
			// Of course we can implify the closure, but that is not the goal here.
			remoteUseCase.execute { result in
				switch result {
					case .success(let transactions):
						completion(.success(transactions))
					case .failure(let error):
						completion(.failure(error))
				}
			}	
			
		} else {

			// using `localUseCase` when offline
			// Of course we can implify the closure, but that is not the goal here.
			localUseCase.execute { result in
				switch result {
					case .success(let transactions):
						completion(.success(transactions))
					case .failure(let error):
						completion(.failure(error))
				}
			}			
		}
	}
}

This new type conforms to the same abstraction, the LoadTransactionsUseCase. This enables client to just replacingthe instantiation easily.

What if in the same app but different screen, we need to implement load transaction from local first, then remote?

We can achieve this by creating new type again, without breaking the current available types, especially if it is still being used by another client.

LoadTransactionsFromLocalWithRemoteFallbackUseCase

class LoadTransactionsFromLocalWithRemoteFallbackUseCase: LoadTransactionsUseCase {

	private let localUseCase: LoadTransactionsUseCase
	private let remoteUseCase: LoadTransactionsUseCase
	private let networkReachailityManager: NetworkReachabilityManager
	
	init(localUseCase: LoadTransactionsUseCase, remoteUseCase: LoadTransactionsUseCase, networkReachabilityManager: NetworkReachabilityManager) {

		self.localUseCase = localUseCase
		self.remoteUseCase = remoteUseCase
		self.networkReachabilityManager = NetworkReachabilityManager
	}
	
	func execute(completion: @escaping (Result<[Transaction], Error>) -> Void) {

		let isOffline = !networkReachabilityManager.isOnline()
		
		if isOffline {
			// using `localUseCase` when offline
			// Of course we can implify the closure, but that is not the goal here.
			localUseCase.execute { result in
				switch result {
					case .success(let transactions):
						completion(.success(transactions))
					case .failure(let error):
						completion(.failure(error))
				}
			}	
			
		} else {

			// using `remoteUseCase` when online
			// Of course we can implify the closure, but that is not the goal here.
			remoteUseCase.execute { result in
				switch result {
					case .success(let transactions):
						completion(.success(transactions))
					case .failure(let error):
						completion(.failure(error))
				}
			}			
		}
	}
}

Client’s point of view


let remoteUseCase: LoadTransactionsUseCase = LoadTransactionsFromRemoteUseCase(service: URLSessionTransactionService())
let localUseCase: LoadTransactionsUseCase = LoadTransactionsFromLocalUseCase(cacheStore: CacheTransactionStore())
let loadTransactionsFromRemoteFirstUseCase: LoadTransactionsUseCase = LoadTransactionsFromRemoteWithLocalFallbackUseCase(remoteUseCase: remoteUseCase, localUseCase: localUseCase, networkReachabilityManager: getNetworkReachabilityManager()) // if we want to use remote first
let loadTransactionsFromLocalFirstUseCase: LoadTransactionsUseCase = LoadTransactionsFromLocalWithRemoteFallbackUseCase(localUseCase: localUseCase, remoteUseCase: remoteUseCase, networkReachabilityManager: getNetworkReachabilityManager()) // if we want to use local first


And of because of the using of the same protocol, we can easily replace the implementation from the client.

func createDefaultTransactionsUseCase() -> LoadTransactionsUseCase {

	let remoteUseCase: LoadTransactionsUseCase = LoadTransactionsFromRemoteUseCase(service: URLSessionTransactionService())
	let localUseCase: LoadTransactionsUseCase = LoadTransactionsFromLocalUseCase(cacheStore: CacheTransactionStore())
	let loadTransactionsFromRemoteFirstUseCase: LoadTransactionsUseCase = LoadTransactionsFromRemoteWithLocalFallbackUseCase(remoteUseCase: remoteUseCase, localUseCase: localUseCase, networkReachabilityManager: getNetworkReachabilityManager()) // if we want to use remote first
	return loadTransactionsFromRemoteFirstUseCase
}

// ...
let transactionsViewController = TransactionViewController(loadTransactionsUseCase: createDefaultTransactionsUseCase())

Conclusion

Creating a new functionality of a component can be achieved by modifying the component itself. But, we can also create new type that compose the old type, which mean to reuse it, to create another new behavior, using the composite reuse principle. In this case, we can breakdown a component into a smaller classes, test separately, easy to maintain, test, and more!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s