Skip to content

erikcameron/decorum

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Decorum

Gem Version Build Status

Decorum implements lightweight decorators for Ruby, which we're calling "tasteful decorators." (See below.) It is very small, and has no requirements outside of the standard library. Use it wherever.

Quick Start

gem install decorum

class BirthdayParty
  include Decorum::Decorations
end

class Confetti < Decorum::Decorator
  def shoot_confetti
    "boom, yay"
  end
end

bp = BirthdayParty.new
bp.respond_to?(:shoot_confetti) # ==> false
bp.is_decorated? # ==> false
# but wait!
bp.decorate(Confetti)
# and now...
bp.respond_to?(:shoot_confetti) # ==> true
bp.is_decorated? # ==> true
bp.shoot_confetti # ==> "boom, yay"

0.5.1

  • Test suite maintenance, all is well.

New in 0.5.0

  • The (public) methods in Decorum::Decorations (decorate, undecorate, etc.) may now be aliased. If you were having weird bugs beacuse Decorum was clobbering some existing method named #decorate, this is the version for you. More below.

New in 0.4.0

  • Decorators and their attributes may now be specified in class definitions, and loaded with #load_decorators_from_class
  • #is_decorated?

New in 0.3.0

  • Methods may be called directly on decorators
  • Decorator namespaces
  • Set superhash and shared state classes via environment
  • Entire decorator interfaces may be declared immediate (see below)
  • #post_decorate callback

About

Skip to the action

Decorum expands on the traditional Decorator concept by satisfying a few additional contraints. The constraints are designed to make decorators' role in your overall object structure both clear and safe. More on these points below.

  • Object Identity: After you decorate an object, you're still dealing with that same object.
  • Defers to the original interface: by default, Decorum decorators will not override the decorated object's public methods. (Though you can instruct it to.) This is intentional.
  • Respects existing overrides of #method_missing
  • Decorators are unloadable

By adhering to these constraints, decorators tend to do the Right Thing, i.e, integrate into existing applications easily, and stay out of the way when they aren't doing your bidding. Hence "tasteful decorators." (Not meant to imply others are tacky. The name just stuck.)

In addition, Decorum provides a few helpful features:

  • Stackable decorators, with shared state
  • Recursion, via #decorated_tail
  • Intercept/change/reroute messages, a la Chain of Reponsibility
  • Build stuff entirely out of decorators

As an example of how this is in use right now, suppose you're interfacing a content management system with an existing data application. You want to build a sidebar of image links. The images are in the CMS, but their metadata are stored in the application. You want those systems to stay uncoupled. You can use a decorator to slap the metadata on the image at runtime, e.g.,:

image_collection = Application::ImageData.sidebar_images
images = Cms::Images.where(identifier: image_collection.keys)
# ==> { blah: { url: 'http://blah.foo/', alt: 'The Blah Conglomerate' ... }}

images.each do |img|
  img.decorate(ImageMetaDecorator, image_collection[img.identifier])
end

# defined in ImageMetaDecorator:
images[0].url # ==> 'http://blah.foo'
images[0].alt # ==> 'The Blah Conglomerate'
images[0].fetch_thumbnail_or_queue_to_create

Isn't a Decorator like a Presenter which is like an HTML macro?

