google / wire

Compile-time Dependency Injection for Go
Apache License 2.0
13.14k stars 625 forks source link

How to use wire.Bind() if concrete implementation is resolved at runtime? #398

Closed carlosafonso closed 11 months ago

carlosafonso commented 11 months ago

(Apologies if this has been discussed somewhere else but I have been unable to find an answer to this.)

I'm learning Wire and trying to use it in my Go apps. Assume, for example, that my app calls a Generative AI model to do something. (This is just an example; this is not specifically tied to Generative AI or LLMs but I believe they make for a good analogy.)

type GenAiModel interface {
    GenerateText() string
}

func DoSomething(GenAiModel model) string {
    return model.GenerateText()
}

As there are many Generative AI models out there, my app does not really care which one is used by the implementer as long as it implements GenerateText(), hence the dependency on an interface.

Let's also assume this very same app also provides two built-in implementations of this interface: GeminiModel wraps code which uses Google Gemini, and ChatGptModel does the same for Open AI's ChatGPT.

If I wanted to expose this as a CLI flag, or environment variable, the specific implementation would need to be resolved during injection. So, for example, the provider for the GenAiModel interface could be something like this:

type GenAiModelVendor string

func ProvideGenAiModelVendor() GenAiModelVendor {
    // This returns a GenAiModelVendor with a correct value as provided by the app user.
}

func ProvideGenAiModel(genAiVendor GenAiModelVendor) GenAiModel {
    // This returns `GeminiModel` if the vendor is `google`, or `ChatGptModel` if the vendor is `openai`.
}

And this works. However I understand this is not idiomatic, as we are returning an interface, not a concrete type.

I understand that wire.Bind() is used to bind interfaces to concrete implementations, however I'm not sure how it fits in my use case above.

Am I understanding Wire correctly here? Am I doing things in the completely wrong way?

bobvawter commented 11 months ago

You wouldn't use wire.Bind() in this case, since the concrete binding isn't known at compile-time. Having provider functions that return an interface type is how you'd solve for cases where the implementation is selected at runtime.

One design choice that I've found helpful in my non-trivial Wire-based projects is to have a *Config type that handles CLI bindings and which is amenable to being composed should you have many different packages that all have some degree of configurability. The *Config object is passed into the top-level injector function and subsequently made available to the provider stack. The use of a *Config instead of individual, per-knob binding types makes the interface between a CLI package like cobra less tedious and amenable to testing.

For example:

type Config struct {
  SomeParam string
  // ... more fields
}
func (c *Config) Bind(flags *pflag.FlagSet) { /* Attach flags to fields */ }
func (c *Config) Preflight() error { /* Set reasonable defaults */ }

func ProvideThing(cfg *Config) (Thing, error) {
  // May return a different implementation of the Thing interface based on flags.
}

type MyStack {
  Some Thing
  Other *Doohicky
}

func MyInjector(cfg *Config) (*MyStack, error) {
  panic(wire.Build(
    ProvideThing(),
    wire.Struct(new(MyStack), "*"),
    // Other bindings as usual
  ))
}

func main() {
  cfg := &Config{}
  cmd := &cobra.Command{
    RunE: func(*cobra.Command, []args) error {
      if err := cfg.Preflight(); err != nil {
        return err
      }
      stack, err := MyInjector(cfg)
      // Do other things
    }
  }
  cfg.Bind(cmd.Flags())
  cmd.Execute()
}

For a larger example: https://github.com/cockroachdb/cdc-sink/blob/master/internal/source/cdc/server/wire_gen.go

carlosafonso commented 11 months ago

Thank you Bob! Glad to hear we are doing pretty much the same, as I'm also relying on a *Config type to capture settings from the user (I also happen to be getting them from env vars, but the idea remains).