deject 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
- require 'deject'
23
-
24
- # Represents some client like https://github.com/voloko/twitter-stream
25
- # Some client that will be used by our service
26
- class Client
27
- def initialize(credentials)
28
- @credentials = credentials
29
- end
30
-
31
- def login(name)
32
- @login = name
33
- end
34
-
35
- def has_logged_in?(name) # !> `&' interpreted as argument prefix
36
- @login == name
37
- end
38
-
39
- def initialized_with?(credentials)
40
- @credentials == credentials
41
- end
42
- end
43
-
44
-
45
- class Service
46
- Deject self # <-- we'll talk more about this later
47
-
48
- # you can basically think of the block as a factory that
49
- # returns a client. It is evaluated in the context of the instance
50
- # ...though I'm not sure that's a good strategy to employ
51
- # (I suspect it would be better that these return constants as much as possible)
52
- dependency(:client) { Client.new credentials }
53
-
54
- attr_accessor :name
55
-
56
- def initialize(name)
57
- self.name = name
58
- end
59
-
60
- def login
61
- client.login name
62
- end
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 can still get obnoxious. What do you do? There's basically two options:
107
-
108
- 1. Add it as an argument when initializing (or _possibly_ when invoking your method). This works
109
- fine if you aren't already doing anything complicated with your arguments. If you can just throw
110
- an optional arg in there for the dependency, giving it a default of the hard dependency, then
111
- it's not too big of a deal. But what if you have two dependencies? Then you can't use optional
112
- arguments, because how will you know which is which? What if you're already taking optional args?
113
- Then again, you can't pass this in optionally. So you have to set it to an ordinal argument, which
114
- means that everywhere you use the thing, you have to deal with the dependency. It's cumbersome and ugly.
115
- Or you can pass it in with an options hash, but what if you're already taking a hash (as I was when
116
- I decided I wanted this) and it's not for this object? Then you have to namespace the common options
117
- such that you can tell them apart, it's gross (e.g. passing html options to a form in Rails), and you
118
- only need to do it for something that users shouldn't need to care about unless they really want to.
119
-
120
- 2. Defining methods to return the dependency that can be overridden by setting the value. This is a heavier
121
- choice than the above, but it can become necessary. Define an `attr_writer :whatever` and a getter
122
- whose body looks like `@whatever ||= HardDependency.new`. Not the worst thing in the world, but it takes
123
- about four lines and clutters things up. What's more, it must be set with a setter, and setters always
124
- return the RHS of the assignment. So to override it, you have to have three lines where you probably only want one.
125
- And of course, having a real method in there is a statement. It says "this is the implementation", people
126
- don't override methods all willy nilly, I'd give dirty looks to colleagues if they overrode it as was convenient.
127
- For instance, say you _always_ want to override the default (e.g. a FakeUser in the test environment and User in
128
- development/production environments). Then you have to open the class and redefine it in an initialization file.
129
- Not cool.
130
-
131
- Deject handles these shortcomings with the default ways to inject dependencies. Declaring something a dependency
132
- inherently identifies it as overridable. Overriding it by environment is not shocking or unexpected, and only requires one line,
133
- and has the advantage of closures during overriding -- as opposed to having to metaprogramming to set that default.
134
-
135
- It makes it very easy to declare and to override dependencies, by adding an inline call to the override.
136
- You don't have to deal with arguments, you don't have to define methods, it defines the methods for you
137
- and gives you an easy way to inject a new value. In the end, it's simpler and easier to understand.
138
-
139
-
140
- Statements I am trying to make by writing this
141
- ----------------------------------------------
142
-
143
- Dependencies should be soft by default, dependency injection can have a place in Ruby
144
- (even though I'll probably get made fun of for it). I acknowledge that I really enjoyed
145
- the post [Why I love everything you hate about Java](http://magicscalingsprinkles.wordpress.com/2010/02/08/why-i-love-everything-you-hate-about-java/).
146
- Though I agreed with a lot of the rebuttals in the comments as well.
147
-
148
- I intentionally didn't do this with module inclusion. Module inclusion has become a cancer
149
- (I'll probably write a blog about that later). _Especially_ the way people abuse the `self.included` hook.
150
- I wanted to show people that you don't _HAVE_ to do that. There's no reason your module can't have
151
- a method that is truthful about its purpose, something like `MyModule.apply_to MyClass`, it can include and extend
152
- in there all it wants. That's fine, that's obvious, it isn't lying. But when people `include MyModule`
153
- just so they can get into the included hook (where they almost never need to) and then **EXTEND** the class... grrrrr.
154
-
155
- And of course, after I decided I wasn't going to directly include / extend the module, I began
156
- thinking about how to get Deject onto the class. `Deject.dejectify SomeClass`? Couldn't think of
157
- a good verb. But wait, do I _really_ need a verb? I went and read
158
- [Execution in the Kingdom of Nouns](http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html)
159
- and decided I was okay with having a method that applies it, hence `Deject SomeClass`. Not a usual practice
160
- but not everything needs to be OO. Which led to the next realization that I didn't need a module at all.
161
-
162
- So, there's two implementations. You can set which one you want to use with an environment variable (not that you care).
163
- The first is "functional" which is to say that I was trying to channel functional ideas when writing it. It's really just one
164
- big function that defines and redefines methods. Initially I hated this, I found it very difficult to read (might have been
165
- better if Ruby had macros), I had to add comments in to keep track of what was happening.
166
- But then I wrote the object oriented implementation, and it was pretty opaque as well.
167
- Plus there were a lot of things I wanted to do that were very difficult to accomplish, and it was much longer.
168
-
169
- So in the end, I'm hoping someone takes the time to look at both implementations and gives me feedback on their thoughts
170
- Is one better? Are they each better in certain ways? Can this code be made simpler? Any feedback is welcome.
171
-
172
- Oh, I also intentionally used closures over local variables rather than instance variables, because I
173
- wanted to make people realize it's better to use setters and getters than to directly access instance variables
174
- (to be fair, there are some big names that [disagree with](http://www.ruby-forum.com/topic/211544#919648) me on this).
175
- I think most people directly access ivars because they haven't found themselves in a situation where it mattered.
176
- But what if `attr_accessor` wound up changing implementations such that it didn't use ivars? "Ridiculous" I can hear
177
- people saying, but it's not so ridiculous when you realize that you can remove 4 redundant lines by inheriting from
178
- a Struct. If you use indirect access, everything still works just fine. And structs aren't the only place this occurs,
179
- think about ActiveRecord::Base, it doesn't use ivars, so if you use attr_accessor in your model somewhere, you need to
180
- know how a given attribute was defined so that you can know if you should use ivars or not... terrible. Deject's functional
181
- implementation does not use ivars, you **must** use the getter and the overrider (there isn't currently a setter).
182
- That is intentional (though I used ivars in the OO implementation).
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
@@ -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? name
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
- # Not a common way of writing code in Ruby, I know.
30
- # But I tried out several implementations and found this was the easiest to
31
- # work with within the constraints of the gem (that it doesn't leave traces
32
- # of itself all over your objects)
33
- def Deject(klass)
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
@@ -1,3 +1,3 @@
1
1
  module Deject
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -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 'raises an ArgumentError error if registration clobbers a previously set value' do
35
+ it 'does not raise an ArgumentError error if registration clobbers a previously set value' do
36
36
  Deject.register(:abc){}
37
- expect { Deject.register(:abc){} }.to raise_error ArgumentError, /abc/
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.1.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: &70114295054960 !ruby/object:Gem::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: *70114295054960
24
+ version_requirements: *70254829462080
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: pry
27
- requirement: &70114295054020 !ruby/object:Gem::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: *70114295054020
35
+ version_requirements: *70254829461380
36
36
  description: Provides a super simple API for dependency injection
37
37
  email:
38
38
  - josh.cheek@gmail.com