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 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