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 +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +356 -0
- data/Rakefile +1 -0
- data/decorum.gemspec +24 -0
- data/examples/coffee.rb +7 -0
- data/examples/fibonacci_decorator.rb +24 -0
- data/examples/milk_decorator.rb +29 -0
- data/examples/sugar_decorator.rb +12 -0
- data/lib/decorum.rb +14 -0
- data/lib/decorum/bare_particular.rb +5 -0
- data/lib/decorum/chain_stop.rb +7 -0
- data/lib/decorum/decorated_state.rb +29 -0
- data/lib/decorum/decorations.rb +101 -0
- data/lib/decorum/decorator.rb +85 -0
- data/lib/decorum/version.rb +3 -0
- data/spec/integration/coffee_spec.rb +41 -0
- data/spec/integration/fibonacci_spec.rb +20 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/decorated_state/shared_state_stub.rb +15 -0
- data/spec/support/decorations/decorated_object_stub.rb +17 -0
- data/spec/support/decorations/first_decorator.rb +20 -0
- data/spec/support/decorations/second_decorator.rb +20 -0
- data/spec/support/decorations/third_decorator.rb +20 -0
- data/spec/support/decorator/basic_decorator.rb +20 -0
- data/spec/support/decorator/decorated_object_stub.rb +17 -0
- data/spec/support/decorator/decorated_state_stub.rb +11 -0
- data/spec/support/decorator/decorator_stub.rb +11 -0
- data/spec/unit/bare_particular_spec.rb +13 -0
- data/spec/unit/chain_stop_spec.rb +12 -0
- data/spec/unit/decorated_state_spec.rb +31 -0
- data/spec/unit/decorations_spec.rb +233 -0
- data/spec/unit/decorator_spec.rb +166 -0
- metadata +146 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
|
data/examples/coffee.rb
ADDED
@@ -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
|