decorum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in decorum.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 EC
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,356 @@
1
+ # Decorum
2
+
3
+ Decorum implements lightweight decorators for Ruby, called "tasteful decorators." (See below.)
4
+ It is very small, possibly very fast, and has no requirements outside of the standard library.
5
+ Use it wherever.
6
+
7
+ ## Quick Start
8
+ ```ruby
9
+ gem install decorum
10
+
11
+ class BirthdayParty
12
+ include Decorum::Decorations
13
+ end
14
+
15
+ class Confetti < Decorum::Decorator
16
+ def shoot_confetti
17
+ "boom, yay"
18
+ end
19
+ end
20
+
21
+ bp = BirthdayParty.new
22
+ bp.respond_to?(:shoot_confetti) # ==> false
23
+ bp.decorate(Confetti)
24
+ bp.shoot_confetti # ==> "boom, yay"
25
+ ```
26
+
27
+ ## Rationale
28
+
29
+ Decorum decorators are in the mold of the traditional, Gang of Four style pattern, with
30
+ a few additional conditions. They aren't a subtype of this pattern (for various reasons)
31
+ but they agree in the following way: Decorator is a family of basic object oriented
32
+ patterns which (a) are implemented with composition/delegation and (b) respect the original
33
+ public interface of the objects being decorated. As such, they're suitable for use in
34
+ any kind of Ruby program.
35
+
36
+ ### Isn't a Decorator like a Presenter which is like an HTML macro?
37
+
38
+ In Blogylvania there is considerable disagreement about what these terms entail.
39
+ [In RefineryCMS, for example](http://refinerycms.com/guides/extending-controllers-and-models-with-decorators),
40
+ "decorating" a class means opening it up with a `class_eval`. (In this conception, the decorator isn't even
41
+ an _object_, which is astonishing in Ruby.)
42
+
43
+ I use the terms as follows: a "presenter" is an object which
44
+ mediates between a model, controller, etc. and a view. A "decorator" is an object
45
+ which answers messages ostensibly bound for another object, and either responds on its behalf or
46
+ lets it do whatever it was going to in the first place.
47
+
48
+ ### What's so special about these?
49
+
50
+ Decorum decorators are like GoF decorators, but they are designed to satisfy two more constraints,
51
+ _object identity_ and _implementation consistency._
52
+
53
+ #### Object Identity
54
+
55
+ In the GoF scheme, you aren't dealing with the same object after decoration. That's fine if
56
+ it's a totally anonymous interface, but if the identity of the object is significant---if
57
+ calling objects are storing references to them, say---it can become a problem.
58
+ For example, in a common Rails idiom, if you want to do this:
59
+
60
+ ```ruby
61
+ render @user
62
+ ```
63
+
64
+ ...having already `@user = User.find(params[:id])`, you'd better remember to do this:
65
+
66
+ ```ruby
67
+ if latest_winners.include(@user.id)
68
+ @user = FreeVacationCruiseDecorator.new(@user)
69
+ end
70
+ ```
71
+ The controller has to update the reference for `@user` if it wants to decorate it.
72
+ The model's decoration status has essentially become part of the controller's state.
73
+
74
+ In Decorum, objects use decorator classes (descendents of Decorum::Decorator) to decorate themselves:
75
+
76
+ ```ruby
77
+ if latest_winners.include?(@user.id)
78
+ @user.decorate(FreeVacationCruiseDecorator, because: "You are teh awesome!")
79
+ @user.assault_with_flashing_gifs! # # ==>= that method wasn't there before!
80
+ end
81
+ ```
82
+
83
+ The "decorated object" is the same old object, because it manages all of its
84
+ state, including its decorations. References don't need to change,
85
+ and state stays where it should.
86
+
87
+ #### Implementation Consistency
88
+
89
+ By design, Decorum decorators do not override the methods of the decorated object. In general,
90
+ this seems like a bad practice. Obviously, there are edge cases, which is why this will probably
91
+ appear as an option at some point, but only with scolding. Consider:
92
+
93
+ - It risks breaking interfaces across a class. Even if the decorator respects the
94
+ method type, objects which are ostensibly identical (same database id, say) may differ
95
+ in their attributes. Suppose Bob's family calls him Skippy; with a NameDecorator, you could
96
+ have weird conditions like this:
97
+
98
+ ```ruby
99
+ work.bob == home.bob
100
+ true
101
+ work.bob.name == home.bob.name
102
+ false
103
+ ```
104
+
105
+ That may sound academic, but imagine what tracking this bug down might be like.
106
+
107
+ - More importantly: the fact that the method _needs_ overriding implies
108
+ the original object doesn't have the relevant state to fulfill it. This seems like evidence
109
+ the method belonged in the decorator to begin with.
110
+
111
+ The delegation system in Decorum gives first preference to the original object. The decorator chain
112
+ is only consulted if the original object defers the request. GoF require that Decorators respect
113
+ the object's original interface; you could say Decorum requires that they respect the original
114
+ implementation as well.
115
+
116
+ ### Tasteful Decorators
117
+ Decorators which satisfy the conditions stated earlier plus these two are "tasteful decorators,"
118
+ because they stay out of the way. (It's not a comment on other implementations. The name just
119
+ stuck.)
120
+
121
+ ## Usage
122
+
123
+ ### Helpers
124
+ The decorated object is accessible as either `#root` or `#object`. A helper method:
125
+
126
+ ```ruby
127
+ class Royalty < Human
128
+ # makes our model decoratable
129
+ include Decorum::Decorations
130
+ attr_accessor :fname, :lname, :array_of_middle_names, :array_of_styles
131
+ end
132
+
133
+ class StyledNameDecorator < Decorum::Decorator
134
+ def styled_name
135
+ parts = [:fname, :lname, :array_of_middle_names, :array_of_styles].map do |m|
136
+ root.send(m)
137
+ end.flatten
138
+
139
+ ProperOrderOfStyles.sort_and_join_this_madness(parts)
140
+ end
141
+ end
142
+
143
+ r = Royalty.find_by_palace_name(:bob)
144
+ r.respond_to? :styled_name # ==> false
145
+ r.decorate StyledNameDecorator
146
+ r.styled_name # ==> "Duke Baron His Grace Most Potent Sir Percy Arnold Robert \"Bob\" Gorpthwaite, Esq."
147
+ ```
148
+
149
+ A decorator that keeps state: (code for these is in Examples)
150
+
151
+ ```ruby
152
+ c = Coffee.new
153
+ # two milks
154
+ c.decorate(MilkDecorator, animal: "cow")
155
+ c.decorate(MilkDecorator, animal: "soycow")
156
+ # one sugar
157
+ c.decorate(SugarDecorator)
158
+ c.add_milk
159
+ c.add_sugar
160
+ c.milk_level # # ==> 2
161
+ c.sugar_level # # ==> 1
162
+ ```
163
+
164
+ Decorators are stackable, and can take an options hash. You
165
+ can declare decorator attributes in a few ways:
166
+
167
+ ```ruby
168
+ class MilkDecorator < Decorum::Decorator
169
+ attr_accessor :milk_type
170
+ share :milk_level
171
+ default_attributes animal: "cow", milk_type: "two percent"
172
+ ...
173
+ ```
174
+
175
+ `attr_accessor` works like normal, in that it gets/sets state on the
176
+ decorator; when they are called on the decorated object, the most recent
177
+ decorator that implements the method will answer it. `share` declares an
178
+ attribute that is shared across all decorators of the same class on that
179
+ object; this shared state can be used for a number of purposes. Finally,
180
+ `default_attributes` lets you set class-level defaults; these will be
181
+ preempted by options passed to the constructor.
182
+
183
+ ### Shared State
184
+
185
+ When attributes are declared with `share` (or `accumulator`), they
186
+ are shared among all decorators of that class on a given object:
187
+ if an object has three MilkDecorators, the `#milk_level`/`#milk_level=` methods
188
+ literally access the same state on all three.
189
+ In addition, you get `#milk_level?` and `#reset_milk_level` to
190
+ perform self-evident functions.
191
+
192
+ Access to the shared state is proxied first through the root object,
193
+ and then through an instance of Decorum::DecoratedState, before
194
+ ultimately pointing to an instance of Decorum::SuperHash. (SuperHash
195
+ is used for a few things---see the source. It's
196
+ normally OpenStruct, to limit Decorum's dependencies to the standard
197
+ library, but you can override it; I use Hashr personally.)
198
+
199
+ In the examples above and below, shared state is mainly used to
200
+ accumulate results, like in `#milk_level`. It can also be used for
201
+ other things:
202
+ - Serialize it, stick it in an HTML `data` attribute, and use it
203
+ to initailize Javascript applications
204
+ - Store a Rails view context for rendering
205
+
206
+ ...or for more esoteric purposes:
207
+
208
+ - Provide context-specific response selections for decorators, e.g.,
209
+ `return current_shared_responder.message(my_condition)`
210
+ - Implement polymorphic factories as decorators by storing references to classes
211
+
212
+ And so on.
213
+
214
+ ### `#decorated_tail`
215
+
216
+ How exactly did the first MilkDecorator know to keep passing `#add_milk`
217
+ down the chain instead of returning, you ask? In general, the decision
218
+ whether to return directly or to pass the request down the chain for further
219
+ input rests with the decorator itself. Cumulative decorators, like the milk example,
220
+ can be implemented in Decorum with a form of tail recursion:
221
+
222
+ ```ruby
223
+ class MilkDecorator < Decorum::Decorator
224
+ share :milk_level
225
+ ...
226
+ def add_milk
227
+ self.milk_level = milk_level.to_i + 1
228
+ decorated_tail(milk_level) { next_link.add_milk }
229
+ end
230
+ end
231
+ ```
232
+
233
+ Because `milk_level` is shared across all of the instances of
234
+ MilkDecorator attached to the current cup of coffee, each
235
+ decorator can update it individually. The "tail call" actually
236
+ goes down the decorator chain, and is picked up by the next
237
+ decorator that implements the method. The call is wrapped
238
+ in `#decorated_tail`, which will catch the end of the decorator
239
+ chain, and return its argument; in this case, `#milk_level` called
240
+ on the final instance, so, the total amount of milk in the coffee.
241
+ The state is saved, and because it's shared among all the MilkDecorators,
242
+ the most recent one on the chain can service the getter method
243
+ like a normal decorated attribute.
244
+
245
+ For a standard demonstration of tail recursion in Decorum, see
246
+ Decorum::Examples::FibonacciDecorator:
247
+
248
+ ```ruby
249
+ fibber = SomeDecoratableClass.new
250
+ # generate the first 100 terms of the Fibonacci sequence
251
+ 100.times do
252
+ fibber.decorate(FibonacciDecorator)
253
+ end
254
+ # call it
255
+ fibber.fib # ==> 927372692193078999176
256
+ # it stores both the return and the sequence in shared state:
257
+ fibber.sequence.length == 100 # ==> true
258
+ fibber.current # ==> 927372692193078999176
259
+ ```
260
+
261
+ `#decorated_tail` can be used to produce other results.
262
+ Normally, methods are handled by the most recent decorator in the chain
263
+ to implement it. To give the method to the _oldest_ decorator to
264
+ implement it, call `#decorated_tail` with a non-shared attribute/method:
265
+
266
+ ```ruby
267
+ class MilkDecorator
268
+ attr_accessor :animal
269
+ ...
270
+ def first_animal
271
+ decorated_tail(animal) { next_link.first_animal }
272
+ end
273
+ end
274
+ ```
275
+
276
+ This returns the animal responsible for the first MilkDecorator
277
+ Bob took. Or call it with some other object:
278
+
279
+ ```ruby
280
+ def all_animals(animals=[])
281
+ animals << animal
282
+ decorated_tail(animals) { next_link.all_animals(animals) }
283
+ end
284
+ ```
285
+
286
+ This will return a list of all of the animals who have contributed milk
287
+ to Bob's coffee.
288
+
289
+ If such a method returns normally, `#decorated_tail` will return that
290
+ value instead, enabling Chain of Responsibility-looking things like this:
291
+ (sorry, no code in the examples for this one)
292
+
293
+ ```ruby
294
+ [ErrorHandler, SuccessHandler].each do |handler|
295
+ @agent.decorate(handler)
296
+ end
297
+ this_service = find_service_decorator(params) # # ==> SomeServiceHandler
298
+ @agent.decorate(this_service)
299
+ @agent.service_request(params)
300
+
301
+ # meanwhile:
302
+
303
+ class SomeServiceHandler < Decorum::Decorators
304
+ def service_request
305
+ status = perform_request_on(object)
306
+ if status
307
+ decorated_tail(DefaultSuccess.new) { next_link.special_success_method("Outstanding!") }
308
+ else
309
+ decorated_tail(DefaultFailure.new) { next_link.special_failure_method("uh-oh") }
310
+ end
311
+ end
312
+ end
313
+ ```
314
+
315
+ You can now parameterize your responses based on whatever conditions you like,
316
+ by loading different decorators before the request is serviced. If nobody claims
317
+ the specialized method, your default will be returned instead.
318
+
319
+ ### It's Decorators All the Way Down
320
+
321
+ Decorum includes a class called Decorum::BareParticular, which descends from
322
+ SuperHash. You can initialize any values you like on it, call them as methods,
323
+ and any method it doesn't understand will return nil. The only other distinguishing
324
+ feature of this class is that it can be decorated, so you can create objects
325
+ whose interfaces are defined entirely by their decorators, and which will
326
+ return nil by default.
327
+
328
+ ## to-do
329
+ A few things I can imagine showing up soon:
330
+ - Namespaced decorators, probably showing up as a method on the root object,
331
+ e.g., `object.my_namespace.namespaced_method`
332
+ - Thread safety: probably not an issue if you're retooling your Rails helpers,
333
+ but consider a use case like this:
334
+
335
+ ```ruby
336
+ 10.times do
337
+ my_decorator = nil # scope the name
338
+ @object.decorate(RequestHandler) { |d| my_decorator = d }
339
+ Thread.new do
340
+ my_decorator.listen_for_changes_to_shared_state(...)
341
+ end
342
+ end
343
+ ```
344
+
345
+ - Easy subclassing of Decorum::DecoratedState
346
+
347
+ &c. I'm open to suggestion.
348
+
349
+ ## Contributing
350
+ I wrote most of this super late at night, so that would be awesome:
351
+
352
+ 1. Fork it
353
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
354
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
355
+ 4. Push to the branch (`git push origin my-new-feature`)
356
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/decorum.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'decorum/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "decorum"
8
+ spec.version = Decorum::VERSION
9
+ spec.authors = ["Erik Cameron"]
10
+ spec.email = ["erik.cameron@gmail.com"]
11
+ spec.description = %q{Tasteful decorators for Ruby. Use it wherever.}
12
+ spec.summary = %q{Decorum implements the Decorator pattern (more or less) in a fairly unobtrusive way.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rspec", "~> 2.14"
23
+ spec.add_development_dependency "rake"
24
+ end
@@ -0,0 +1,7 @@
1
+ module Decorum
2
+ module Examples
3
+ class Coffee
4
+ include Decorum::Decorations
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module Decorum
2
+ module Examples
3
+ class FibonacciDecorator < Decorum::Decorator
4
+ accumulator :sequence
5
+ accumulator :current
6
+
7
+ def fib(a = nil, b = nil)
8
+ unless a && b
9
+ reset_current
10
+ self.sequence = []
11
+ a = 1
12
+ b = 1
13
+ end
14
+
15
+ self.current = a + b
16
+ self.sequence << current
17
+
18
+ decorated_tail(current) do
19
+ next_link.fib(b, current)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end