Search code examples
swiftdependency-injectioninversion-of-controlioc-container

Better understanding of Dependency Injection - Resolving New Instances?


I have been working a job that requires me to focus on Dependency Injection. For posterity, I am using this in Swift/SwiftUI, though I believe my lack of understanding is more inherent in the concept than the language.

I have created a Dependency Injection container, which can be used to register and resolve types and components. As such;

protocol MyContainerProtocol {
    func register<Component>(type: Component.Type, component: Any)
    func resolve<Component>(type: Component.Type) -> Component?
}

final class MyContainer: MyContainerProtocol {
    
    static let shared = DependencyContainer()
    private init() { }
    
    var components: [String: Any] = [:]
    
    func register<Component>(type: Component.Type, component: Any) {
        components["\(type)"] = component
    }
    
    func resolve<Component>(type: Component.Type) -> Component? {
        return components["\(type)"] as? Component
    }
}

This will become relevant below, but I have a class in my project, named VideoProcessor;

class VideoProcessor: SomeProtocol {
    var codec: String
    var format: String

    init(codec: String, format: String) {
      self.codec = codec
      self.format = format
    }
}

Early on in the app's lifecycle, I am registering the component. For example;

let container = DependencyContainer.shared
container.register(type: VideoProcessor.self, component: VideoProcessor(codec: "H264", format: "MP4"))
...
let processor = container.resolve(type: VideoProcessor.self)!

My Confusion: What is being asked of me is to resolve an instance of a type, without having to construct it when registering. Effectively, I'm being asked to resolve a new instance of a registered type each time it is resolved. In my mind, this means my code would resemble something like;

let container = DependencyContainer.shared
container.register(type: VideoProcessor.self)
...
let processorA = container.resolve(type: VideoProcessor.self)!
processorA.codec = "H264"
processorA.format = "MP4"

let processorB = container.resolve(type: VideoProcessor.self)!
processorB.codec = "H265"
processorB.format = "MOV"

However, VideoProcessor has its own dependencies, leading me to be unsure how I would register a type.

I'm unsure if my issue exists in the way my Dependency Container is built, the way my classes are built, or if the question of what's being asked of me I'm just not understanding. Even looking at popular Swift libraries like Swinject or DIP, I don't entirely see what my Container is doing improperly (or if this is where the Factory method comes in).


Solution

  • You would need to add an extra register function.

    protocol MyContainerProtocol {
      func register<Component>(type: Component.Type, component: Any)
      func register<Component>(type: Component.Type, builder: @escaping (MyContainerProtocol) -> Component)
      func resolve<Component>(type: Component.Type) -> Component?
    }
    
    final class MyContainer: MyContainerProtocol {
      
      static let shared = MyContainer()
      private init() { }
      
      var components: [String: Any] = [:]
      
      func register<Component>(type: Component.Type, component: Any) {
        components["\(type)"] = component
      }
      
      func register<Component>(type: Component.Type, builder: @escaping (MyContainerProtocol) -> Component) {
        components["\(type)"] = builder
      }
      
      func resolve<Component>(type: Component.Type) -> Component? {
        if let singleton = components["\(type)"] as? Component {
          return singleton
        }
        
        if let builder = components["\(type)"] as? (MyContainerProtocol) -> Component {
          return builder(self)
        }
        
        return nil
      }
    }
    

    Then it would look like this at the call site:

    struct Animal {
      let type: String
      let id = UUID()
    }
    
    struct Person {
      let name: String
      let pet: Animal
      let id = UUID()
    }
    
    class ComplicatedNetworkStack {
      let id = UUID()
      /// so much stuff in here
    }
    
    MyContainer.shared.register(type: Animal.self) { _ in Animal(type: "Dog") }
    MyContainer.shared.register(type: Person.self) { container in
      Person(
        name: "Joe Dirt",
        pet: container.resolve(type: Animal.self)!
      )
    }
    
    MyContainer.shared.register(type: ComplicatedNetworkStack.self, component: ComplicatedNetworkStack())
    

    If you were to run that code in a playground and resolve Person and Animal a few times, you'll see the UUID's are all different, while ComplicatedNetworkStack's id is the same.