Grab your brushes! Decorators we shall become

The problem with "decorators" in rails

The term decorator seems to have taken a wrong turn at some point on its journey to peoples projects in the rails community, and while this isn't true of all implementations, it certainly is for some of the most popular ones.

When decorators aren't decorating

Some of the most popular "decorator" gems that are often discussed or recommended by people are draper or active_decorator, and while many people may find them useful or helpful for their projects, I am in no way trying to discourage using the tools you like, I do however have a slight issue with this hijacking of the term "decorator" that has occurred. Lets take a look at how these two gems describe themselves:

draper:

Draper: View Models for Rails

active_decorator:

A simple and Rubyish view helper for Rails 3, Rails 4 and Rails 5. Keep your helpers and views Object-Oriented!

Both describe themselves as View Objects / View Models, but name themselves as decorators, which in my own experience from teams I have worked in has caused a lot of misinterpretation and confusion as to what the decorator pattern actually is.

To decorate, or not to decorate, that is the question

The decorator pattern can be used to extend (decorate) the functionality of a certain object statically, or in some cases at run-time, independently of other instances of the same class, provided some groundwork is done at design time. This is achieved by designing a new Decorator class that wraps the original class.....This pattern is designed so that multiple decorators can be stacked on top of each other, each time adding a new functionality to the overridden method(s).

The first point that springs to mind, this design pattern is completely separate from however one chooses to organize their code base, it doesn't care if it's for the view, for a JSON response, to be passed to another model, a service, another decorator, so when draper says to me:

Because decorators are designed to be consumed by the view, you should only be accessing them there.

My spidey-senses start tingling! If we are merely preparing some object for the view are we really decorating? No, we are doing exactly what we said, preparing something to present in the view. If something should only be accessed in the view, is it really a decorator? no, because a decorator is only concerned with wrapping its arms of added functionality around that beautiful class, it doesn't care what bed it does it in. Draper encourages this strong relation with the View (template), creating strong links with helper methods and so on, and discourages the use of "decorators" anywhere else, which is why I will never be turning to draper when I need to decorate.

The second point that draws my attention, is that multiple decorators can be stacked. This instantly throws active_decorator (and any other implementation that includes or extends modules) right out the window as an option for decorators, because while you can override methods, you can not stack them on top of each other, all separate extensions / inclusions will override the original instances / components method, not the next layer down of the stack's method, meaning we can't do things like stack #cost methods with decorators to build up a final price, an extremely convenient functionality to have.

The third point that comes to mind is the focus that decorators somehow need to have a direct relation with Models. This thinking is perhaps the most difficult to understand, as it encourages the very opposite of what we are trying to achieve by using decorators, which is a DRY, reusable code base. One can argue that with mixins (more popularly referred to as concerns in rails) this can be easily achieved, however using mixins together with decorators is much akin to using a hammer to drive in a screw, their core concepts are polar opposites.

To tackle this issue I decided to attempt writing my own gem as to how I want my decorators to work, to be simple, no fancy magic, and achieve the core principles of the decorator pattern. What I ended up making is Bottled Decorators

Giving birth to decorators

Creating new decorators is as easy as pie with the BottledDecorator Generator. just add the decorator name, and if you want the decorator methods, to the generator command:

rails g bottled_decorator LargeFoodItem
rails g bottled_decorator PiratizedUser name greeting

Just like that we have two new decorators ready for use, a currently empty decorator for decorating food as large food items, and a decorator with two methods prepared for us, name and greeting, that decorates users with pirate like behaviour.

Decorator Methods

A possible implementation of the two decorators we generated above could be:

class LargeFoodItem
  include BottledDecorator

  # A large food item has 50 cents added onto its cost
  def cost
    super + 50
  end

end
class PiratizedUser
  include BottledDecorator

  # Pirates all dream of being the captain of their own ship
  def name
    "Cap'n #{super}"
  end

  # And its a well studied fact that Rum is a pirates poison of choice
  def greeting
    "Yo ho ho and a bottle of rum!"
  end

end

A Decorator's first steps

Once generated, the next things to do is decorate loads of stuff!

Unlike some other implementations of decorators (many of which I dare to say are not actually decorators but just View Objects / Helpers that are hijacking the word decorator), BottledDecorators do not use an extension style of decorating. Instead they wrap up our objects in a lovely cosy blanket of decoration:

