Dependency Management

Hi Rails developers,

I would appreciate some assistance. Please study the following code and provide your reasoning for whether you agree or disagree with the following statements.

I understand the code is not specific to Rails, however I am specifically interested in Rails developer’s perspective. Thanks!

car.rb

class Car def initialise(engine) @engine = engine end

def start @engine.start end end

engine.rb

class Engine

def start

puts 'Engine started'

end

end

  • Car is not dependent on, and therefore not coupled to Engine

  • Car is dependent on an object that responds to “start”

  • Car may or may not be initialised with an Engine object

  • Car uses constructor-based dependency injection

Matthew Riley wrote in post #1141041:

Hi Rails developers,

I would appreciate some assistance. Please study the following code and provide your reasoning for whether you agree or disagree with the following statements.

I understand the code is not specific to Rails, however I am specifically interested in Rails developer’s perspective. Thanks!

car.rb

class Car def initialise(engine)    @engine = engine end

def start    @engine.start end end

engine.rb

class Engine

def start

   puts 'Engine started'

end

end

- Car is not dependent on, and therefore not coupled to Engine

- Car is dependent on an object that responds to “start”

- Car may or may not be initialised with an Engine object

- Car uses constructor-based dependency injection

The example is a little too simple to comment but i can agree with your statements.

- Car is dependent on an object that responds to “start”.

This i think is the trickiest one. If only the engine class has a method called 'start' then Car is effectively coupled to Engine. Also if additional public interface methods get added to Engine, and Car starts to call those: engine.gear_up engine.stop then Car again becomes more and more coupled to the operations of Engine.

Also if Car starts to manage other components, and calling their public interfaces, thus having high fan out, then you also get tight coupling.

So again the point is, this example is still too simple to judge, and the design pattern that will make the most sense for it is dependent on how the rest of the application will flow.

Since one could theoretically create another class that supports #start (and your later examples of #gear_up and #stop, probably delegating #gear_up to a Transmission-like class), I think it's still effectively decoupled. In particular, when *testing* the Car, one could create it with a MockEngine that's only used in the tests.

Matthew Riley wrote in post #1141041:

I understand the code is not specific to Rails, however I am specifically interested in Rails developer’s perspective. Thanks!

Perspective on what? As you demonstrate, Dependency Injection (DI) is a really simple concept, with a really simple implementation, in dynamic ("Duck Typed") languages like Ruby.

Are you asking to verify what you have shown is "Dependency Inject/Inversion of Control?" If so then sure it is.

Are you asking if Rails developers spend a lot of time worrying about "Dependency Injection" or creating elaborate frameworks to support DI? That would be a pretty resounding "NO".

Given that a high percentage of DI usage is related to testability, it might do you good to look at one of the popular testing frameworks used by Rails developers, for example:

You won't see a lot of talk in there about DI or IoC mumbo-jumbo. You'll see the stuff that's actually important, like test doubles, mocks, stubs, etc. Ruby is a dynamically typed language without all those pesky static type dependencies. Take advantage of that in whatever way makes sense.

Hi, thanks for taking the time to respond.

I understand the example is a simple one, but based on that simple example, would you agree that since the Car class does not explicitly use the Engine class, the two classes are completely unrelated?

Also, when you say, “Car again becomes more and more coupled to the operations of Engine”, are you saying that Car becomes more coupled to the concrete implementation of Engine, or to it’s “interface”?

Hi Dave, thanks for taking the time to respond.

Of the two examples below, which do you believe is more testable?

Example 1:

class Car def initialise(engine) @engine = engine end

def start @engine.start end end

Example 2:

class Car def initialise @engine = Engine.new end

def start @engine.start end end

Hi, thanks for taking the time to respond.

I’m experienced with Ruby, but new to Rails. I’m familiar with RSpec.

Ruby certainly does make DI really, really easy.

Ruby also makes it really easy to test code without the need for IoC.

IoC offers other benefits aside from testability. One obvious example is loose coupling.

Loose coupling reduces the risk that changes impact seemingly unrelated areas of the system.

Ruby in itself cannot offer that benefit, nor can Rails.

Would you agree that regardless the language or framework, loose coupling is a valuable engineering practise?

If not, why not? If so, how do you accomplish loose coupling in Rails?

Look forward to your reply, thanks.

Of the two examples below, which do you believe is more testable?

Example 1:

class Car def initialise(engine)    @engine = engine end

...

Example 2:

class Car def initialise    @engine = Engine.new

end

Definitely #1, as it will accept any sort of engine-like thing you care to put in it, so you can use a mock or stub or other kind of fake, even without relying on a faking library to monkey with the item (or worse yet, with Engine).

I might be tempted to make a slight change, though: declare the initializer as "def initialise(engine = Engine.new)", so you don't have to explicitly pass in an Engine in the default case. Yes, that re-couples them somewhat, that's the downside. If you can be sure that Car always has access to Engine, like if they're all part of the same gem you're distributing, this is probably acceptable. In fact, you could do something like:

def initalize(options={})   @engine = options[:engine] || Engine.new   @transmission = options[:transmission] || Transmission.new   @horn = options[:horn] || Horn.new   # and so on end

or if you want to really delve into funky Ruby, maybe even do some metaprogramming to DRY up that pattern, like:

def initalize(options={})   [:engine, :transmission, :horn].each do |sym|     option = options[sym]     klazz = Kernel.const_get(sym.capitalize)     self.instance_variable_set(sym, option || klazz.new)   end end

That might be more worthwhile if you have a long list of options to pass in.

-Dave

If Engine were to inherit ActiveRecord, would that affect your decision to set a default instance?

In such a case, Car would be effectively coupled to both Engine and ActiveRecord.

Also, do you find Rails developers in general care about loose coupling?

If Engine were to inherit ActiveRecord, would that affect your decision to set a default instance?

It depends. Theoretically, the more baggage something drags in, the less you want to be coupled to it. Kinda like with people. :wink:

But in reality, well... see below.

In such a case, Car would be effectively coupled to both Engine and ActiveRecord. Also, do you find Rails developers in general care about loose coupling?

In my experience, most Rails devs aren't going to take the time to decouple from AR. Rails is very opinionated, and one of its opinions is that your models (at least the ones that get stored in the database) should inherit from AR::Base. Yes, it helps makes your tests much faster, swapping out databases much easier, etc. if you can cleanly separate the concerns of business logic versus storage. But it's not the golden path. The tutorials don't tell you how. It's not easy. It takes some advanced thinking to even *care* about it, let alone actually *implement* it. So... while many devs will care deeply about not coupling their own models to *each other*, most seem perfectly OK with, or at least resigned to, having most of them coupled to AR::Base.

I've read some stuff on how to decouple the models from AR and still have the benefits it brings, and tried to do it in a couple little exercises, but still don't have a good intuitive feel for the process, especially what step is next and *why*. Most of what I've read, makes several leaps where I can see the benefit in hindsight, but still don't see/understand any signs pointing in the immediate directions.

-Dave