Skip to content

Instantly share code, notes, and snippets.

@jrochkind
Created December 20, 2011 13:49
Show Gist options
  • Save jrochkind/1501621 to your computer and use it in GitHub Desktop.
Save jrochkind/1501621 to your computer and use it in GitHub Desktop.

An exploration of "dependency injection" in ruby

I recently came across this reddit, demo'ing "dependency injection and inversion of control" in what I think is C#/.Net?

It resulted in a predictable spate of comments on reddit saying basicaly "oh my gosh, dependency injection is so evil, just look at that."

But it got me thinking about how I'd handle the same issues in ruby, and I think the issues to be handled are real issues. So let's look at that.

I don't understand the (.Net?) framework being used in the original example, and I don't feel like reproducing an app environment that would do similar in ruby, so let's just stick to the heart of the matter. They've got some class (not entirely sure which one it is in their tree) that has a method that processes payments, translating that method into ruby it looks something like this:

class SomeClass

  def process_payment(string orderData)
    paymentProvider = PayPalPaymentProvider.new
    result = paymentProvider.Execute(orderData)

    MessageBox.Show(result.to_string) # yeah, just assume this exists somewhere
  end

end 

Now, the problem is that we've hard-coded process_payment to use PayPalPaymentProvider. Why is this a problem? Well, it may not be, but it legitimately might be, because it might make testing of SomeClass difficult (when you don't actually want to connect to PayPal in testing), or it might make things difficult if your app later expands to handle more real payment providers. I think the original author is not crazy to think some refactoring is called for here.

How can we handle this? Well, here's one way that "Dependency Injection" might look in ruby:

class SomeClass
  class << self
    attr_accessor :default_payment_provider
  end
  self.default_payment_provider = PaypalPaymentProvider

  def process_payment(string orderData, 
      payment_provider = self.class.default_payment_provider.new)
    result = payment_provider.Execute(orderData)

    MessageBox.Show(result.to_string) # yeah, just assume this exists somewhere
  end
end 