pirate_user = PiratizedUser.(@user)

# or

@large_burger = LargeFoodItem.(burger)

You can also wrap a collection, to get an Array of decorated objects:

@pirates = PiratizedUser.(@users)
# returns an Array

As can be seen above, this wrapping is done using the decorator class' ::call method, so a simple .() will suffice to get the job done, or any other preferred choice of invoking the call method.

Bottled Decorators in action

Once we have our decorated object, its just a case of invoking your methods:

@user.name
# => "John Hayes-Reed"
@user.age
# => 27

@user = PiratizedUser.(@user)
@user.name
# => "Cap'n John Hayes-Reed"
@user.greeting
# => "Yo ho ho and a bottle of rum!"

# of course we can still access the components original methods as well
@user.age
# => 27

Because BottledDecorators wrap instead of extend, we can also keep wrapping on multiple layers, adding on functionality to overridden methods indefinately, because each layer looks at its own component for its super, and not the original instance:

class DrunkUser
  include BottledDecorator

  def name
    "#{super}, the drunkard!"
  end

  def greeting
    "#{super} (hiccup)"
  end
end
@user.name
# => "John Hayes-Reed"

@user = PiratizedUser.(@user)
@user.name
# => "Cap'n John Hayes-Reed"
@user.greeting
# => "Yo ho ho and a bottle of rum!"

@user = DrunkUser.(@user)
@user.name
# => "Cap'n John Hayes-Reed, the drunkard!"
@user.greeting
# => "Yo ho ho and a bottle of rum! (hiccup)"

Because of this layering ability, we can use a single decorator to represent multiple possible states of objects:

# a regular burger
@burger.cost
# => 100

# a large burger
LargeFoodItem.(@burger).cost
# => 150

# an extra large burger
LargeFoodItem.(LargeFoodItem.(@burger)).cost
# => 200

# SUPERSIIIIIZE
LargeFoodItem.(LargeFoodItem.(LargeFoodItem.(@burger))).cost
# => 250

This can be done with multiple decorators to build up the cost of a whole variety of possibilities:

class WithDrink
  include BottledDecorator

  def cost
    super + 15
  end

end
# a regular burger
@burger.cost
# => 100

# with a side order drink
WithDrink.(@burger).cost
# => 115

# a large burger with a side order drink
LargeFoodItem.(WithDrink.(@burger)).cost
# => 165

Conclusion

The first thing I want conclude, is that if you read all the way down to here, then I thank you for taking some time out to give me platform to join the discussion.

The concepts I have tried to discuss are well known, well discussed, and well opinionated topics. This is a single point of view and opinion in the discussion. But I truly believe that the rails community in particular has drifted a bit in this particular area.
I hope that if you decide to use or take a look at bottled decorators that they can be of some use to you as an alternative to what seems to be some of the mainstream implementations. I hope even more that if you haven't thought of trying to implement something like this yourself, that you do, it is completely do-able, and fun. But my biggest hope, is that if you can contribute to or improve bottled decorators in any way to make them more helpful, and a decorator implementation that works for everyone, that you get stuck in!

While you're at it, take a look at my other gem bottled services too, and service the hell out of your projects!

SNS

Contact Me

  • john.hayes.reed@gmail.com

Recent Activity

Attended Kansai Ruby Kaigi 2017

A Ruby conference in the kansai region of japan, held in Osaka. Listened to a variety of presentations from many people in the Ruby community. This years theme was Community and Business.

Bottled observers gem release

The first production version of bottled_observers has been released (v0.1.0)


New blog post!

A blog about more advanced decorating concepts in ruby.


Bottled decorators gem update

v0.1.5 of bottled decorators has been released.


New blog post!

A blog post about the concept of class-instance variables


Website Design Update

johnhayesreed.com has had a makeover with Bootstrap 4

New blog post!

A new blog about design patterns in rails - Decorators


Bottled decorators gem release

The first production version of bottled_decorators has been released (v0.1.4)


New blog post!

A new blog about desing patterns in rails - Services


Bottled services gem release

The first production version of bottled_services has been released (v0.1.3)


Ruby Rampage 2016

Took part in 2016's Ruby Rampage 48 hour Hackathon.