If you use dependency injection as a default get-go tool, you might get intimidated by the fact that you need to construct dependencies each time you want to call a given class.
Let me pick apart an example, where you need to receive an input, parse it and send it to a certain third party provider:
class Notifier def initialize(input, parser, gateway) @input = input @parser = parser @gateway = gateway end def call parsed_input = parser.parse(input) gateway.call(parsed_input) end attr_reader :parser, :gateway end
Now, if we do it like this, obviously the construction of this class would look like so:
parser = Parser.new gateway = Gateway.new input = "SOME INPUT FORM WHATEVER" Notifier.new(input, parser, gateway).call
If you have to deal with something like rails, there is a big chance that you want to keep your controllers free of all this code. Let me present you with some techniques I use to deal with this issues.
One of the simplest ways to solve this is to define a simple build method that defines the defaults. It's actually pretty straight forwards, so let's dig into the code:
class Notifier class << self def build(input) new( input, Parser.new, Gateway.new ) end end def initialize(input, parser, gateway) @input = input @parser = parser @gateway = gateway end def call parsed_input = parser.parse(input) gateway.call(parsed_input) end attr_reader :parser, :gateway, :input end
In this example, we are simply assigning all of our dependencies in build method. This is quite an okayish solution, but it requires for us to have yet another method and I want to avoid writing the code as much as possible. I use .build method whenever I need to do some additional work on arguments, before passing them to the constructor.
We have achieved a cleaner interface of the notifier class:
input = "SOME INPUT FORM WHATEVER" Notifier.build(input).call
I think it is quite nice and acceptable.
Using default arguments
This method is more straightforward than the previous one. If I use this approach, I heavily leverage the keyword arguments for Ruby 2.
class Notifier def initialize(input:, parser: Parser.new, gateway: Gateway.new) @input = input @parser = parser @gateway = gateway end def call parsed_input = parser.parse(input) gateway.call(parsed_input) end attr_reader :parser, :gateway, :input end
This approach eliminates the need for a class method and gives us much more tidy class. One side note here is the fact that the initialize method argument list tends to get long if you have namespaced class names. If you are okay with that (I am), then use this method.
With this approach we achieved the following result:
input = "SOME INPUT FORM WHATEVER" Notifier.new(input: input).call
Why is this good?
I write code and I do it a lot. I want my codebase to be maintainable and testable. First of all, dependency injection just eliminates the need for calling "real" objects. Let's imagine, that sending a message to the gateway is quite expensive HTTP call. If you want to mock it, it's quite simple to do and pass the double as a dependency.
describe Notifier do it "sends a message to a gateway" do gateway = instance_double(Gateway) expect(gateway).to receive(:call) input = "My random input again" Notifier.new(input: input, gateway: gateway) end end
I really like the new interface checking capabilities of spec doubles. They validate that an instance of a particular class can receive a message and it saves a lot of troubles with outdated doubles.
Usually at the end of each test, I will have an integration test, that touches the whole chain of dependencies, otherwise mocking is not safe, even with the interface checking that RSpec provides.
As a side note, it is generally a good idea to mock only the things you own. If it's a third party library, create a wrapper around it, that has an interface acceptable to you.
In "the real world" code tends to get little more complex than the examples above, but with the little bit of imagination, it is pretty easy to figure that stuff out.
Some of these ideas are taken from Sandi Metz's book "POODR". I can't recommend it enough.