hmlongco / Factory

A new approach to Container-Based Dependency Injection for Swift and SwiftUI.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

A tagging system for injecting collections of dependencies [Feature suggestion]

DoubleREW opened this issue · comments

Hi,
first of all thank you for this great project, reading its documentation it's a pleasure.

What about a type-safe tagging system for injecting collections of dependencies into our services? An use case could be to allow different modules to register factories into a pool of services without having to interact directly with an external service.

Example 1:
A Pipeline that requires an ordered pool of services of type Processor to process its workload.
Each module in our system registers a factory for its own implementation of the Processor protocol, tagging it with a well-known tag.
Then the Pipeline asks the Container to resolve all the services that have been tagged with the desired tag.

Example 2:
A FormBuilder that allows the user to create forms by selecting from a dynamic set of fields.
Each module in our system registers a factory for a particular Field type and tags it. Into the FormBuilder instance are injected all the services that the Container knows to have the requested tag.

Proposed solution:
The implementation must be type-safe, so we define a tag as a protocol with an associated type:

public protocol Tag<T> {
    associatedtype T
}

So we can declare our tags like the following:

struct PipelineProcessorTag : Tag {
    typealias T = Processor
}

struct FormFieldTag : Tag {
    typealias T = Field
}

With this approach, we can also take advantage of the Swift protocol extension for a syntax sugar:

extension Tag where Self == PipelineProcessorTag {
    static var pipelineProcessor: PipelineProcessorTag { PipelineProcessorTag() }
}

extension Tag where Self == FormFieldTag {
    static var formField: FormFieldTag { FormFieldTag() }
}

As usual, we then register our factories in the DI container:

extension Container {
  var myProcessor1: Factory<Processor> { self { ProcessorImpl1() } }
  var myProcessor2: Factory<Processor> { self { ProcessorImpl2() } }

  var myField1: Factory<Field> { self { NumberField() } }
  var myField2: Factory<Field> { self { TextField() } }
}

As a final step, we tag our factories with a well-known tag and a priority to define their order, or an optional alias to easily distinguish them:

extension Container : AutoRegistering {
  func autoRegister() {
    myProcessor1.tag(.pipelineProcessor, priority: 10)
    myProcessor2.tag(.pipelineProcessor, priority: 20)

    myField1.tag(.formField, alias: "number")
    myField2.tag(.formField, alias: "text")
  }
}

And then to retrieve the previously tagged services:

Container.shared.resolve(tagged: .pipelineProcessor) // Returns [Processor]
Container.shared.resolveAssociative(tagged: .formField) // Returns [String: Field] where the key is the alias

A proof of concept of the implementation is available on this fork: https://github.com/DoubleREW/Factory
It only works with unparametized factories.

Glad you're enjoying Factory. (And the documentation. ;) )

Something similar to this is kicking around over in Resolver, but I'm still not totally convinced of the general need for such a thing. Your associated type and sugar make things type safe, but also makes it a little clunky to setup.

I think I need to kick it around in my head for awhile and see what comes up.

Just noting that your code has a significant problem in that you're storing a factory in an object that belongs to a container. Factory's are designed to be short-lived entities for several reasons, but one of them is that they contain a hard reference to their container so they can resolve when needed.

In short there's a retain cycle there. You may need to consider something like the following with key paths...

    func autoRegister() {
         tag(\MyContainer.processor1, as: pipelineProcessor)
         tag(\MyContainer.processor2, as: pipelineProcessor)
    }

Thanks for the work, regardless.

Thanks for taking that into consideration.
I've updated the forked repository following your suggestion, the previous poc also leads to an unattended behavior if you tag the factory in its computed variable.

Then again, there's this...

extension Container {
    static var processors: [KeyPath<Container, Factory<Processor>>] = [
        \.processor1,
        \.processor2,
    ]

    func processors() -> [Processor] {
        Container.processors.map { self[keyPath: $0]() }
    }

    var processor1: Factory<Processor> { self { Processor(name: "processor #1") } }
    var processor2: Factory<Processor> { self { Processor(name: "processor #2") } }
}

Anyone is also free to add their own extensions and append to the array as needed.

Seems like a really clean and intuitive alternative to a tagging system, thanks.
Maybe you could add this snippet of code to the docs (if it's not already there), I know it's just pure and simple Swift code, but maybe it could point developers coming from other DI frameworks that offer a tagging system in the right direction. Thanks again.

I have the code for later consideration should I decide to implement this as a feature in Factory, but for now I think I'll go with the documentation suggestion and see if that covers most of the bases for most of the people.

Thanks for the suggestions.