Ruby On REST 2: Representers and the DCI Pattern
Friday, December 16th, 2011We talked about Representers earlier last week. Today, I wanna describe DCI and how it fits perfectly together with representers in Roar.
What is DCI?
For years we’ve been taught that “Fat Models, Skinny Controllers” is the way to organize code. Push all your domain code into your ActiveRecord classes. Many people, including me, agree that this is bullshit. This leads to bloated classes, usually combined with one or two object instances taking care of an entire workflow (aka request).
DCI is an architectural pattern that makes the model “fat-on-demand”: Data objects are enhanced with roles at runtime, but only when needed.
The DCI pattern consists of three concepts.
- Data objects are supposed to contain application data. Nothing else. This would be your ActiveRecord row instance, but without any predefined behaviour. Just data.
- Context objects define what is done in the current workflow. In Rails, a controller running an action could be a context object.
- Roles are behaviour for data objects in a particular context. In Ruby, a Rails controller could mixin a module (a “role”) into an empty model row and thus let it process something.
Hooray for DCI – But Why?
Now the thing is, I absolutely hate to hype something (except stuff I did, of course). However, DCI makes code better maintainable, easier to follow and way easier to test. Period.
BTW, did you already check out my new Roar gem? It’s hip!
Think of a classical model found in many Rails applications with maybe one hundred methods. Or fifty. Fifty methods to interfere with each other. In a DCI setup, an object has a limited behaviour scope. The limited knowledge makes it easier to debug and predictable.
One Model, Multiple Faces
Since representers are modules they fit perfectly into the DCI approach, making them roles. Let’s consider the fruit representer from last week, again.
require 'roar/representer/json' module FruitRepresenter include Roar::Representer::JSON property :title collection :colors end
You still can include this representer into its “host” class directly.
class Fruit include Roar::Representer include FruitRepresenter end
The problem now is that the Fruit class is limited to one representation, only. Forever and a day you may only render one particular representation. The key about representations, however, is that one model may be represented by multiple, yeah, representations.
Now, back to the start, let’s assume the Fruit class is some empty ActiveRecord class without any prior knowledge of representations at all.
class Fruit < ActiveRecord::Base end lemon = Fruit.from_json("{\"title\":\"Lemon\"}") #=> undefined method `from_json' for Fruit:Class
Yeah, there is no #from_json class method.
Representers On DCI
In order to use the DCI approach, we first need an instance. Here’s how the lemon instance, and only this instance, can be extended with the fruit representer.
lemon = Fruit.new.extend(FruitRepresenter) lemon.from_json("{\"title\":\"Lemon\"}") lemon.title #=> "Lemon"
Awesome! The built-in Ruby #extend method makes the lemon instance aware of its representation and mixes in #from_json and friends.
Naturally, this works in both ways.
orange = Fruit.find_by_title("Orange") orange.extend(FruitRepresenter).to_json #=> {"title":"Apple","colors":["green"]}
The mixed-in representer also lets us render the JSON document.
Gimme Complexity!
On a flat level with single objects, this works out-of-the-box with recent Roar/representable versions. Problems arise when we have nested setups.
module BowlRepresenter include Roar::Representer::JSON include Roar::Representer::Feature::Hypermedia property :location collection :items, :as => Fruit, :extend => FruitRepresenter link :self do "http://bowl/#{location}" end end
Using the :extend option we can give representable a hint about the representer to use when rendering/parsing contained objects.
And, hey, this works.
empty_bowl = Bowl.new empty_bowl.extend(BowlRepresenter) emtpy_bowl.from_json("{\"location\":\"kitchen\", \ \"items\":[{\"title\":\"Lemon\",\"colors\":[]}]}") puts empty_bowl.items.first.title #=> "Lemon"
Discussion Needed!
Having representers on an object level makes the whole thing really clean and versatile. You can test document rendering and parsing without polluting classes. And you can have a FruitRepresenter, a RottenVegetableRepresenter and what else you like for your orange instance. BTW, oranges ain’t no vegetables.
We still discuss about the API and need your opinion. Right now, we got this
collection :items, :as => Fruit, :extend => FruitRepresenter
Here, :as describes the class that encapsulates the contained data. The :extend option helps representable to find the correct module for the nested item. Now, what about this?
collection :items, :class => Fruit, :module => FruitRepresenter
Does that make sense? These options really just describe what’s on the right side of the hash rocket. Thanks to Scott for discussion here. Now, tell us your story!
