Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is a good practice for dependency injection in Ruby?

I've been reading Sandi Metz's Practical Object-Oriented Design in Ruby and many sites online discussing design in Ruby. Something I've had a hard time fully understanding is the proper way to implement dependency injection.

The internet is flooded with blog posts that explain how dependency injection works in what I think is a very partial way.

I understand that this is supposed to be bad:

class ThisClass
  def initialize
    @another_class = AnotherClass.new
  end
end

While this is a solution:

class ThisClass
  def initialize(another_class)
    @another_class = another_class
  end
end

And that I could send the AnotherClass.new like this:

this_class = ThisClass.new(AnotherClass.new)

That is the approach that Sandi Metz recommends at least. What I don't understand is where should a line like that go? It has to go somewhere and generally in examples of this what's shown is a line like that being placed totally outside of any class, method, or module as if I'm simply entering it all by hand in IRB for testing purposes.

This post (among others) suggests this different approach:

class ThisClass
  def another_class
    @another_class ||= AnotherClass.new
  end
end

Jamis Buck would take a similar approach like this:

class AnotherClass
end

class ThisClass
  def another_class_factory(class_name = AnotherClass)
    class_name.new
  end
end

However, these two examples both preserve AnotherClass's name inside ThisClass, which Sandi Metz says is one of the main things we're trying to avoid.

So what is the best practice for doing this? Should I make a 'dependency' module filled with methods that are factories for objects of each class in my application?

like image 337
Eric Marchese Avatar asked Oct 25 '15 04:10

Eric Marchese


Video Answer


1 Answers

Something I've had a hard time fully understanding is the proper way to implement dependency injection.

I think the best definition of a "proper" implementation is one that adheres to the SOLID principles of object oriented design. In this case mostly the Dependency Inversion Principle.

In this regard, this is the only presented solution that does not violate the DIP(1):

class ThisClass
  def initialize(another_class)
    @another_class = another_class
  end
end

In all other cases, ThisClass has a hard dependency on AnotherClass, and can not function without it. Furthermore, if we wish to replace AnotherClass with a third, we need to modify ThisClass, which is a violation of the Open Closed Principle.

Of course, in the example above, naming the parameter and instance variable another_class is not ideal, since we do not now (and do not need to know) what object is passed to us, as long as it responds to the expected interface. This is the beauty of polymorphism.

Consider the below example, taken from this ThoughtBot video on DIP:

class Copier
  def initialize(reader, writer)
    @reader = reader
    @writer = writer
  end

  def copy
    @writer.write(@reader.read_until_eof)
  end
end

Here you can pass any reader and writer objects that respond to read_until_eof and write respectively. This gives you full freedom to compose your business logic using different pairs of read and write implementations, even at runtime:

Copier.new(KeyboardReader.new, Printer.new)
Copier.new(KeyboardReader.new, NetworkPrinter.new)

Which brings us to your next question.


It has to go somewhere and generally in examples of this what's shown is a line like that being placed totally outside of any class, method, or module [...]

You are correct. While object thinking involves modelling the domain with well isolated, decoupled, and composable objects, you will still need to define how these objects interact, in order to implement any business logic. After all, having composable objects is no good unless we compose them.

The analogy that is often made here is to think of your objects as actors. You are the director, and you still need to create a script(2) for the actors to know how to interact with each other.

That is, you need an entry point into your application. A place where the script starts. This might itself be an object--normally an abstract one. In a command line application, it can be your classic Main class, and in a Rails application it can be your controller.

This might seem strange at first, because the focus of object thinking is on modelling concrete domain objects, and a great deal of all writings on the subject is dedicated to this effort, but just remember the actor-script metaphor, and you'll be on your way.


I strongly recommend you pick up the book Object Thinking. It does a great job explaining the mindset behind object oriented design, without which knowing the language specific implementation details becomes rather futile.


(1): It is worth noting that some proponents consider storing an instance of another class in an instance variable an anti-pattern, but in Ruby, this is fairly idiomatic.

(2): I am not sure if this is the origin of the term script in programming in general, but maybe some historian can shed some light on this.

like image 132
Drenmi Avatar answered Nov 07 '22 22:11

Drenmi