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.