deject 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Readme.md +220 -148
- data/lib/deject.rb +12 -7
- data/lib/deject/version.rb +1 -1
- data/spec/deject_function_spec.rb +6 -0
- data/spec/global_registration_spec.rb +7 -2
- metadata +5 -5
data/Readme.md
CHANGED
@@ -19,81 +19,86 @@ If you have to use sudo and you don't know why, it's because you need to set you
|
|
19
19
|
Example
|
20
20
|
=======
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
22
|
+
```ruby
|
23
|
+
require 'deject'
|
24
|
+
|
25
|
+
class HumanPlayer
|
26
|
+
def type
|
27
|
+
'human player'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ComputerPlayer
|
32
|
+
def type
|
33
|
+
'computer player'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class MockPlayer
|
38
|
+
def type
|
39
|
+
'mock player'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Game
|
44
|
+
Deject self
|
45
|
+
dependency(:player1) { ComputerPlayer.new }
|
46
|
+
dependency :player2
|
47
|
+
|
48
|
+
def name
|
49
|
+
'poker'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# register a global value (put this into an initializer or dependency injection file)
|
54
|
+
# if you are worried about clobbering a previously set value, invoke with `:player2, safe: true`
|
55
|
+
# this is turned off by default because I found that code reloading was horking everything up
|
56
|
+
Deject.register(:player2) { HumanPlayer.new }
|
57
|
+
|
58
|
+
# declared with a block, so will default to block value
|
59
|
+
Game.new.player1.type # => "computer player"
|
60
|
+
|
61
|
+
# declared without a block, so will default to the global definition for player1
|
62
|
+
Game.new.player2.type # => "human player"
|
63
|
+
|
64
|
+
# we can override for this entire class
|
65
|
+
Game.override(:player2) { MockPlayer.new }
|
66
|
+
Game.new.player2.type # => "mock player"
|
67
|
+
|
68
|
+
# we can override for some specific instance using either a block or a value
|
69
|
+
# instance level overriding is done using method with_<dependnecy_name>, which returns the instance
|
70
|
+
Game.new.with_player2 { HumanPlayer.new }.player2.type # => "human player"
|
71
|
+
Game.new.with_player2(ComputerPlayer.new).player2.type # => "computer player"
|
72
|
+
|
73
|
+
# anywhere a block is used, the instance will be passed into it
|
74
|
+
generic_player = Struct.new :type
|
75
|
+
|
76
|
+
game = Game.new.with_player1 { |game| generic_player.new "#{game.name} player1" }
|
77
|
+
game.player1.type # => "poker player1"
|
78
|
+
|
79
|
+
Game.override(:player2) { |game| generic_player.new "#{game.name} player2" }
|
80
|
+
game.player2.type # => "poker player2"
|
81
|
+
```
|
82
|
+
|
83
|
+
|
84
|
+
Note that dependencies using the defaults can be declared when dejecting the class:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
lass Game
|
88
|
+
# this
|
89
|
+
Deject self
|
90
|
+
dependency :player1
|
91
|
+
dependency :player2
|
92
|
+
|
93
|
+
# is the same as this
|
94
|
+
Deject self, :player1, :player2
|
95
|
+
end
|
96
|
+
```
|
63
97
|
|
64
|
-
def credentials
|
65
|
-
# a login key or something, would probably be dejected as well
|
66
|
-
# to retrieve the result from some config file or service
|
67
|
-
'skj123@#KLFNV9ajv'
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
# using the default
|
72
|
-
service = Service.new('josh')
|
73
|
-
service.login
|
74
|
-
service.client # => #<Client:0x007ff97a92d9b8 @credentials="skj123@#KLFNV9ajv", @login="josh">
|
75
|
-
service.client.has_logged_in? 'josh' # => true
|
76
|
-
service.client.initialized_with? service.credentials # => true
|
77
|
-
|
78
|
-
# overriding the default at instance level
|
79
|
-
client_mock = Struct.new :recordings do
|
80
|
-
def method_missing(*args)
|
81
|
-
self.recordings ||= []
|
82
|
-
recordings << args
|
83
|
-
end
|
84
|
-
end
|
85
|
-
client = client_mock.new
|
86
|
-
sally = Service.new('sally').with_client client # <-- you can also override with a block
|
87
|
-
|
88
|
-
sally.login
|
89
|
-
client.recordings # => [[:login, "sally"]]
|
90
|
-
|
91
|
-
sally.login
|
92
|
-
client.recordings # => [[:login, "sally"], [:login, "sally"]]
|
93
|
-
|
94
98
|
Reasons
|
95
99
|
=======
|
96
100
|
|
101
|
+
|
97
102
|
Why write this?
|
98
103
|
---------------
|
99
104
|
|
@@ -103,83 +108,150 @@ So when you go to test, it sucks. When you want to reuse, it sucks. How to get a
|
|
103
108
|
Inject your dependencies.
|
104
109
|
|
105
110
|
And while it's not the worst thing in the world to do custom dependency injection in Ruby,
|
106
|
-
it
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
111
|
+
it still gets obnoxious.
|
112
|
+
|
113
|
+
|
114
|
+
Example: passing dependency when initializing
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class SomeClass
|
118
|
+
attr_accessor :some_dependency
|
119
|
+
|
120
|
+
# cannot set this unless also setting arg2
|
121
|
+
def initialize(arg1, arg2=default, some_dependency=default)
|
122
|
+
end
|
123
|
+
|
124
|
+
# cannot set arg2 without being forced to set dependency
|
125
|
+
def initialize(arg1, some_dependency=default, arg2=default)
|
126
|
+
end
|
127
|
+
|
128
|
+
# forced to deal with the dependency *every place* you use this class
|
129
|
+
def initialize(some_dependency, arg1, arg2=default)
|
130
|
+
end
|
131
|
+
|
132
|
+
# okay, this isn't too bad unless:
|
133
|
+
# 1) You want to change the default
|
134
|
+
# 2) You only have one other optional arg
|
135
|
+
# as you must degrade the interface for this new requirement
|
136
|
+
# 3) Your options aren't simple,
|
137
|
+
# (e.g. will be passed to some other class as I was dealing with when I decided to write this),
|
138
|
+
# then you will have to namespace your options and theirs
|
139
|
+
def initializing(arg1, options={})
|
140
|
+
arg2 = options.fetch(:arg2) { default }
|
141
|
+
self.some_dependency = options.fetch(:some_dependency) { default }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
|
147
|
+
Example: try to set it in a method that you change later
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class SomeClass
|
151
|
+
class << self
|
152
|
+
attr_writer :some_dependency
|
153
|
+
def some_dependency(instance)
|
154
|
+
@some_dependency ||= default
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
attr_writer :some_dependency
|
159
|
+
def some_dependency
|
160
|
+
@some_dependency ||= self.class.some_dependency self
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# blech, that's:
|
165
|
+
# 1) complicated -- as in difficult to easily look at and understand
|
166
|
+
# especially if you were to have more than one dependency
|
167
|
+
# 2) probably needs explicit tests given that there's quite a bit of
|
168
|
+
# indirection and behaviour going on in here
|
169
|
+
# 3) the class level override can't take into account anything unique
|
170
|
+
# about the instance (ie it must be an object, so must work for all instances)
|
171
|
+
# 4) instances must be overridden like this: instance = SomeClass.new
|
172
|
+
# instance.some_dependency = override
|
173
|
+
# instance.whatever
|
174
|
+
# whereas Deject would be like this: SomeClass.new.with_some_dependency(override).whatever
|
175
|
+
```
|
176
|
+
|
177
|
+
|
178
|
+
Example: redefine the method
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
class SomeClass
|
182
|
+
def some_dependency
|
183
|
+
@some_dependency ||= default
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# then later in some other file, totally unbeknownst to anyone reading the above code
|
188
|
+
class SomeClass
|
189
|
+
def some_dependency
|
190
|
+
@some_dependency ||= new_default
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Want to piss off your colleagues? Imagine how long it will take them to figure out
|
195
|
+
# why this code doesn't behave as they expect. What's more, guess what happens when
|
196
|
+
# someone refactors that main class... your redefinition of some_dependency just becomes
|
197
|
+
# a definition. It doesn't fail, it has no idea about the method it's overriding,
|
198
|
+
# or the changes that happened to it.
|
199
|
+
```
|
200
|
+
|
201
|
+
Compare to Deject
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
class SomeClass
|
205
|
+
Deject self
|
206
|
+
dependency(:some_dependency) { |instance| default }
|
207
|
+
end
|
208
|
+
|
209
|
+
# straightforward (no one will be surprised when this changes),
|
210
|
+
# convenient to override for all instances or any specific instance.
|
211
|
+
```
|
212
|
+
|
213
|
+
|
214
|
+
|
215
|
+
About the Code
|
216
|
+
--------------
|
217
|
+
|
218
|
+
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).
|
219
|
+
I ultimately chose the current implementation because it was the easiest to add features to.
|
220
|
+
That said, it is not canonical Ruby style code, and will take an open mind to work with.
|
221
|
+
|
222
|
+
I intentially chose to avoid using a module because this is pervasive and widely abused in Ruby, for more, see my [blog post](http://blog.8thlight.com/josh-cheek/2012/02/03/modules-called-they-want-their-integrity-back.html).
|
223
|
+
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.
|
224
|
+
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](http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html)
|
225
|
+
and decided I was okay with having a method named after the class that applies it, hence `Deject SomeClass`. Not a usual practice
|
226
|
+
but not unheard of, and I don't think it makes sense to force an OO like interface where it doesn't fit well.
|
227
|
+
|
228
|
+
We use `with_<dependency>` instead of `dependency=` because taking blocks is grotesque with assignment methods. Further, I have a general
|
229
|
+
disdain for assignment methods as they encourage a mindset that doesn't appreciate the advantages of OO.
|
230
|
+
_"When you have a 'setter' on an object, you have turned an object back into a data structure" -- Alan Kay_.
|
231
|
+
Furthermore, I nearly always want to be able to override the result inline, which you can't easily do with assignment methods
|
232
|
+
as the interpreter guarantees they return the RHS (best solution would be to `tap` the object).
|
233
|
+
|
234
|
+
In general, all variables are stored as locals in closures rather than instance variables on the object. This is
|
235
|
+
partially due to the implementation (alternative implementations used ivars), and partially because I wanted to
|
236
|
+
make a point that relying on ivars is a bad practice: You cannot change implementations (without changing all the code using the ivar)
|
237
|
+
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
|
238
|
+
from an `attr_accessor` into the database). Furthermore, directly accessing ivars requires you to know when they were
|
239
|
+
initialized, which you should not have to deal with, and this also impedes you from extracting the variable into a
|
240
|
+
method you inherit from a module (the module can't lazily initialize it, because their methods are completely bypassed).
|
241
|
+
And it even impedes refactoring. If you previously initialized `@full_name` in the `#initialize` method, you could not then decide to
|
242
|
+
refactor `def fullname() @fullname end` into `def fullname() "#@firstname #@lastname" end` because users of
|
243
|
+
fullname aren't using the method, they're accessing the variable directly. In general, I think it is best to
|
244
|
+
encapsulate from everyone, including other methods in the same object. In Deject you don't have a choice,
|
245
|
+
you use the methods because there are no variables. If you'd like to read an argument against my position on this,
|
246
|
+
Rick Denatale summarizes Kent Beck's opinion on [ruby-talk](http://www.ruby-forum.com/topic/211544#919648).
|
247
|
+
|
248
|
+
Deject does not litter your classes or instances with unexpected methods or variables.
|
249
|
+
|
250
|
+
|
251
|
+
Special Thanks
|
252
|
+
==============
|
253
|
+
|
254
|
+
To the [8th Light](http://8thlight.com/)ers who have provided feedback, questions, and criticisms.
|
183
255
|
|
184
256
|
|
185
257
|
Todo
|
data/lib/deject.rb
CHANGED
@@ -4,8 +4,8 @@ module Deject
|
|
4
4
|
UninitializedDependency = Class.new StandardError
|
5
5
|
|
6
6
|
class << self
|
7
|
-
def register(name, &initializer)
|
8
|
-
raise ArgumentError, "#{name} has been registered multiple times" if registered?
|
7
|
+
def register(name, options={}, &initializer)
|
8
|
+
raise ArgumentError, "#{name} has been registered multiple times" if options[:safe] && registered?(name)
|
9
9
|
raise ArgumentError, "#{name} has been registered with Deject without an initialization block" unless initializer
|
10
10
|
@registered[name.intern] = initializer
|
11
11
|
end
|
@@ -26,11 +26,13 @@ module Deject
|
|
26
26
|
reset
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
29
|
+
|
30
|
+
def Deject(klass, *initial_dependencies)
|
31
|
+
# Not a common way of writing code in Ruby, I know.
|
32
|
+
# But I tried out several implementations and found this was the easiest to
|
33
|
+
# work with within the constraints of the gem (that it doesn't leave traces
|
34
|
+
# of itself all over your objects)
|
35
|
+
|
34
36
|
uninitialized_error = lambda do |meth|
|
35
37
|
raise Deject::UninitializedDependency, "#{meth} invoked before being defined"
|
36
38
|
end
|
@@ -96,5 +98,8 @@ def Deject(klass)
|
|
96
98
|
self
|
97
99
|
end
|
98
100
|
|
101
|
+
# add the initial dependencies
|
102
|
+
initial_dependencies.each { |dependency| klass.dependency dependency }
|
103
|
+
|
99
104
|
klass
|
100
105
|
end
|
data/lib/deject/version.rb
CHANGED
@@ -18,4 +18,10 @@ describe 'Deject()' do
|
|
18
18
|
it 'returns the class' do
|
19
19
|
Deject(klass).should be klass
|
20
20
|
end
|
21
|
+
|
22
|
+
let(:default) { :some_default }
|
23
|
+
it "can take a list of dependencies that don't have blocks" do
|
24
|
+
Deject.register(:abc) { default }
|
25
|
+
Deject(klass, :abc).new.abc.should == default
|
26
|
+
end
|
21
27
|
end
|
@@ -32,9 +32,14 @@ describe Deject, '.register and registered' do
|
|
32
32
|
Deject.registered(:abc).should == nil
|
33
33
|
end
|
34
34
|
|
35
|
-
it '
|
35
|
+
it 'does not raise an ArgumentError error if registration clobbers a previously set value' do
|
36
36
|
Deject.register(:abc){}
|
37
|
-
|
37
|
+
Deject.register(:abc){}
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'raises an error if registration clobbers a previously set value when passed safe: true' do
|
41
|
+
Deject.register(:abc){}
|
42
|
+
expect { Deject.register(:abc, safe: true){} }.to raise_error ArgumentError, /abc/
|
38
43
|
end
|
39
44
|
|
40
45
|
it 'knows what has been registered' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deject
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2012-04-29 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
requirement: &
|
16
|
+
requirement: &70254829462080 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70254829462080
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: pry
|
27
|
-
requirement: &
|
27
|
+
requirement: &70254829461380 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70254829461380
|
36
36
|
description: Provides a super simple API for dependency injection
|
37
37
|
email:
|
38
38
|
- josh.cheek@gmail.com
|