In Blogylvania there is some disagreement about what these terms entail. For example, in RefineryCMS, "decorating" a class means opening it up with a class_eval. (In this conception, the decorator isn't even an object, which is astonishing in Ruby.) I use the terms as follows: a "presenter" is an object which mediates between a model, controller, etc. and a view. A "decorator" is an object which answers messages ostensibly bound for another object, and either responds on its behalf or lets it do whatever it was going to in the first place. Presenters may or may not be implemented as Decorators; Decorators may or may not present.

Like "traditional" (i.e., Gang of Four-style) decorator patterns, Decorum is a general purpose, object-oriented tool. Use it wherever.

Tasteful Decorators

Object Identity

Decorators, as conceived of by GoF, Python, etc., masquerade as the objects they decorate by responding to their interface. They are not the original objects themselves. This may or may not be a problem, depending on how your code is structured. In general though, (I doubt this is news) it risks breaking encapsulation: Any code which stores direct references to the original object will have to update them to get the decorated behavior. For example, in a common Rails idiom, in order to do this:

render @user

...having already @user = User.find(params[:id]), you have to do this:

if latest_winners.include(@user.id)
  @user = FreeVacationCruiseDecorator.new(@user)
end

@user is an instance variable of the controller, but it has to be updated in order for the model to be decorated. In practical terms, if you store multiple references to the same object, (say the original object is in an array somewhere, in addition to @user) you have to update both references to get consistent behavior. The model's decoration status has essentially become part of the controller's state.

users.include?(@user) # ==> true
winning_users = users.map { |u| FreeVacationCruiseDecorator.new(u) }
decorated = winning_users.detect { |u| u.id == @user.id } # the decorated object---should be the "same" thing
decorated.destination # ==> "tahiti"
@user.destination # ==> NoMethodError

In Decorum, objects use decorator classes (descendents of Decorum::Decorator) to decorate themselves:

users.each do |user|
  user.decorate(FreeVacationCruiseDecorator, because: "You are teh awesome!")
end

@user.assault_with_flashing_gifs! # ==> that method wasn't there before!

The "decorated object" is the same old object, because it manages all of its state, including its decorations. References don't need to change, and state stays where it should.

Defers to the original interface

Unless instructed otherwise, Decorum will not override existing methods on the decorated object. (See below for how to instruct it otherwise.) There are certainly cases where this is useful, and having come across one myself since the 0.2.0 release, I've amended my position slightly. But not much.

I'm suspicious of this practice, as it blurs the line between "decorating" an object and straight-up monkey patching it. The fact that the method needs overriding implies the original object doesn't have the relevant state to fulfill it. The method is now spread out over two classes, that of the original object and that of the decorator. This may be unavoidable, or monkey patching may be your intention---in that case, by all means. But I don't think it's a pattern to be designed to.

The paradigm cases of decorators don't generally address overriding either. Consider three common examples:

  • Adding a scrollbar to a window
  • Adding milk to a cup of coffee
  • Providing #full_name to an object that supplies #first_name and #last_name

In all of these cases, the decorators provide some new functionality; they don't change the object's original implementation. Obviously, you shouldn't rule it out just because the common examples don't have it, but it's by no means an essential use of the pattern. And from a design perspective, it's a red flag that concerns are becoming... unseparated. (It also risks weird bugs by breaking transparency, i.e., you can have cases where a and b are literally identical, but have different attributes.) With Decorum, you can implement that entire domain of behavior, possibly using a namespace.

When an object is decorated, Decorum inserts its own #method_missing and #respond_to_missing? into the object's eigenclass. Decorum's #method_missing is only consulted after the original object has abandoned the message. (When overriding original methods with immediate, each method gets its own redirect in the eigenclass as well, intercepting the message before the original definition is found.)

Respects existing overrides of #method_missing

A sizeable amount of the world's total Ruby functionality is implemented by overriding #method_missing, so decorators shouldn't get in the way. Because Decorum intercepts messages in the objects eigenclass, it also respects existing class-level overrides. If the decorator chain doesn't claim the message, super is called and lookup proceeds normally.

Unloadable decorators

Decorators can be unloaded, if necessary:

@bob.decorate(IsTheMole)

# meanwhile:
class IsTheMole < Decorum::Decorator
  # hook method
  def post_decorate
    object.revoke_security_clearances
  end

  def revoke_security_clearances
    clearances = object.decorators.select { |d| d.is_a?(SecurityClearance) }
    clearances.each { |clearance| object.undecorate(clearance) }
  end
end

This example also shows the use of the #post_decorate hook.

Implementation

As in other implementations, Decorum decorators wrap another object to which they forward unknown messages. In both cases, all decorators other than the first wrap another decorator. Instead of wrapping the original object however, the first decorator in Decorum wraps an instance of Decorum::ChainStop. If a method reaches the bottom of the chain, this object throws a signal back up the stack to Decorum's #method_missing, which then calls super. (This is a throw/catch, not an exception, which would be significantly slower.)

See the source for more details.

Usage

First, objects need to be decoratable. You can do this for a whole class, by including Decorum::Decorations, or for a single object, by extending it. Note: this alone doesn't change the object's method lookup. It just makes #decorate and friends available on the object. (Note: As of 0.5.0 this isn't strictly true. See Aliasing Decorum Methods below.) The behavior is only changed when #decorate is called. The easiest way is probably including Decorum::Decorations at whatever point(s) of the class hierarchy you feel appropriate.

Using Decorators

The decorated object is accessible as either #root or #object. A helper method:

class Royalty < Human
  # makes our model decoratable
  include Decorum::Decorations
  attr_accessor :fname, :lname, :array_of_middle_names, :array_of_styles
end

class StyledNameDecorator < Decorum::Decorator
  def styled_name
    ProperOrderOfStyles.sort_and_join_this_madness(object)
  end
end

r = Royalty.find_by_palace_name(:bob)
r.respond_to? :styled_name # ==> false
r.decorate StyledNameDecorator
r.respond_to? :styled_name # ==> true
r.styled_name # ==> "His Grace Most Potent Baron Sir Percy Arnold Robert \"Bob\" Gorpthwaite, Esq."

A decorator that keeps state: (code for these is in Examples)

c = Coffee.new
# two milks
c.decorate(MilkDecorator, animal: "cow")
c.decorate(MilkDecorator, animal: "soycow")
# one sugar
c.decorate(SugarDecorator)
c.add_milk
c.add_sugar
c.milk_level  # # ==> 2
c.sugar_level # # ==> 1

Decorators are stackable, and can take an options hash. You can declare decorator attributes in a few ways:

class MilkDecorator < Decorum::Decorator
  attr_accessor :milk_type    
  share :milk_level
  default_attributes animal: "cow", milk_type: "two percent"
  ...

attr_accessor works like normal, in that it gets/sets state on the decorator; when they are called on the decorated object, the most recent decorator that implements the method will answer it. share declares an attribute that is shared across all decorators of the same class on that object; this shared state can be used for a number of purposes. Finally, default_attributes lets you set class-level defaults; these will be preempted by options passed to the constructor.

Specifying decorators in classes

Decoration may be performed by the instance method #decorate above, or you can include default decorators and arguments in your class definition:

class Coffee
  include Decorum::Decorations
  # two bovine dairy no sugar by default, please:
  decorum Milk, { animal: "cow" }, Milk, { animal: "cow" }
  ...
end

c = Coffee.new
c.load_decorators_from_class
c.add_milk
c.milk_level # ==> 2

(This class method was called decorators in versions prior to 0.5.0, but in the interest of avoiding naming conflicts, it's been changed to decorum. If however you're using the standard method aliases, it will install an alias for decorators, allowing your existing stuff to keep working without changes.)

Note that this usage does not automatically include decorators on new objects. That would require invasive procedures on your object initialization. Instead, Decorum provides #load_decorators_from_class, which you can call in your initializations, or later.

As a side note, you can disable another decorators methods thus:

class MethodDisabler < Decorum::Decorator
  def method_to_be_disabled(*args)
    throw :chain_stop, Decorum::ChainStop.new
  end
end

Shared State

When attributes are declared with share (or accumulator), they are shared among all decorators of that class on a given object: if an object has three MilkDecorators, the #milk_level/#milk_level= methods literally access the same state on all three. In addition, you get #milk_level? and #reset_milk_level to perform self-evident functions.

Access to the shared state is proxied first through the root object, and then through an instance of Decorum::DecoratedState, before ultimately pointing to an instance of Decorum::SharedState. This class can be set via the environment (see lib/decorum.rb).

In the examples above and below, shared state is mainly used to accumulate results, like in #milk_level. It can also be used for other things:

  • Serialize it, stick it in an HTML data attribute, and use it to initailize Javascript applications
  • Store a Rails view context for rendering
  • Provide context-specific response selections for decorators, e.g., return current_shared_responder.message(my_condition)

And so on.

#decorated_tail

How exactly did the first MilkDecorator pass #add_milk down the chain instead of returning? In general, the decision whether to return directly or to pass the request down the chain for further input rests with the decorator itself. Cumulative decorators, like the milk example, can be implemented in Decorum with a form of tail recursion:

class MilkDecorator < Decorum::Decorator
  share :milk_level
  ...
  def add_milk
    self.milk_level = milk_level.to_i + 1
    decorated_tail(milk_level) { next_link.add_milk }
  end
end

Because milk_level is shared across all of the instances of MilkDecorator attached to the current cup of coffee, each decorator can update it individually. The "tail call" actually goes down the decorator chain, and is picked up by the next decorator that implements the method. The call is wrapped in #decorated_tail, which will catch the end of the decorator chain, and return its argument; in this case, #milk_level called on the final instance, so, the total amount of milk in the coffee. The state is saved, and because it's shared among all the MilkDecorators, the most recent one on the chain can service the getter method like a normal decorated attribute.

For a demonstration of tail recursion in Decorum, see Decorum::Examples::FibonacciDecorator:

fibber = SomeDecoratableClass.new
# generate the first 100 terms of the Fibonacci sequence
100.times do
  fibber.decorate(FibonacciDecorator)
end
# call it
fibber.fib  # ==> 927372692193078999176
# it stores both the return and the sequence in shared state:
fibber.sequence.length == 100  # ==> true
fibber.current  # ==> 927372692193078999176

#decorated_tail can be used to produce other results. Normally, methods are handled by the most recent decorator in the chain to implement it. To give the method to the oldest decorator to implement it, call #decorated_tail with a non-shared attribute/method:

class MilkDecorator 
  attr_accessor :animal
  ...
  def first_animal
    decorated_tail(animal) { next_link.first_animal }
  end
end

This returns the animal responsible for the first MilkDecorator Bob took. Or call it with some other object:

  def all_animals(animals=[])
    animals << animal
    decorated_tail(animals) { next_link.all_animals(animals) }
  end

This will return a list of all of the animals who have contributed milk to Bob's coffee.

If such a method returns normally, #decorated_tail will return that value instead, enabling Chain of Responsibility-looking things like this: (sorry, no code in the examples for this one)

  handlers = condition ? [ErrorA, SuccessA] : [ErrorB, SuccessB]
  handlers.each do |handler|
    @agent.decorate(handler)
  end
  this_service = determine_service_decorator(params) # # ==> SomeServiceHandler
  @agent.decorate(this_service)
  @agent.service_request(params)

  # meanwhile:
  class SomeServiceHandler < Decorum::Decorators
    def service_request
      status = perform_request_on(object)
      if status
        decorated_tail(DefaultSuccess.new) { next_link.special_success_method("Outstanding!") }
      else
        decorated_tail(DefaultFailure.new) { next_link.special_failure_method("uh-oh") }
      end
    end
  end  

You can now parameterize your responses based on whatever conditions you like, by loading different decorators before the request is serviced. If nobody claims the specialized method, your default will be returned instead.

Calling decorators directly

An object's decorators are available via #decorators, or by passing a block to #decorate:

@object.decorate(SomeDecorator) { |dec| @this_decorator = dec }
@object.decorators # <== array of decorators

# start a request in the middle of the decorator chain:
@the_decorator_im_looking_for = @object.decorators.detect { |d| d.name == "cool decorator, bro" }
@the_decorator_im_looking_for.some_method

(Note that these methods pass the decorator(s) back wrapped in Decorum::CallableDecorator, which is necessary to call methods on them directly.)

Namespaced Decorators

As noted above, overriding an objects public methods breaks behavior over two different classes. To package a domain of behavior in a namespace using Decorum:

@request.decorate(MyRouter, namespace: "routes")
@request.routes.my_route(path: "foo/bar")
# namespaces pass anything they can't do back to the root
# object, so they effectively override it:
@request.routes.defined_on_request_object? # <== true

This way, you don't have to define a #my_route stub on the original class for undecorated instances, and the public method of the original object is available on (and in) the namespace.

Overriding existing methods

To give decorator methods preference over an objects existing methods (if you must) declare the method immediate:

class StrongWilledDecorator < Decorum::Decorator
  immediate :method_in_question

  def method_in_question
    "overridden"
  end
end

x = WeakWilledClass.new
x.method_in_question # <== "original"
x.decorate(StrongWilledDecorator)
x.method_in_question # <== "overridden"

If you declare immediate with no arguments, the decorators entire public interface is used.

Aliasing Decorum methods

Including/extending Decorum::Decorations makes a number of public methods (e.g., #decorate) available on an object, which can be a problem if methods under those names already exist. As of 0.5.0, these methods are actually implemented with "internalized" names, (e.g., #_decorum_decorate) and aliased after the fact. Decorum is loaded by requiring two files:

# lib/decorum.rb
require_relative 'decorum/noaliases'
require_relative 'decorum/aliases'

The noaliases file loads up everything except aliasing; after requiring this file, public methods in Decorum::Decorations are available by their internal names. Aliases are then loaded for each of the public methods, either as specified in upcased environment variables (e.g., _DECORUM_DECORATE="my_decorate_alias") or failing that, the default values in aliases. Here are the public methods and their defaults:

# from lib/decorum/aliases.rb
    DEFAULT_ALIASES = { _decorum_decorate: "decorate",
      _decorum_undecorate: "undecorate",
      _decorum_is_decorated?: "is_decorated?",
      _decorum_decorators: "decorators",
      _decorum_decorated_state: "decorated_state",
      _decorum_load_decorators_from_class: "load_decorators_from_class",
      _decorum_raw_decorators: nil,
      _decorum_raw_decorators!: nil,
      _decorum_namespaces: nil }

The .decorum method made available to modules when they include Decorum::Decorations is also aliased here as either the value of _DECORUM_CLASS_DECORATORS or just .decorators.

If you need something else, (e.g., more finely grained aliases) you can just require decorum/noaliases, and all of Decorum's aliasing will be skipped. You can then implement your own scheme however you like.

Decorators All the Way Down

Decorum includes a class called Decorum::BareParticular, which descends from SuperHash. You can initialize any values you like on it, call them as methods, and any method it doesn't understand will return nil. The only other distinguishing feature of this class is that it can be decorated, so you can create objects whose interfaces are defined entirely by their decorators, and which will return nil by default.

To-do

A few things I can imagine showing up soon:

  • See open issues
  • Thread safety: probably not an issue if you're retooling your Rails helpers, but consider a case like this:
  10.times do |i|
    port = port_base + i  # 3001, 3002, 3003...
    @server.decorate(RequestHandler, port: port ) 
    Thread.new do
      @server.listen_for_changes_to_shared_state(port: port)
    end
  end

&c. I'm open to suggestion.

Contributing

I wrote most of this super late at night, (don't worry, the tests pass!) so that would be awesome:

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

About

Tasteful decorators for Ruby.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages