decorum 0.0.1

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