Simple dependency injection
Note: This is only compatible with Ruby 1.9+
On most systems:
$ gem install deject
On some systems:
$ sudo gem install deject
If you have to use sudo and you don't know why, it's because you need to set your GEM_HOME environment variable.
Add Deject to your class.
class Game
Deject self
end
Declare a dependency with a default.
ComputerPlayer = Class.new
class Game
Deject self
dependency(:player) { ComputerPlayer.new }
end
Game.new.player # => #<ComputerPlayer:0x007fb504945f28>
Override the dependency for the class.
ComputerPlayer = Class.new
HumanPlayer = Class.new
class Game
Deject self
dependency(:player) { ComputerPlayer.new }
end
Game.override(:player) { HumanPlayer.new } # in some initialization file
Game.new.player # => #<HumanPlayer:0x007fde2d8ac880>
Override the dependency for an instance by using #with_<dependency>
. You can
pass a specific object, or a block for lazy initialization. This method returns
the instance.
ComputerPlayer = Class.new
HumanPlayer = Class.new
MockPlayer = Class.new
class Game
Deject self
dependency(:player) { ComputerPlayer.new }
end
Game.new.with_player(HumanPlayer.new).player # => #<HumanPlayer:0x007fb2a1015e40>
Game.new.with_player { MockPlayer.new }.player # => #<MockPlayer:0x007fb2a10155f8>
Set a global default value to be used when a value isn't explicitly provided.
If you are worried about clobbering a previously registered value, invoke as .register(:player2, safe: true)
this is turned off by default because I found that code reloading was horking everything up.
ComputerPlayer = Class.new
HumanPlayer = Class.new
# these would go in some initialization file
Deject.register(:player1) { ComputerPlayer.new }
Deject.register(:player2) { ComputerPlayer.new }
class Game
Deject self
dependency(:player1) { HumanPlayer.new }
dependency :player2
end
Game.new.player1 # => #<HumanPlayer:0x007fed8212d7d0>
Game.new.player2 # => #<ComputerPlayer:0x007fed8212d348>
Dependencies without a default block can passed to the Deject function.
ComputerPlayer = Class.new
class Game
Deject self, :player
end
Game.new.with_player(ComputerPlayer.new).player # => #<ComputerPlayer:0x007fac2393a248>
Anywhere a block is used, the instance is passed to it.
ChattyPlayer = Struct.new :message
class Game < Struct.new(:name)
Deject self
dependency(:player) { |game| ChattyPlayer.new "You're good at #{game.name}" }
end
player = Game.new('Monopoly').player
player.message # => "You're good at Monopoly"
player = Game.new('Monopoly').with_player { |game| ChattyPlayer.new "You're terrible at #{game.name}" }.player
player.message # => "You're terrible at Monopoly"
Results are memoized.
NamedPlayer = Struct.new :name
class Game < Struct.new(:name)
Deject self
i = 0
dependency(:player) { NamedPlayer.new "Player#{i+=1}" }
end
game = Game.new
game.player.name # => "Player1"
game.player.name # => "Player1"
Game.new.player.name # => "Player2"
Hard dependencies kick ass. They make your code clear and easy to understand. But, of course, they're hard, you can't change them (or can't reasonably change them). So when you go to test, it sucks. When you want to reuse, it sucks. How to get around this? Inject your dependencies.
And while it's not the worst thing in the world to do custom dependency injection in Ruby, it still gets obnoxious.
Example: passing dependency when initializing
class SomeClass
attr_accessor :some_dependency
# cannot set this unless also setting arg2
def initialize(arg1, arg2=default, some_dependency=default)
end
# cannot set arg2 without being forced to set dependency
def initialize(arg1, some_dependency=default, arg2=default)
end
# forced to deal with the dependency *every place* you use this class
def initialize(some_dependency, arg1, arg2=default)
end
# okay, this isn't too bad unless:
# 1) You want to change the default
# 2) You only have one other optional arg
# as you must degrade the interface for this new requirement
# 3) Your options aren't simple,
# (e.g. will be passed to some other class as I was dealing with when I decided to write this),
# then you will have to namespace your options and theirs
def initializing(arg1, options={})
arg2 = options.fetch(:arg2) { default }
self.some_dependency = options.fetch(:some_dependency) { default }
end
end
Example: try to set it in a method that you change later
class SomeClass
class << self
attr_writer :some_dependency
def some_dependency(instance)
@some_dependency ||= default
end
end
attr_writer :some_dependency
def some_dependency
@some_dependency ||= self.class.some_dependency self
end
end
# blech, that's:
# 1) complicated -- as in difficult to easily look at and understand
# especially if you were to have more than one dependency
# 2) probably needs explicit tests given that there's quite a bit of
# indirection and behaviour going on in here
# 3) the class level override can't take into account anything unique
# about the instance (ie it must be an object, so must work for all instances)
# 4) instances must be overridden like this: instance = SomeClass.new
# instance.some_dependency = override
# instance.whatever
# whereas Deject would be like this: SomeClass.new.with_some_dependency(override).whatever
Example: redefine the method
class SomeClass
def some_dependency
@some_dependency ||= default
end
end
# then later in some other file, totally unbeknownst to anyone reading the above code
class SomeClass
def some_dependency
@some_dependency ||= new_default
end
end
# Want to piss off your colleagues? Imagine how long it will take them to figure out
# why this code doesn't behave as they expect. What's more, guess what happens when
# someone refactors that main class... your redefinition of some_dependency just becomes
# a definition. It doesn't fail, it has no idea about the method it's overriding,
# or the changes that happened to it.
Compare the above examples to Deject
class SomeClass
Deject self
dependency(:some_dependency) { |instance| default }
end
# straightforward (no one will be surprised when this changes),
# declarative so easy to understand
# convenient to override for all instances or any specific instance.
There have been maybe four or five implementations of Deject throughout it's life (though I think only two were ever committed to the repo). I ultimately chose the current implementation because it was the easiest to add features to. That said, it is not canonical Ruby style code, and will take an open mind to work with.
I intentially chose to avoid using a module because this is pervasive and widely abused in Ruby, for more, see my blog post.
I thought a long time about how to add the functionality, thinking about Deject.execute
or some other verb that the Deject noun could perform.
But I couldn't think of a good one. But wait, do I really need a verb? I went and re-read Execution in the Kingdom of Nouns
and decided I was okay with having a method named after the class that applies it, hence Deject SomeClass
. Not a usual practice
but not unheard of, and I don't think it makes sense to force an OO like interface where it doesn't fit well.
We use with_<dependency>
instead of dependency=
because taking blocks is grotesque with assignment methods. I have a general
disdain for assignment methods as they encourage a mindset that doesn't appreciate the advantages of OO.
"When you have a 'setter' on an object, you have turned an object back into a data structure" -- Alan Kay.
Furthermore, I nearly always want to be able to override the result inline, which you can't easily do with assignment methods
as the interpreter guarantees they return the RHS (best solution would be to tap
the object).
The with_<name>
pattern is a common pattern in IO.
In general, all variables are stored as locals in closures rather than instance variables on the object. This is
partially due to the implementation (alternative implementations used ivars), and partially because I wanted to
make a point that relying on ivars is a bad practice: You cannot change implementations (without changing all the code using the ivar)
if you use the ivar instead of the getter (e.g. switch from attr_accessor
to a struct, or in an ActiveRecord::Base
subclass, moving a variable
from an attr_accessor
into the database). Furthermore, directly accessing ivars requires you to know when they were
initialized, which you should not have to deal with, and this also impedes you from extracting the variable into a
method you inherit from a module (the module can't lazily initialize it, because their methods are completely bypassed).
And it even impedes refactoring. If you previously initialized @full_name
in the #initialize
method, you could not then decide to
refactor def fullname() @fullname end
into def fullname() "#@firstname #@lastname" end
because users of
fullname aren't using the method, they're accessing the variable directly. In general, I think it is best to
encapsulate from everyone, including other methods in the same object. In Deject you don't have a choice,
you use the methods because there are no variables. If you'd like to read an argument against my position on this,
Rick Denatale summarizes Kent Beck's opinion on ruby-talk.
Deject does not litter your classes or instances with unexpected methods or variables.
- To Enova for helping me find the best use cases and most relevant missing features.
- To the 8th Lighters who have provided feedback, questions, and criticisms.
Maybe raise an error if you call with_whatever
and pass no args.
Maybe add a setter rather than only provide the overrider.
Copyright (c) 2012 Joshua Cheek
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.