Dependency Injection with Functions
This is the code structure I am using for Dash for loading, saving and deleting credentials from the keychain:
import Combine
import KeychainSwift
public protocol UserStorageProtocol {
func save(credentials: Credentials) -> AnyPublisher<Credentials, CoreError>
func load() -> AnyPublisher<Credentials, CoreError>
func delete() -> AnyPublisher<Void, Never>
}
public struct UserStorage: UserStorageProtocol {
private let keychain: KeychainSwift
private let credentialsKey = "credentials"
public init() {
/// ...
}
public func save(
credentials: Credentials
) -> AnyPublisher<Credentials, CoreError> {
/// ...
}
public func load()
-> AnyPublisher<Credentials, CoreError> {
/// ...
}
public func delete()
-> AnyPublisher<Void, Never> {
/// ...
}
}
If I want to test something that depends on UserStorage
, I could do this:
public struct MockUserStorage: UserStorageProtocol {
func save(
credentials: Credentials
) -> AnyPublisher<Credentials, CoreError> {
//...
}
func load(
) -> AnyPublisher<Credentials, CoreError> Published {
//...
}
func delete(
) -> AnyPublisher<Void, Never> {
//...
}
}
There are many ways to go about MockUserStorage
implementation. One approach is to inject three AnyPublisher
:
public struct MockUserStorage: UserStorageProtocol {
private let saving: AnyPublisher<Credentials, CoreError>
private let loading: AnyPublisher<Credentials, CoreError>
private let deleting: AnyPublisher<Void, Never>
init(
saving: AnyPublisher<Credentials, CoreError>,
loading: AnyPublisher<Credentials, CoreError>,
deleting: AnyPublisher<Void, Never>
) {
self.saving = saving
self.loading = loading
self.deleting = deleting
}
}
And the implementation:
public func save(
credentials: Credentials
) -> AnyPublisher<Credentials, CoreError> {
return saving
}
public func load(
) -> AnyPublisher<Credentials, CoreError> {
return loading
}
public func delete(
) -> AnyPublisher<Void, Never> {
return deleting
}
Another way of thinking about this problem, is to remove the protocol entirely and use functions instead:
public struct UserStorage {
var save: (Credentials) -> AnyPublisher<Credentials, CoreError> = { credentials in
fatalError("Not implemented")
}
var load: () -> AnyPublisher<Credentials, CoreError> = {
fatalError("Not implemented")
}
var delete: () -> AnyPublisher<Void, Never> = {
fatalError("Not implemented")
}
}
We also removed the original UserStorage
’s properties, we won’t need them anymore.
Now that we have the new UserStorage
’s structure, we add the following:
extension UserStorage {
static var live: UserStorage {
var userStorage = UserStorage()
/// We use `keychain` + `credentialsKey` as part of
/// the functions implementation
let keychain: KeychainSwift = // ...
let credentialsKey = "credentialsKey"
userStorage.save = { credentials -> AnyPublisher<Credentials, CoreError> in
/// real implementation
}
userStorage.load = { () -> AnyPublisher<Credentials, CoreError> in
/// real implementation
}
userStorage.delete = { () -> Never in
/// real implementation
}
return userStorage
}
}
From a consumer perspective, it’s quite ergonomic:
class ProfileViewModel {
init(
/// ...
userStorage: UserStorage = .live
) {
}
}
From a testing perspective, it’s more flexible than the original approach and with a lot less code. Notice that we only require the load
function. So no need provide an implementation for save
and delete
, like we had with the protocol approach.
let testingCredentials = /// ...
var userStorage = UserStorage()
userStorage.load = { () -> AnyPublisher<Credentials, CoreError> in
return Just(testingCredentials).eraseToAnyPublisher()
}
Approaching dependency injection with functions in mind, provides a more flexible way of structuring our entities. In the example above, we gained higher testing flexibility, less code with the removal of a protocol and improved ergonomics via .live
.