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 some piece of my stack, that depends on UserStorage, I would do something along the lines:

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. The most flexible is to inject three AnyPublisher, so we can do whatever we want:

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 as properties.

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 as flexible as the original approach, but 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 higher ergonomics via .live.