Using dry-auto_inject Args strategy with existing initialize method

I’m having some difficulty using AutoInject in a class that already has an initialize method defined. Here is a representative code snippet:

require 'dry-auto_inject'

di_container = Dry::Container.new
di_container.register(:injected_value, "injected")
AutoInject = Dry::AutoInject(di_container)

class Tester
  include AutoInject.args[:injected_value]
  attr_accessor :value

  def initialize(value)
    @value = value
  end
  
  def test
    puts @value
    puts @injected_value
  end
end

t = Tester.new("initialized")
t.test

The injected_value variable does not get set by AutoInject. If the initialize method is removed, injected_value gets set. I’ve read through examples and other posts on this discussion board, as well as the spec for dry-auto_inject, but have been unable to identify where a mistake has been made. Any help that could be offered would be appreciated.

There is no mistake actually, the behavior is intentional. You can override AI’s dependencies by passing values to the constructor, this way AI injects dependencies.

Oh, ok. So is there no way for some attributes to be set via the constructor as normal (like @value in my snippet) while at the same time a different attribute is set via AutoInject (like @injected_value)? I would like to get @value set manually while @injected_value is set by AutoInject, but I understand if it is not possible.

Thank you for the help

I’m afraid it’s not gonna work now because it defines methods on your class, but not via an anonymous module, so you can’t call super to trigger it’s behavior. This means when you implemented your own initialize then you overridden the one define by injection extension.

We probably could easily improve that by using an anonymous module so that you can call super although I gotta say classes that use DI with auto-inject should not need a custom constructor, as the whole point of this library is to remove the need of having to implement constructors with same boilerplate. On the other hand I understand that people may have all kinds of reasons for having to do that (ie existing codebase where they want to introduce auto-inject w/o having to refactor too much), so I’d be OK with improving the situation.

Ok, cool. I can definitely survive without that functionality, I was just curious since it seemed like something you should be able to do. Thank you for your feedback everyone

We actually have been defining .new and #initialize in anonymous modules for a while now, rather than defining those methods directly on the class. See here and here for how it’s working currently.

This makes me feel like it should be possible to get that original class to work.

@doubtedgoat, does this work?

class Tester
  include AutoInject.args[:injected_value]
  attr_accessor :value

  def initialize(value, *args)
    super(*args)
    @value = value
  end
end

Deps go first so this should be

class Tester
  include AutoInject.args[:injected_value]
  attr_accessor :value

  def initialize(*args, value)
    super(*args)
    @value = value
  end
end

and

Tester.new(nil, :my_value)

Which is quite cumbersome FWIW

1 Like

Hi there,

I’m enjoying using dry-auto_inject and friends as I’m adding code to an existing rails (5, although it started as 4.1) codebase.

For reference, Tim’s article Dependency Injections at Scale really inspired me to actually try some of this stuff out.

I’ve also hit the problem that @doubtedgoat mentions, not for new code that I’m writing, but when refactoring existing code.

I wonder if we could have an API for dealing with DI for classes that have an existing interface for new/initialize:

class LegacyFoo
  # I can't think of a better name for this strategy than explicit
  include MyInjector.explicit[:foos_repo, :sell_foo]

  def initialize(how_many = 3)
  # ...
end

# Instantiating LegacyFoo via existing call sig results in the default dependencies
LegacyFoo.new(5).foos_repo # => MyContainer[:foos_repo]

# To instantiate with a new set of dependencies do this:
LegacyFoo.inject(foos_repo: TestFoosRepo.new).new(5)

I believe the mechanics of this could be worked out in a number of ways, but before embarking on that, I thought I’d see if that was a sensible looking solution to this sort of problem.

Cheers,
Ian

Hi Ian, great to hear from you! Been a long time.

It’d be good to make this nicer with legacy objects where #initialize is used to set state with user-provided values. Our approach to support existing args (the one @flash-gordon shared above) is definitely unwieldy and it’s not nice to have to override #initialize.

So I had a play :smirk:

How does something like this seem to you?

require "dry-container"
require "dry-auto_inject"

# Set up the strategy
################################################################################