Note:

  1. It's actually completely backwards compatible with the previous code, anything can still call process_payment(orderData), and get the same behavior they got before.

  2. Alternately, something can call `process_payment(orderData, payment_provider_instance) to call that particular invocation with a different payment provider (any class that implements #execute to do what you want)

  3. Alternately, you can globally set SomeClass.default_payment_provider = SomeOtherClass to change the default payment provider. This might be useful in testing.

  4. Unlike strongly typed languages, we have no interfaces to document or enforce that the class/instance we are using impelements an #execute method to do what we want. This is just a difference between ruby and Java-like languages, and I don't neccesarily find it better in ruby, I point it out precisely because it is potentially a pitfall to be aware of. Strongly typed languages and community practices like in C# or Java compared to weakly typed languages like ruby have trade-offs;

  5. but my point here is that apparently ruby's approach leads to much more sane 'dependency injection' -- note well that there's absolutely no need of a huge "dependency injector library", with confusing XML configuration or anything else.

Is 'dependency injection' even a thing?

"Dependency Injection" in ruby can be so simple that it almost doesn't even need a name, it's just "abstracting out something that varies, via parameterization", same as we do all the time when we write and refactor code. If anything, I think the name for what's going on here is actually the 'strategy pattern' -- the thing that led to PaypalPaymentProvider being it's own class in the first place, instead of just some methods in the original class. If you had started out with that logic just in methods of the same class, the recognition that you actually needed it parameterized, combined with knowing the 'strategy' pattern (by name or intuitively) is what would lead you to put it in it's own class and parameterize it in a refactoring.

But I suggest that what I've done here is essentially the same pattern as in the original C# (?) example. If that was 'dependency injection', so is this.

So while it almost seems so simple it doesn't need a name -- knowing of the strategy pattern, and even knowing something of what the heck people mean by "dependency injection" in other languages helps me arrive at this solution quicker when I need it, or build it in from the start when it seems likely to be needed. I am not a pattern hater -- I don't actually believe that 'patterns are just ways of working around language limitations'; rather, if the language (or framework/library) gives you adequate support, then applying patterns are so easy and simple that they aren't monstrous, and in some cases are so intuitive you don't even need to know the name of it. But being familiar with some commonly useful patterns can still be helpful in arriving at a nice solution quickly.

Because it can be done so backwards-compatibly, there's not neccesarily a need to abstract everything possible out in this way when you start. It's easy enough to add later where you discover you actually need it. There is such a thing as "premature abstraction" (aka over-engineering).

On the other still positive hand, if done sensibly it can be so simple, without adding much complexity or making the code any harder to read or follow, why not add it when writing initially if it intuively seems to make sense? The YAGNI absolutists won't like it, but I think there's nothing special about 'dependency injection' here, it's just a usual case of writing at the proper level of abstraction, factoring out things that may vary as parameters, which we make intuitive decisions about all the time when practicing the craft (craft!) of writing software.

But what about that awful class var?

Okay, so I store the default 'strategy' (paypal) in a class var.

People don't like class variables, considering them a 'code smell' -- but what's the reason for this? It's essentially because it's global state. But Java/C# "dependency injection" is STILL global state, is it not?

Sometimes it actually makes sense to have global state, and a large subset of such times is the one we call 'configuration', which is essentially what we're doing with the settable default strategy.

Now, configuration is actually somewhat of a 'not clear best practice' problem in ruby, as evidenced by the too-many gems to do configuration for you available, each with their own not-quite-right balance of simplicity/flexibility/power.

But if are going to have global state, why not do it simply, instead of a monstrous 'dependency injection' framework?

If it's a rails app, instead of sticking it in a class var, you could put it in ./config/application.rb in your own custom config.my_key.

Although if you do it with class-level state carefully, you can have better behavior under inheritance and such -- a sub-class of SomeClass can inherit SomeClass's default or set it's own (just for it and it's subclasses). But the straighforward Ruby class variable approach I demo'd above wont' do that. Class state in ruby ends up being kind of 'too many ways to do it'. ActiveSupport's class_attribute is one way I like when I have ActiveSupport.

If someone did end creating the 'perfect' flexible-enough-to-fit-all-cases 'configuration' library for ruby, would it end up being just as monstrous as 'dependency injection' in other languages? Maybe. But then I guess it wouldn't be 'perfect'. Maybe better to just do it case by case as above, I dunno. Software is hard.

Or?

Or is what I've suggested a terrible way to do things in ruby too? I'm no architecture expert, I'm just muddling through trying to find the simplest ways to concisely support the behavior I need, same as everyone else. Let me know if you think there's a better way.

Some people might do this in ruby by implementing the methods in a mix-in module, rather than a seperate class, include'ing that module in SomeClass, and then somehow at runtime swapping that module for a different module or over-riding it by including a different module, either at the class level or even the instance level with extend.

But I find those types of solutions more complex, not less, than putting a strategy like this in it's own class/object. Let's face it, mix-in modules like this are really nothing but multiple inheritance for ruby (but only of 'abstract' classes), and are decidedly not "composition over inheritance". If giant XML-based dependency injection frameworks are the tar pit of Java/C# developers, then crazy complicated mix-in interactions are the tar-pit of ruby developers. Which lead to the same problems that avoiding multiple inheritance and 'composition over inheritance' are meant to avoid -- weird hard to predict interactions, hard to follow/read code, undocumented interface dependencies between classes/modules, etc.

Ruby style duck-typing makes it so easy to use actual composition instead, and it leads to much more readable and testable and clearly concerns-seperated code, why not just do it?

@stevewedig
Copy link

I've looked into similar questions for Python. Here's an outdated presentation I gave in April 2010:
http://code.google.com/p/dippy/
and here's a somewhat more recent comment on the subject:
http://www.reddit.com/r/programming/comments/mpusp/9_things_i_learned_from_reading_the_clean_coder/c32yick

@steveklabnik
Copy link

Please read this: http://weblog.jamisbuck.org/2008/11/9/legos-play-doh-and-programming

Oh, and

 def process_payment(string orderData, payment_provider_klass = PayPalPaymentProvider)
    paymentProvider = payment_provider_klass.new

    result = payment_provider.Execute(orderData)

    MessageBox.Show(result.to_string) # yeah, just assume this exists somewhere
  end

is a lot simpler.

@stevewedig
Copy link

@steveklabnik:

Thanks for that link. Reading that talk could have sped up my learning in 2010. As happens often, I am reminded of the Peter Landin quote "Most papers in computer science describe how their author learned what someone else already knew."

It seems we came to similar conclusions from the same starting point: porting Java's Dependency Injection techniques into a dynamic language (Ruby/Python).

Nowadays I use the term Dependency Inversion for the general principle, and only use the term Dependency Injection to refer to the bulky implementation techniques required in Java. Other languages have a simpler implementation technique: first-class functions and classes.

@blatyo
Copy link

blatyo commented Dec 24, 2011

@steveklabnik

I think it would be better to pass an object rather than the class. That way, you remove the dependency on the new method and allow objects to have parameters.

  def process_payment(string orderData, payment_provider = PayPalPaymentProvider.new)
    result = payment_provider.execute(orderData) #execute is the only dependency between this object and a payment provider

    MessageBox.Show(result.to_string) # yeah, just assume this exists somewhere
  end

  process_payment("data", GoogleCheckoutPaymentProvider.new(1,2,3)) #process payment has no dependency on new and can have arguments for its constructor

@steveklabnik
Copy link

That's also a good idea.

@jrochkind
Copy link
Author

Yeah your example there is actually pretty close to exactly what I suggested above in the original post!

I did have have the default value be changeable too (instead of hard-coded to PayPalPaymentProvider) -- but I made the default settable as a class with the assumption of a no-arg constructor, it's true. I can't think of a better way to do that -- theoretically the default could just be a global re-used instance, but it's so unpredictable in ruby whether a particular instance is thread-safe in what ways or not, that it seems dangerous to ever re-use objects of arbitrary classes as shared state accross instances.

@blatyo
Copy link

blatyo commented Dec 25, 2011

Personally, I don't like the idea that you can change the default. The method allows for you to override the default and that should be all you need. It would make this code much more difficult to understand because you would have to trace down where the default was being overridden, if at all. Also, it allows for the expected behavior to change under your feet.

@jrochkind
Copy link
Author

jrochkind commented Dec 25, 2011 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment