Examples of how to use dry-container & dry-auto_inject

Hi,

I’d like to experiment with Dependency Injections. I have looked at dry-component and I think it does a bit too much for my needs. Do you have examples of dry-container & dry-auto_inject ?

I am struggling a bit with:

# set up your auto-injection function
AutoInject = Dry::AutoInject(my_container)

Which file to define this function? And when

Are there recommended ways to define Autoinject ? I can have a look to tests & implementations on Monday. And I’ll post a reply to this topic.

Thanks,

JM

I was able to write a proof of concept. But I am not very happy with the result.

Feedback is welcome :slight_smile: At that stage, I am very tempted to write some boilerplate code to inject my own dependencies, I think it will be more readable and feel less “magic”.

require 'dry-container'
require 'dry-auto_inject'

module Api
  def self.configure
    container.register :main_component, -> { MainComponent.new }
    container.register :dependency, -> { Dependency.new }
    container.freeze
  end

  def self.AutoInject(*keys)
    inject[*keys]
  end

  def self.try_dry_rb_container
    container.resolve(:main_component).hello_world
  end

  # private
  def self.container
    @container ||= Dry::Container.new
  end

  def self.inject
    # TODO this is probably not how tp set up your auto-injection function
    # 2 things:
    # 1. Syntax is surprising. I expected some kind of method call like Dry::AutoInject.make_or_build(container)
    # 2. Is there another way? Looking at the doc, it seems that I should define a module dynamically (?)
    # "module" somewhere else
    @inject ||= Dry::AutoInject(container)
  end
end

class MainComponent
  # TODO How does this work? I though only modules could be included in classes
  include Api::AutoInject(:dependency)

  def hello_world
    dependency.say_hi
  end
end

class Dependency
  def say_hi
    "Hi from dry-rb dry-container injected with dry-auto-inject"
  end
end

Api.configure
puts Api.try_dry_rb_container

Place where you can put that code depends on your application.

The main idea that it should be globally available to all classes that you want use it in.

For example, if it’s Rails application, I would put it into config/initializers/auto_inject.rb.
If it’s custom application, you can put it into the main file, above everything.

One downside of this that your classes become less portable because of this only one external dependency.
So I would put this only in the application-related classes only, not isolated and unrelated.

You’re right about include modules. You can include only modules. So, I guess how does it works, that Api::AutoInject(:dependency) module returns module itself, that’s ready for inclusion.

So, Dry::AutoInject(container) returns a hash, where values are modules (actually I believe there is lazy construction). Something like this Dry::AutoInject(container)[:dependency] will return module, that you can include.

Also one thing that you can do better, is to just assign Dry::AutoInject method inside API module:

module Api
  AutoInject = Dry::AutoInject(container)
end

and avoid that proxy method self.inject etc.

class MainComponent
  # TODO How does this work? I though only modules could be included in classes
  include Api::AutoInject[:dependency]
end

This function returns a module, so effectively you are including a module. It defines a constructor for you with default arguments set up by using a container that you configured. It uses a less known technique called module subclassing which allows you to create a module which has a state (in our case, attr names and containers is our state).

I should mention that was my deliberate decision as I simply wanted to remove boilerplate caused by manual DI. I really like the fact you specify dependencies in classes, it’s easy to see how things are wired up this way. I treat auto-inject more like a tool for importing objects into other objects.

If we used a complex container that knows how to build objects along with their dependencies you would still have a dependency on that container, which btw would be a bigger dependency than a small module which creates a constructor for you. So when you think about it, your classes wouldn’t be easily portable either, as they would need a container to initialize them.

Another thing is that this system relies on really basic protocols. ie our container implements basic Hash protocol, and auto-inject modules rely on it. This means that IF somebody wanted to port their classes, it wouldn’t be very painful, as the interfaces we rely on are very small. Even when you want to remove the whole auto-injection stuff, it’s still a relatively small amount of work, as you would just need to define attr readers and a constructor.

1 Like

Thank you. @Kukunin

I tried your approach and I got a <module:Api>: undefined local variable or method 'container' for Api:Module (NameError)

The fact I have a 2 steps process makes it impossible to initialize the AutoInject before runtime.

Anyway, I will give a try to these 2 projects and see how it goes. First step will be to convince my co-workers :wink:

This is the hardest part. In our community convincing people about the benefits of using DI is really hard due to our history with Rails and a very strong criticism of DI with rather ridiculous arguments that it’s not needed because you can monkey-patch. If we took a closer look at the results of monkey-patch-based solutions like Timecop or http-mocking libs, I’m pretty sure we’d clearly see it’s been giving more trouble than actual benefits in testing (brittle tests, unstable tests, less flexibility etc.). With DI you have an explicit way of defining dependencies and the fact it makes testing easier is a nice side-effect of this technique, but it’s not the reason to use it, as the actual reason is the reduction of coupling between individual components of your system.

Another problem is explaining why to use a container. I’ve heard many times an argument that it’s an unnecessary indirection. It surprises me especially when I hear this from really experienced developers. Tight coupling between your code and concrete classes and modules from 3rd party gems has been literally one of the biggest reasons why upgrading apps to newer versions of gems (esp Rails!) could be very difficult. I worked on projects where upgrading to newer version of Rails was practically impossible due to the risk it would bring with upgrading additional gems along with the rails upgrade (classic example: devise).

With a container you are forced to register objects that you’ll be depending on, and it gets you closer to coming up with your own abstractions. Even if you register objects from 3rd party gems without wrapping them in a custom abstraction, you will at least rely on their specific interfaces, once you clearly see what you really need, it’ll be very easy to wrap it with your own abstraction that provides exactly what you need. If it turns out that suddenly a given 3rd party gem is no longer working well for you, you’ll see that your system depends on, ie, mailer service, and it needs one, maybe two methods only, and replacing it is going to be simple. Contrast that with the typical approach where 3rd party code pollutes your entire codebase, it’s especially problematic in large projects, but honestly even on a smaller scale it could be dangerous too.

I believe we’ve come up with a solution that’s lightweight and unobtrusive. I’ve managed to use dry-(container,auto-inject) in a rails project, which was added few months after starting the project. It worked extremely well. If I had another chance to use it in a rails app I would definitely try to create a railtie for dry-component, as it should work even better.

@Kukunin I managed my way to implement a simpler version, without the proxy methods :smile:

Thanks @solnic for your comments. You’ll be happy to hear that my colleagues are happy to try Dependency Injection containers :sunny:

Here is the code:

require 'dry-container'
require 'dry-auto_inject'

module Api
  def self.configure
    container.register :main_component, -> { MainComponent.new }
    container.register :dependency, -> { Dependency.new }
    container.freeze
  end

  def self.try_dry_rb_container
    container.resolve(:main_component).hello_world
  end

  private

  @@container = Dry::Container.new

  AutoInject = Dry::AutoInject(@@container)

  def self.container
    @@container
  end
end

class MainComponent
  include Api::AutoInject[:dependency]

  def hello_world
    dependency.say_hi
  end
end

class Dependency
  def say_hi
    "Hi from dry-rb dry-container injected with dry-auto-inject"
  end
end

Api.configure
puts Api.try_dry_rb_container
1 Like