class AttrWriters < Module
  ClassMethods = Class.new(Module)
  InstanceMethods = Class.new(Module)

  attr_reader :container
  attr_reader :dependency_map
  attr_reader :instance_mod
  attr_reader :class_mod

  def initialize(container, *dependency_names)
    @container = container
    @dependency_map = Dry::AutoInject::DependencyMap.new(*dependency_names)
    @instance_mod = InstanceMethods.new
    @class_mod = ClassMethods.new
  end

  def included(klass)
    define_new
    define_accessors
    define_injector

    klass.send(:include, instance_mod)
    klass.extend(class_mod)

    super
  end

  private

  def define_accessors
    instance_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
      attr_accessor #{dependency_map.names.map { |name| ":#{name}" }.join(', ')}
    RUBY
    self
  end

  def define_new
    class_mod.class_exec(container, dependency_map) do |container, dependency_map|
      define_method :new do |*args|
        super(*args).tap do |obj|
          obj.inject
        end
      end
    end
  end

  def define_injector
    instance_mod.class_exec(container, dependency_map) do |container, dependency_map|
      # TODO: make name configurable
      define_method :inject do |**args|
        deps = dependency_map.to_h.each_with_object({}) { |(name, identifier), obj|
          obj[name] = args[name] || container[identifier]
        }.merge(args)

        deps.each do |name, dep|
          send :"#{name}=", dep
        end

        self
      end
    end
  end
end

class Strategies < Dry::AutoInject::Strategies
  register :attr_writers, AttrWriters
end

# Now set up some objects in our container
################################################################################

class MyOperation
end

class AnotherOperation
end

class MyContainer
  extend Dry::Container::Mixin

  register "my_op" do
    MyOperation.new
  end
  register "another_op" do
    AnotherOperation.new
  end
end

Import = Dry::AutoInject(MyContainer, strategies: Strategies)

# Time to test things!
################################################################################

class Legacy
  include Import.attr_writers[
    first_op: "my_op",
    second_op: "another_op",
  ]

  def initialize(value)
    @value = value
  end
end

# Create an object with the default dependencies:

legacy = Legacy.new("hi")
p legacy
#<Legacy:0x007f82e39d51a8 @value="hi", @first_op=#<MyOperation:0x007f82e39d4fc8>, @second_op=#<AnotherOperation:0x007f82e39d4f78>>

# Now let's use the helper method to replace just one of the dependencies (you can see the first one is preserved!):

class ManualOp
end
legacy.inject(second_op: ManualOp.new)
p legacy
#<Legacy:0x007f82e39d51a8 @value="hi", @first_op=#<MyOperation:0x007f82e39d4a28>, @second_op=#<ManualOp:0x007f82e39d4b90>>

As you can see here, all the injections can be done via attr_writers (or in bulk via an #inject method that takes a hash, much like we do by default with in our kwargs strategy). This means that as user of this injection strategy doesn’t need to adjust their class’ #initialize at all, and you still maintain the ability to replace dependencies manually if you need (like in the final line in that example). The only downside is that replacing the dependencies mutates the object’s state, which we don’t see as “clean”, but hey, like you say, this is an “escape valve” for legacy classes that we don’t have as much control over.

How does that seem to you?

It feels compelling enough to me that we could almost distribute it directly in the gem!

Hi Tim,

(yes, great to be in contact again!)

That looks ideal to me. It would be a solution to the issues mentioned in this thread IMO.


The next bit is optional! - But given this is a discussion forum…

Re: mutating after initialisation. If achieving a ‘cleaner’ solution is desired, then a class method could orchestrate the injection via a proxy object that sets the dependencies via a ‘private’ (to dry-container and friends) protocol.

An example of what I mean in code:

injectedLegacy = Legacy.inject(second_op: ManualOp.new)
# => Legacy(second_op: #<ManualOp>) 
#      ^^^ this thing quacks like Legacy class, but when new
#          is called it sets up the dependencies via some agreed 'private' protocol

# then later
legacy = injectedLegacy.new("hi")
# => #<Legacy:... @value="hi" ... @second_op=#<ManualOp...
# no need to mutate object after initialisation, or change the call sequence

# so no need for this won't work (and it now doesn't need to work)
legacy.first_op = NewOp

Pros:

  • It would remove the need for attr_accessor on dependencies
  • No need to set dependencies after new (which might require changes in collaborating code)
  • The dependency injection protocol could be hidden and protected by a mutex

Cons:

  • It’s hard to understand
  • It would require something gnarly under the hood (as apart of an agreed private protocol to inject the deps after initialisation)

@i2w glad the code I posted will help you out!

I like your suggestion for the cleaner approach. I’ll give it a try when I get a moment!

Do you have anything you could suggest for learning more about the mutex-based protection that you hinted at? It’s not something I’m familiar with.