decorum 0.0.1 → 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/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
|