decorum 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +8 -0
- data/README.md +212 -86
- data/decorum.gemspec +1 -1
- data/examples/strong_willed_decorator.rb +25 -0
- data/examples/weak_willed_class.rb +15 -0
- data/lib/decorum/decorations.rb +16 -1
- data/lib/decorum/decorator.rb +9 -0
- data/lib/decorum/version.rb +1 -1
- data/spec/integration/immediate_methods_spec.rb +33 -0
- data/spec/unit/decorator_spec.rb +36 -1
- metadata +8 -3
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# Decorum
|
2
2
|
|
3
|
-
Decorum implements lightweight decorators
|
4
|
-
|
5
|
-
|
3
|
+
Decorum implements lightweight decorators
|
4
|
+
for Ruby, called "tasteful decorators." (See below.) It is very small,
|
5
|
+
possibly very fast, and has no requirements outside of the standard
|
6
|
+
library. Use it wherever.
|
6
7
|
|
7
8
|
## Quick Start
|
8
9
|
```ruby
|
@@ -23,102 +24,204 @@ bp.respond_to?(:shoot_confetti) # ==> false
|
|
23
24
|
bp.decorate(Confetti)
|
24
25
|
bp.shoot_confetti # ==> "boom, yay"
|
25
26
|
```
|
27
|
+
## About
|
26
28
|
|
27
|
-
|
29
|
+
[Skip to the action](#usage)
|
28
30
|
|
29
|
-
Decorum
|
30
|
-
|
31
|
-
|
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.
|
31
|
+
Decorum expands on the traditional Decorator concept by satisfying [a few additional
|
32
|
+
contraints](#tasteful_decorators). The constraints are designed to make decorators' role in your
|
33
|
+
overall object structure both clear and safe. More on these points below.
|
35
34
|
|
36
|
-
|
35
|
+
- Object Identity: After you decorate an object, you're still dealing with that same
|
36
|
+
object.
|
37
|
+
- Defers to the original interface: by default, Decorum decorators will _not_ override
|
38
|
+
the decorated object's public methods. (Though you can instruct it to.) This is intentional.
|
39
|
+
- Respects existing overrides of `#method_missing`
|
40
|
+
- Decorators are unloadable
|
41
|
+
|
42
|
+
By adhering to these constraints, decorators tend to do the Right Thing, i.e, integrate into existing
|
43
|
+
applications easily, and stay out of the way when they aren't doing your bidding. Hence "[tasteful
|
44
|
+
decorators](#tasteful_decorators)." (Not meant to imply others are tacky. The name just stuck.)
|
45
|
+
|
46
|
+
In addition, Decorum provides a few helpful features:
|
47
|
+
|
48
|
+
- Stackable decorators, with shared state
|
49
|
+
- Recursion, via `#decorated_tail`
|
50
|
+
- Intercept/change messages
|
51
|
+
- Build stuff entirely out of decorators
|
52
|
+
|
53
|
+
As an example of how this is in use right now, suppose you're interfacing a content
|
54
|
+
management system with an existing data application. You want to build a sidebar
|
55
|
+
of image links. The images are in the CMS, but their metadata are stored in the application.
|
56
|
+
You want those systems to stay uncoupled. You can use a decorator to slap the metadata
|
57
|
+
on the image at runtime, e.g.,:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
image_collection = Application::ImageData.sidebar_images
|
61
|
+
# say this returns a hash keyed by identifier:
|
62
|
+
# { blah: { url: 'http://blah.foo/', alt: 'The Blah Conglomerate' ... }}
|
63
|
+
images = Cms::Images.where(identifier: image_collection.keys)
|
64
|
+
|
65
|
+
images.each do |img|
|
66
|
+
img.decorate(ImageMetaDecorator, image_collection[img.identifier])
|
67
|
+
end
|
68
|
+
|
69
|
+
images[0].url # ==> 'http://blah.foo'
|
70
|
+
images[0].alt # ==> 'The Blah Conglomerate'
|
71
|
+
```
|
37
72
|
|
38
|
-
|
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.)
|
73
|
+
### Isn't a Decorator like a Presenter which is like an HTML macro?
|
42
74
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
75
|
+
In Blogylvania there is some disagreement
|
76
|
+
about what these terms entail. [For example, in
|
77
|
+
RefineryCMS](http://refinerycms.com/guides/extending-controllers-and-models-with-decorators),
|
78
|
+
"decorating" a class means opening it up with a `class_eval`. (In this
|
79
|
+
conception, the decorator isn't even an object, which is astonishing
|
80
|
+
in Ruby.) I use the terms as follows: a "presenter" is an object which
|
81
|
+
mediates between a model, controller, etc. and a view. A "decorator" is
|
82
|
+
an object which answers messages ostensibly bound for another object, and
|
83
|
+
either responds on its behalf or lets it do whatever it was going to in
|
84
|
+
the first place. Presenters may or may not be implemented as Decorators;
|
85
|
+
Decorators may or may not present.
|
47
86
|
|
48
|
-
|
87
|
+
Like "traditional" (i.e., [Gang of Four](http://en.wikipedia.org/wiki/Design_Patterns)-style)
|
88
|
+
decorator patterns, Decorum is a general purpose, object-oriented tool. Use it wherever.
|
49
89
|
|
50
|
-
|
51
|
-
_object identity_ and _implementation consistency._
|
90
|
+
### <a name="#tasteful_decorators"></a>Tasteful Decorators
|
52
91
|
|
53
92
|
#### Object Identity
|
54
93
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
94
|
+
Decorators, as conceived of by GoF, Python, etc., masquerade as the
|
95
|
+
objects they decorate by responding to their interface. They are _not_ the
|
96
|
+
original objects themselves. This may or may not be a problem, depending
|
97
|
+
on how your app is structured. In general though, (I doubt this is news)
|
98
|
+
it risks breaking encapsulation. Any code which stores direct references
|
99
|
+
to the original object will have to update them to get the decorated
|
100
|
+
behavior. For example, in a common Rails idiom, in order to do this:
|
59
101
|
|
60
102
|
```ruby
|
61
103
|
render @user
|
62
104
|
```
|
63
105
|
|
64
|
-
...having already `@user = User.find(params[:id])`, you
|
106
|
+
...having already `@user = User.find(params[:id])`, you have to do this:
|
65
107
|
|
66
108
|
```ruby
|
67
109
|
if latest_winners.include(@user.id)
|
68
110
|
@user = FreeVacationCruiseDecorator.new(@user)
|
69
111
|
end
|
70
112
|
```
|
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
113
|
|
114
|
+
`@user` is an instance variable of the controller, but it has to be
|
115
|
+
updated in order for the model to be decorated. In practical terms,
|
116
|
+
if you store multiple references to the same object, (say the
|
117
|
+
original object is in an array somewhere, in addition to `@user`)
|
118
|
+
you have to update both references to get consistent behavior. The model's
|
119
|
+
decoration status has essentially become part of the controller's state.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
users.include?(@user) # ==> true
|
123
|
+
winning_users = users.map { |u| FreeVacationCruiseDecorator.new(u) }
|
124
|
+
decorated = winning_users.detect { |u| u.id == @user.id } # the decorated object---should be the "same" thing
|
125
|
+
decorated.destination # ==> "tahiti"
|
126
|
+
@user.destination # ==> NoMethodError
|
127
|
+
```
|
74
128
|
In Decorum, objects use decorator classes (descendents of Decorum::Decorator) to decorate themselves:
|
75
129
|
|
76
130
|
```ruby
|
77
|
-
|
78
|
-
|
79
|
-
@user.assault_with_flashing_gifs! # # ==>= that method wasn't there before!
|
131
|
+
users.each do |user|
|
132
|
+
user.decorate(FreeVacationCruiseDecorator, because: "You are teh awesome!")
|
80
133
|
end
|
134
|
+
|
135
|
+
@user.assault_with_flashing_gifs! # ==> that method wasn't there before!
|
81
136
|
```
|
82
137
|
|
83
138
|
The "decorated object" is the same old object, because it manages all of its
|
84
139
|
state, including its decorations. References don't need to change,
|
85
140
|
and state stays where it should.
|
86
141
|
|
87
|
-
####
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
142
|
+
#### Defers to the original interface
|
143
|
+
|
144
|
+
Unless instructed otherwise, Decorum will not override existing methods
|
145
|
+
on the decorated object. In practice, this might be useful, (see below
|
146
|
+
for how to instruct it otherwise) but from a design standpoint, it
|
147
|
+
looks to me like an anti-pattern. The fact that the method _needs_
|
148
|
+
overriding implies the original object doesn't have the relevant state to
|
149
|
+
fulfill it. The method is now spread out over two classes, that of the original
|
150
|
+
object and that of the decorator.
|
151
|
+
|
152
|
+
The paradigm cases of decorators don't generally address this, either. Consider three
|
153
|
+
common examples:
|
154
|
+
|
155
|
+
- Adding a scrollbar to a window
|
156
|
+
- Adding milk to a cup of coffee
|
157
|
+
- Providing `#full_name` to an object that supplies `#first_name` and `#last_name`
|
158
|
+
|
159
|
+
In all of these cases, the decorators provide some new functionality;
|
160
|
+
they don't change the object's original implementation. Obviously, you
|
161
|
+
shouldn't rule it out just because the common examples don't have it,
|
162
|
+
but it's by no means an essential use of the pattern. And from a design
|
163
|
+
perspective, it's a red flag that concerns are becoming... unseparated.
|
164
|
+
(It also risks weird bugs by breaking transparency, i.e., you can have
|
165
|
+
cases where `a` and `b` are literally identical, but have different
|
166
|
+
attributes.)
|
167
|
+
|
168
|
+
When an object is decorated, Decorum inserts its own `#method_missing`
|
169
|
+
and `#respond_to_missing?` into the object's eigenclass. Decorum's `#method_missing`
|
170
|
+
is only consulted after the original object has abandoned the message. (When
|
171
|
+
overriding original methods with `immediate`, each method gets its own
|
172
|
+
redirect in the eigenclass as well, intercepting the message before the
|
173
|
+
original definition is found.)
|
174
|
+
|
175
|
+
#### Respects existing overrides of `#method_missing`
|
176
|
+
|
177
|
+
A sizeable amount of the world's total Ruby functionality is implemented
|
178
|
+
by overriding `#method_missing`, so decorators shouldn't get in the way.
|
179
|
+
Because Decorum intercepts messages in the objects eigenclass, it also
|
180
|
+
respects existing overrides. If the decorator chain doesn't claim the
|
181
|
+
message, `super` is called and lookup proceeds normally.
|
182
|
+
|
183
|
+
#### Unloadable decorators
|
184
|
+
|
185
|
+
Decorators can be unloaded, if necessary. (The following case illustrates
|
186
|
+
this, and the need for callbacks, e.g., `#after_decorate`. Definitely one
|
187
|
+
my next priorities.)
|
97
188
|
|
98
189
|
```ruby
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
190
|
+
@bob.decorate(IsTheMole)
|
191
|
+
@bob.revoke_security_clearances
|
192
|
+
# ideally this would be called automatically by IsTheMole
|
193
|
+
# on decoration
|
194
|
+
|
195
|
+
class IsTheMole < Decorum::Decorators
|
196
|
+
def revoke_security_clearances
|
197
|
+
clearances = object.decorators.select { |d| d.is_a?(SecurityClearance) }
|
198
|
+
clearances.each { |clearance| object.undecorate(clearance) }
|
199
|
+
end
|
200
|
+
end
|
103
201
|
```
|
104
202
|
|
105
|
-
|
203
|
+
### Implementation
|
106
204
|
|
107
|
-
|
108
|
-
|
109
|
-
the
|
205
|
+
As in other implementations, Decorum decorators wrap another object
|
206
|
+
to which they forward unknown messages. In both cases, all decorators
|
207
|
+
other than the first wrap another decorator. Instead of wrapping the
|
208
|
+
original object however, the first decorator in Decorum wraps an instance
|
209
|
+
of Decorum::ChainStop. If a method reaches the bottom of the chain, this
|
210
|
+
object throws a signal back up the stack to Decorum's `#method_missing`,
|
211
|
+
which then calls `super.` (This is a throw/catch, not an exception,
|
212
|
+
which would be significantly slower.)
|
110
213
|
|
111
|
-
|
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.
|
214
|
+
See the source for more details.
|
115
215
|
|
116
|
-
|
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.)
|
216
|
+
## <a name="usage"></a> Usage
|
120
217
|
|
121
|
-
|
218
|
+
First, objects need to be decoratable. You can do this for a whole class,
|
219
|
+
by including Decorum::Decorations, or for a single object, by extending it.
|
220
|
+
(Note: this alone doesn't change the object's method lookup. It just makes
|
221
|
+
`#decorate` available on the object. The behavior is only changed when
|
222
|
+
`#decorate` is called.) The easiest method is probably including
|
223
|
+
Decorum::Decorations at whatever point(s) of the class hierarchy you
|
224
|
+
feel appropriate.
|
122
225
|
|
123
226
|
### Helpers
|
124
227
|
The decorated object is accessible as either `#root` or `#object`. A helper method:
|
@@ -132,18 +235,14 @@ end
|
|
132
235
|
|
133
236
|
class StyledNameDecorator < Decorum::Decorator
|
134
237
|
def styled_name
|
135
|
-
|
136
|
-
root.send(m)
|
137
|
-
end.flatten
|
138
|
-
|
139
|
-
ProperOrderOfStyles.sort_and_join_this_madness(parts)
|
238
|
+
ProperOrderOfStyles.sort_and_join_this_madness(object)
|
140
239
|
end
|
141
240
|
end
|
142
241
|
|
143
242
|
r = Royalty.find_by_palace_name(:bob)
|
144
243
|
r.respond_to? :styled_name # ==> false
|
145
244
|
r.decorate StyledNameDecorator
|
146
|
-
r.styled_name # ==> "
|
245
|
+
r.styled_name # ==> "His Grace Most Potent Baron Sir Percy Arnold Robert \"Bob\" Gorpthwaite, Esq."
|
147
246
|
```
|
148
247
|
|
149
248
|
A decorator that keeps state: (code for these is in Examples)
|
@@ -180,6 +279,16 @@ object; this shared state can be used for a number of purposes. Finally,
|
|
180
279
|
`default_attributes` lets you set class-level defaults; these will be
|
181
280
|
preempted by options passed to the constructor.
|
182
281
|
|
282
|
+
As a side note, you can disable another decorators methods thus:
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
class MethodDisabler < Decorum::Decorator
|
286
|
+
def method_to_be_disabled(*args)
|
287
|
+
throw :chain_stop, Decorum::ChainStop.new
|
288
|
+
end
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
183
292
|
### Shared State
|
184
293
|
|
185
294
|
When attributes are declared with `share` (or `accumulator`), they
|
@@ -187,7 +296,7 @@ are shared among all decorators of that class on a given object:
|
|
187
296
|
if an object has three MilkDecorators, the `#milk_level`/`#milk_level=` methods
|
188
297
|
literally access the same state on all three.
|
189
298
|
In addition, you get `#milk_level?` and `#reset_milk_level` to
|
190
|
-
perform self-
|
299
|
+
perform self-eviYou can insert this at whatever point in the dent functions.
|
191
300
|
|
192
301
|
Access to the shared state is proxied first through the root object,
|
193
302
|
and then through an instance of Decorum::DecoratedState, before
|
@@ -202,19 +311,15 @@ other things:
|
|
202
311
|
- Serialize it, stick it in an HTML `data` attribute, and use it
|
203
312
|
to initailize Javascript applications
|
204
313
|
- Store a Rails view context for rendering
|
205
|
-
|
206
|
-
...or for more esoteric purposes:
|
207
|
-
|
208
314
|
- Provide context-specific response selections for decorators, e.g.,
|
209
315
|
`return current_shared_responder.message(my_condition)`
|
210
|
-
- Implement polymorphic factories as decorators by storing references to classes
|
211
316
|
|
212
317
|
And so on.
|
213
318
|
|
214
319
|
### `#decorated_tail`
|
215
320
|
|
216
|
-
How exactly did the first MilkDecorator
|
217
|
-
down the chain instead of returning
|
321
|
+
How exactly did the first MilkDecorator pass `#add_milk`
|
322
|
+
down the chain instead of returning? In general, the decision
|
218
323
|
whether to return directly or to pass the request down the chain for further
|
219
324
|
input rests with the decorator itself. Cumulative decorators, like the milk example,
|
220
325
|
can be implemented in Decorum with a form of tail recursion:
|
@@ -242,7 +347,7 @@ The state is saved, and because it's shared among all the MilkDecorators,
|
|
242
347
|
the most recent one on the chain can service the getter method
|
243
348
|
like a normal decorated attribute.
|
244
349
|
|
245
|
-
For a
|
350
|
+
For a demonstration of tail recursion in Decorum, see
|
246
351
|
Decorum::Examples::FibonacciDecorator:
|
247
352
|
|
248
353
|
```ruby
|
@@ -291,15 +396,15 @@ value instead, enabling Chain of Responsibility-looking things like this:
|
|
291
396
|
(sorry, no code in the examples for this one)
|
292
397
|
|
293
398
|
```ruby
|
294
|
-
[
|
399
|
+
handlers = condition ? [ErrorA, SuccessA] : [ErrorB, SuccessB]
|
400
|
+
handlers.each do |handler|
|
295
401
|
@agent.decorate(handler)
|
296
402
|
end
|
297
|
-
this_service =
|
403
|
+
this_service = determine_service_decorator(params) # # ==> SomeServiceHandler
|
298
404
|
@agent.decorate(this_service)
|
299
405
|
@agent.service_request(params)
|
300
406
|
|
301
407
|
# meanwhile:
|
302
|
-
|
303
408
|
class SomeServiceHandler < Decorum::Decorators
|
304
409
|
def service_request
|
305
410
|
status = perform_request_on(object)
|
@@ -316,7 +421,27 @@ You can now parameterize your responses based on whatever conditions you like,
|
|
316
421
|
by loading different decorators before the request is serviced. If nobody claims
|
317
422
|
the specialized method, your default will be returned instead.
|
318
423
|
|
319
|
-
###
|
424
|
+
### Overriding existing methods
|
425
|
+
|
426
|
+
To give decorator methods preference over an objects existing methods (if you
|
427
|
+
must) declare the method `immediate`:
|
428
|
+
|
429
|
+
```ruby
|
430
|
+
class StrongWilledDecorator < Decorum::Decorator
|
431
|
+
immediate :method_in_question
|
432
|
+
|
433
|
+
def method_in_question
|
434
|
+
"overridden"
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
x = WeakWilledClass.new
|
439
|
+
x.method_in_question # <== "original"
|
440
|
+
x.decorate(StrongWilledDecorator)
|
441
|
+
x.method_in_question # <== "overridden"
|
442
|
+
```
|
443
|
+
|
444
|
+
### Decorators All the Way Down
|
320
445
|
|
321
446
|
Decorum includes a class called Decorum::BareParticular, which descends from
|
322
447
|
SuperHash. You can initialize any values you like on it, call them as methods,
|
@@ -325,29 +450,30 @@ feature of this class is that it can be decorated, so you can create objects
|
|
325
450
|
whose interfaces are defined entirely by their decorators, and which will
|
326
451
|
return nil by default.
|
327
452
|
|
328
|
-
##
|
453
|
+
## To-do
|
329
454
|
A few things I can imagine showing up soon:
|
455
|
+
- Probably the most important thing is before/after callbacks for decoration
|
456
|
+
and undecoration.
|
330
457
|
- Namespaced decorators, probably showing up as a method on the root object,
|
331
458
|
e.g., `object.my_namespace.namespaced_method`
|
332
459
|
- Thread safety: probably not an issue if you're retooling your Rails helpers,
|
333
|
-
but consider a
|
460
|
+
but consider a case like this:
|
334
461
|
|
335
462
|
```ruby
|
336
|
-
10.times do
|
337
|
-
|
338
|
-
@
|
463
|
+
10.times do |i|
|
464
|
+
port = port_base + i # 3001, 3002, 3003...
|
465
|
+
@server.decorate(RequestHandler, port: port )
|
339
466
|
Thread.new do
|
340
|
-
|
467
|
+
@server.listen_for_changes_to_shared_state(port: port)
|
341
468
|
end
|
342
469
|
end
|
343
470
|
```
|
344
|
-
|
345
|
-
- Easy subclassing of Decorum::DecoratedState
|
471
|
+
- Easy subclassing of Decorum::DecoratedState, so you can do wacky things with it
|
346
472
|
|
347
473
|
&c. I'm open to suggestion.
|
348
474
|
|
349
475
|
## Contributing
|
350
|
-
I wrote most of this super late at night, so that would be awesome:
|
476
|
+
I wrote most of this super late at night, (don't worry, the tests pass) so that would be awesome:
|
351
477
|
|
352
478
|
1. Fork it
|
353
479
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
data/decorum.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["erik.cameron@gmail.com"]
|
11
11
|
spec.description = %q{Tasteful decorators for Ruby. Use it wherever.}
|
12
12
|
spec.summary = %q{Decorum implements the Decorator pattern (more or less) in a fairly unobtrusive way.}
|
13
|
-
spec.homepage = ""
|
13
|
+
spec.homepage = "http://erikcameron.github.io/"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Decorum
|
2
|
+
module Examples
|
3
|
+
class StrongWilledDecorator < Decorum::Decorator
|
4
|
+
immediate :method_in_question
|
5
|
+
immediate :second_immediate_method, :third_immediate_method
|
6
|
+
immediate :fourth_immediate_method
|
7
|
+
|
8
|
+
def method_in_question
|
9
|
+
"overridden"
|
10
|
+
end
|
11
|
+
|
12
|
+
def second_immediate_method
|
13
|
+
"method dos"
|
14
|
+
end
|
15
|
+
|
16
|
+
def third_immediate_method
|
17
|
+
"method tres"
|
18
|
+
end
|
19
|
+
|
20
|
+
def fourth_immediate_method
|
21
|
+
"method quatro"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/decorum/decorations.rb
CHANGED
@@ -73,12 +73,27 @@ module Decorum
|
|
73
73
|
|
74
74
|
base = @_decorator_chain || Decorum::ChainStop.new
|
75
75
|
@_decorator_chain = klass.new(base, self, options)
|
76
|
+
|
77
|
+
if klass.immediate_methods
|
78
|
+
immediate = Module.new do
|
79
|
+
klass.immediate_methods.each do |method_name|
|
80
|
+
define_method(method_name) do |*args, &block|
|
81
|
+
response = catch :chain_stop do
|
82
|
+
@_decorator_chain.send(__method__, *args, &block)
|
83
|
+
end
|
84
|
+
response.is_a?(Decorum::ChainStop) ? super(*args, &block) : response
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
extend immediate
|
89
|
+
end
|
90
|
+
|
76
91
|
decorators!
|
77
92
|
@_decorator_chain
|
78
93
|
end
|
79
94
|
|
80
95
|
def remove_from_decorator_chain(decorator)
|
81
|
-
return nil unless decorators.include?(decorator)
|
96
|
+
return nil unless decorator.is_a?(Decorum::Decorator) && decorators.include?(decorator)
|
82
97
|
|
83
98
|
if decorator == @_decorator_chain
|
84
99
|
@_decorator_chain = decorator.next_link
|
data/lib/decorum/decorator.rb
CHANGED
@@ -80,6 +80,15 @@ module Decorum
|
|
80
80
|
def get_default_attributes
|
81
81
|
@default_attributes || {}
|
82
82
|
end
|
83
|
+
|
84
|
+
# allow Decorator classes to override the decorated object's
|
85
|
+
# public methods (tsk tsk)
|
86
|
+
def immediate(*method_names)
|
87
|
+
@immediate_methods ||= []
|
88
|
+
@immediate_methods += method_names
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :immediate_methods
|
83
92
|
end
|
84
93
|
end
|
85
94
|
end
|
data/lib/decorum/version.rb
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "When overriding original methods with .immediate" do
|
4
|
+
let(:base_object) { Decorum::Examples::WeakWilledClass.new }
|
5
|
+
|
6
|
+
it "has an original method" do
|
7
|
+
expect(base_object.method_in_question).to eq("original")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "overrides original methods" do
|
11
|
+
base_object.decorate(Decorum::Examples::StrongWilledDecorator)
|
12
|
+
expect(base_object.method_in_question).to eq("overridden")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "reverts to original method when decorator is unloaded" do
|
16
|
+
base_object.decorate(Decorum::Examples::StrongWilledDecorator)
|
17
|
+
base_object.undecorate(base_object.decorators.first)
|
18
|
+
expect(base_object.method_in_question).to eq("original")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "stops chain on vanished method" do
|
22
|
+
base_object.decorate(Decorum::Examples::StrongWilledDecorator)
|
23
|
+
# raise on violated assumptions rather than have multiple conditions in the same test?
|
24
|
+
resp = base_object.second_immediate_method
|
25
|
+
unless resp == "method dos"
|
26
|
+
bail_message = "Bad test data: base_object doesn't have #second_immediate_method, got #{resp}"
|
27
|
+
raise bail_message
|
28
|
+
end
|
29
|
+
|
30
|
+
base_object.undecorate(base_object.decorators.first)
|
31
|
+
expect(base_object.second_immediate_method).to eq("class method_missing")
|
32
|
+
end
|
33
|
+
end
|
data/spec/unit/decorator_spec.rb
CHANGED
@@ -15,6 +15,22 @@ describe Decorum::Decorator do
|
|
15
15
|
it 'responds to .accumulator' do
|
16
16
|
expect(Decorum::Decorator.respond_to?(:accumulator)).to be_true
|
17
17
|
end
|
18
|
+
|
19
|
+
it 'responds to .default_attributes' do
|
20
|
+
expect(Decorum::Decorator.respond_to?(:default_attributes)).to be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'responds to .get_default_attributes' do
|
24
|
+
expect(Decorum::Decorator.respond_to?(:get_default_attributes)).to be_true
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'responds to .immediate' do
|
28
|
+
expect(Decorum::Decorator.respond_to?(:immediate)).to be_true
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'responds to .immediate_methods' do
|
32
|
+
expect(Decorum::Decorator.respond_to?(:immediate_methods)).to be_true
|
33
|
+
end
|
18
34
|
|
19
35
|
describe '#decorated_state' do
|
20
36
|
it 'defers to the root object' do
|
@@ -129,7 +145,7 @@ describe Decorum::Decorator do
|
|
129
145
|
end
|
130
146
|
end
|
131
147
|
|
132
|
-
context 'when attributes
|
148
|
+
context 'when attributes are declared personally' do
|
133
149
|
describe '#setter' do
|
134
150
|
it 'sets local attribute via initialize' do
|
135
151
|
expect(decorator.name).to eq('bob')
|
@@ -163,4 +179,23 @@ describe Decorum::Decorator do
|
|
163
179
|
expect(response).to be_a(Decorum::ChainStop)
|
164
180
|
end
|
165
181
|
end
|
182
|
+
|
183
|
+
context 'when methods are declared immediate' do
|
184
|
+
it 'includes them in @immediate_methods' do
|
185
|
+
expect(Decorum::Examples::StrongWilledDecorator.immediate_methods.include?(:method_in_question)).to be_true
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'respects various forms of declaration' do
|
189
|
+
# i.e.:
|
190
|
+
# - it respects mulitple immediate declarations
|
191
|
+
# - it respects single methods or an array of methods
|
192
|
+
# see Examples::StrongWilledDecorator
|
193
|
+
methods = ["second", "third", "fourth"].map do |pre|
|
194
|
+
"#{pre}_immediate_method".to_sym
|
195
|
+
end
|
196
|
+
|
197
|
+
got_em = methods.map { |m| Decorum::Examples::StrongWilledDecorator.immediate_methods.include?(m) }.inject(:&)
|
198
|
+
expect(got_em).to be_true
|
199
|
+
end
|
200
|
+
end
|
166
201
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: decorum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-01-
|
12
|
+
date: 2014-01-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -67,6 +67,7 @@ extensions: []
|
|
67
67
|
extra_rdoc_files: []
|
68
68
|
files:
|
69
69
|
- .gitignore
|
70
|
+
- CHANGELOG.md
|
70
71
|
- Gemfile
|
71
72
|
- LICENSE.txt
|
72
73
|
- README.md
|
@@ -75,7 +76,9 @@ files:
|
|
75
76
|
- examples/coffee.rb
|
76
77
|
- examples/fibonacci_decorator.rb
|
77
78
|
- examples/milk_decorator.rb
|
79
|
+
- examples/strong_willed_decorator.rb
|
78
80
|
- examples/sugar_decorator.rb
|
81
|
+
- examples/weak_willed_class.rb
|
79
82
|
- lib/decorum.rb
|
80
83
|
- lib/decorum/bare_particular.rb
|
81
84
|
- lib/decorum/chain_stop.rb
|
@@ -85,6 +88,7 @@ files:
|
|
85
88
|
- lib/decorum/version.rb
|
86
89
|
- spec/integration/coffee_spec.rb
|
87
90
|
- spec/integration/fibonacci_spec.rb
|
91
|
+
- spec/integration/immediate_methods_spec.rb
|
88
92
|
- spec/spec_helper.rb
|
89
93
|
- spec/support/decorated_state/shared_state_stub.rb
|
90
94
|
- spec/support/decorations/decorated_object_stub.rb
|
@@ -100,7 +104,7 @@ files:
|
|
100
104
|
- spec/unit/decorated_state_spec.rb
|
101
105
|
- spec/unit/decorations_spec.rb
|
102
106
|
- spec/unit/decorator_spec.rb
|
103
|
-
homepage:
|
107
|
+
homepage: http://erikcameron.github.io/
|
104
108
|
licenses:
|
105
109
|
- MIT
|
106
110
|
post_install_message:
|
@@ -129,6 +133,7 @@ summary: Decorum implements the Decorator pattern (more or less) in a fairly uno
|
|
129
133
|
test_files:
|
130
134
|
- spec/integration/coffee_spec.rb
|
131
135
|
- spec/integration/fibonacci_spec.rb
|
136
|
+
- spec/integration/immediate_methods_spec.rb
|
132
137
|
- spec/spec_helper.rb
|
133
138
|
- spec/support/decorated_state/shared_state_stub.rb
|
134
139
|
- spec/support/decorations/decorated_object_stub.rb
|