I think by now we all know to prefer composition over inheritance. But in a language with a lot of options, what’s the best kind of composition to use?
Composing an adventure
class Character # ... end
A Character
can be described:
class Character # ... def describe puts "You are a dashing, rugged adventurer." end # ... end
A Character
can look, listen, and smell his environment:
class Character # ... def look list("You can see", ["a lightning bug", "a guttering candle"]) end def listen list("You hear", ["a distant waterfall"]) end def smell list("You smell", ["egg salad"]) end def list(prefix, objects) objects.each do |o| puts "#{prefix} #{o}." end end # ... end
require './decoration-vs-extension' cohen = Character.new cohen.describe cohen.look cohen.listen
You are a dashing, rugged adventurer. You can see a lightning bug. You can see a guttering candle. You hear a distant waterfall.
The character can also consult all of his senses at once:
class Character # ... def observe look listen smell end # ... end
require './decoration-vs-extension' cohen = Character.new cohen.observe
You can see a lightning bug. You can see a guttering candle. You hear a distant waterfall. You smell egg salad.
Characters can have various effects conferred upon them by items, potions, etc. A simple example is a hat:
require 'delegate' class BowlerHatDecorator < SimpleDelegator def describe super puts "A jaunty bowler cap sits atop your head." end end
At each turn of the game, the Character
object will be decorated with whatever effects are currently active, and then a user command will be performed:
require './decoration-vs-extension' cohen = BowlerHatDecorator.new(Character.new) cohen.describe
You are a dashing, rugged adventurer. A jaunty bowler cap sits atop your head.
Seeing in the dark
class InfravisionPotionDecorator < SimpleDelegator def describe super puts "Your eyes glow dull red." end def look super look_infrared end def look_infrared list("You can see", ["the ravenous bugblatter beast of traal"]) end end
While the character is experiencing the effects of an infravision potion, his powers of observation increase:
require './decoration-vs-extension' cohen = InfravisionPotionDecorator.new(Character.new) cohen.describe cohen.look
You are a dashing, rugged adventurer. Your eyes glow dull red. You can see a lightning bug. You can see a guttering candle. You can see the ravenous bugblatter beast of traal.
There’s just one little problem that crops up when the #observe
method is called.
require './decoration-vs-extension' cohen = InfravisionPotionDecorator.new(Character.new) cohen.observe
You can see a lightning bug. You can see a guttering candle. You hear a distant waterfall. You smell egg salad.
Hey, where’d that bugblatter beast go?
The Character#observe
method calls #look
—but since the wrapped object has no knowledge whatsoever of the InfravisionPotionDecorator
, it calls the original definition of #look
, not the one which also calls #look_infrared
.
Now, granted, this flaw actually works out in our intrepid adventurer’s favor, since the ravenous bugblatter beast of Traal is so stupid it thinks that if you can’t see it, it can’t see you. But never mind that: it’s still a bug, and bugs must be blattered.
A solution that’s all wet
#observe
as well in the decorator:
class InfravisionPotionDecorator < SimpleDelegator def observe look listen smell end end
Yuck! This is the exact same implementation as in Character
, just copied and pasted so that the correct implementaiton of #look
will be called. Clearly this is non-DRY. But even worse, we’ve introduced a nasty variety of connascence. Every time we introduces a new Character
method which calls #look
, we’ll have to cull through every single effect decorator which overrides #look
, adding copy-and-pasted versions of the new method so that it doesn’t accidentally ignore the effect-wrapped version. Double yuck!
Modules to the rescue
module InfravisionPotionModule def describe super puts "Your eyes glow dull red." end def look super look_infrared end def look_infrared list("You can see", ["the ravenous bugblatter beast of traal"]) end end
require './decoration-vs-extension' cohen = Character.new.extend(InfravisionPotionModule) cohen.observe
You can see a lightning bug. You can see a guttering candle. You can see the ravenous bugblatter beast of traal. You hear a distant waterfall. You smell egg salad.
This time the overridden method is added directly to the object via its singleton class. So even the object’s own unmodified methods get the new infravision version of #look
.
Sadly, by enabling him to see the monster we have sealed our protagonists’s fate. But at least we fixed the bug!
Other solutions
Character
into individual body parts, with separate attributes for eyes
, nose
, and ears
. The Character
could then delegate the individual senses to their respective organs:
require 'forwardable' class Character extend Forwardable attr_accessor :eyes attr_accessor :ears attr_accessor :nose def_delegator :eyes, :look def_delegator :ears, :lisen def_delegator :nose, :smell end
A potion of infravision might then replace the character’s eyes with infrared-enhanced ones:
class InfravisionPotionDecorator < SimpleDelegator class EyesDecorator < SimpleDelegator # ... end def initialize(character) super(character) character.eyes = EyesDecorator.new(character.eyes) end end
…but this is an awful lot of code and ceremony. It might make sense someday, but right now it feels like massive overkill. The module extension approach, by contrast, is only a small change from our original version.
Are decorators overrated?
In a word, no. For one thing, decoration is a simpler structure to understand. Given object A
wrapped in object B
wrapped in object C
, it’s easy to reason about how method calls will be handled. They’ll always go one-way: a method in object A
will never reference a method in B
or C
.By contrast, method calls in a module-extended object can bounce around the inheritance heirarchy in unexpected ways.
A second consideration is that once you’ve extended an object with a module, its behavior is changed for all clients, including itself. You can’t interact with the “unadorned” object anymore. You might extend an object for your own purposes, then pass it to a third-party method which doesn’t understand the modified behavior of the object and barfs as a result.
Finally, there’s a performance penalty. While it varies from implementation to implementation, dynamically extending objects can slow down your code as a result of the method cache beign invalidated. Of course, as with all performance-related guidelines, be sure to profile before making any code changes based on this point.
Conclusion
For applications where you want to adorn an object with some extra functionality, or modify how it presents itself, a decorator is probably the best bet. Decorators are great for creating Presenters, where we just want to change an object’s “face” in a specific context.
On the other hand, when building up a composite object at runtime object out of individual “aspects” or “facets”, module extension may make more sense. Judicious use of module extension can lead to a kind of “emergent behavior” which is hard to replicate with decoration or delegation.
At least, this has been my experience. Got some experiences or opinions on decoration vs. module extension? Feel free to leave a note in the comments!
Good rundown of the trade-offs. I find that I jump to your higher-ceremony strategy/decorator pattern sooner with less reluctance than most ruby devs (although perhaps slower than Java devs?). I’m more likely to jump straight from the (simple) delegator all the way to the more complicated strategy pattern.
The critical problem with the low ceremony module mixin is that it can’t be un-mixed. Most potions’ effects are temporary, and they leave the bloodstream eventually. You could have the potion module keep track of this state, but I’d much rather remove a stateless potion from the (stateful) character than leave an inert potion mixed in. Another mechanism would be to use stateless characters, destroying and remaking them whenever they change, but down that road lies madness (or perhaps clojure); and anyway that’s just pushing the state pattern up to a higher level in the architecture.
This, of course, is perfectly suited to the GoF’s Strategy and State paterns, but (as you say), they often feel like more high ceremony than is worth it, even if they map better to the domain’s structure/semantics. I seem to recall that someone had a library that simulated mixin and mixout of modules. If so, then that would be a (relatively) low-ceremony way to get the biggest benefits of the Strategy and State patterns. What do you think?
There are indeed some mix/unmix tools, but I’m a trifle dubious of them. As the article says, in this notional game the player object is built up once for every turn, so long-lived objects are not a problem. For long-lived objects we definitely need a different design.
Ah, I missed that: “At each turn of the game, the Character object will be decorated with whatever effects”; the clojure route. 🙂 It’s not really that mad, the more I think on it.
Why are you dubious about mix/unmix? It seems to me that a long-lived object should be able to add and subtract modules at will (for example, I like the idea of subtracting an ORM module from a model object when it gets passed from controller to view in Rails).
I think you’re thinking of Mixology … which looks like it hasn’t been touched since mid 2009. That might be a sign that it “works”, but probably not with ruby 1.9. Mixing out is unfortunately very likely to be highly implementation (of ruby) dependant…
And due to the mixin callbacks (extended and included) I could easily conceive of a situation where mixout ends badly. (module B conditionally defines methods from module A, which is then mixed-out. Hijinks ensue).
I’d love it if it would actually work, but I think mixout is one of those things that has to be dealt with at the language spec level ….
Once again, thank you for putting together this kind article – very helpful.
Glad you enjoyed it!
Just as you couldn’t help writing this article, I couldn’t help trying to implement it. I noticed you left the tests out of this piece. That’s one of the reasons I shy away from module extension (which, by the way is a kind of inheritance 🙂 ). When the methods are on the same object, testing the interaction between the pieces of logic can get hairy. At least that’s my experience, so I wanted to put it to the test.
https://github.com/Peeja/potion_of_infravision
Nothing revelatory, but it made for a good exercise. It was a nice solo CodeRetreat. I encourage others to try the same in other styles.
Ah, good ol’ LPC days. Thanks for the reminder: we implemented extra features of items with modules (we hadn’t call it mixins, the term was not yet coined). Like if you implemented a torch, you could extend Item class with M_Igniteable. It implemented ignite and extinguish command callbacks, and added a hook on item description (in LPC you don’t have super) among other things.
At the time you could learn a ton by just reading mud standard libraries 🙂
Dave Fasthand @ After the Plague mud, retired head admin
Just as I was reading up on self/object schizophrenia and object teams, you post this. Spooky! paranoid
My first impression is the variability addressed by the decoration/extension in this example would be better served by some ‘business logic/rules’ inside or outside of the class. I see the delegation (and likely the extension) as necessary to compensate for display rules that are implicit rather than explicit. Personally I would probably define these rules within a separate Environment class which would be passed as a param to each of the defined character methods (and probably would involve a double dispatch call to the Environment with the Character as a param). The Environment would contain the rules about how characters appeared based on their attributes, and the Character would interact with the Environment to determine what it could see. These rules could of course also be done all within the Character class also. I realize that I’m probably off in a small, small minority on this one…
Holy crap, SimpleDelegator takes 2 seconds to decorate 200 objects!!
Nope.
require "delegate" require "benchmark" RUBY_VERSION # => "2.0.0" objs = 200.times.map { Object.new } puts Benchmark.measure { objs.map {|o| SimpleDelegator.new(o) } } <h1>>> 0.000000 0.000000 0.000000 ( 0.000078)</h1>
Try it with ruby 1.8.7, it’s a LOT slower with that version for some strange reason. Someone else had the issue too : https://gist.github.com/mlomnicki/3905059
Just FYI, his part confused me for awhile, “The Character#observe method calls #look—but since the wrapped object has no knowledge whatsoever of the InfravisionPotionDecorator…”
It’s much clearer to me if the “wrapped object has no knowledge whatsoever” part is removed. Something like this: “The Character#observe method calls #look but it calls the original definition of #look, not the one which also calls #look_infrared.”