About
Simple dependency injection
Install
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.
Examples
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
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 }
Game.new.player
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
Game.new.with_player { MockPlayer.new }.player
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
Deject.register(:player1) { ComputerPlayer.new }
Deject.register(:player2) { ComputerPlayer.new }
class Game
Deject self
dependency(:player1) { HumanPlayer.new }
dependency :player2
end
Game.new.player1
Game.new.player2
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
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
player = Game.new('Monopoly').with_player { |game| ChattyPlayer.new "You're terrible at #{game.name}" }.player
player.message
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
game.player.name
Game.new.player.name
Reasons
Why write this?
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
def initialize(arg1, arg2=default, some_dependency=default)
end
def initialize(arg1, some_dependency=default, arg2=default)
end
def initialize(some_dependency, arg1, arg2=default)
end
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
Example: redefine the method
class SomeClass
def some_dependency
@some_dependency ||= default
end
end
class SomeClass
def some_dependency
@some_dependency ||= new_default
end
end
Compare the above examples to Deject
class SomeClass
Deject self
dependency(:some_dependency) { |instance| default }
end
About the Code
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.
Special Thanks
- 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.
Todo
Maybe raise an error if you call with_whatever
and pass no args.
Maybe add a setter rather than only provide the overrider.
License
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.