{{{
Until today, @extend@ was the only way to inject a representer into a model to render or consume representations. Many people criticised this approach as it adds methods to an object which should rather be treated immutuable. Besides that, using @extend@ seems to affect the performance due to caching invalidation.
song = Song.find(1) song.extend(SongRepresenter).to_json
As a result, *people who actually liked the representer idea declined using it* because they were worried about @extend@. No longer should this keep you from using “roar”:https://github.com/apotonick/roar and “representable”:https://github.com/apotonick/”roar”:https://github.com/apotonick/roar!
h3. Use Decorator Over Extend.
A minor refactoring brought us @Representable::Decorator@.
class SongRepresenter < Representable::Decorator include Representable::JSON property :title property :track end
Instead of a module, derive a class that exposes the very same DSL that you know from the “old” module representers. BTW, you can also include representer modules into @Decorator@ classes.
SongRepresenter.new(song).to_json
Just pass the represented object into the constructor and let representable do the work. Your models won’t get hurt anymore – promised!
Use the @:decorator@ option where you used @:extend@ in a nested setup.
class AlbumRepresenter < Representable::Decorator include Representable::JSON collection :songs, decorator: SongRepresenter end
In roar, you might use the @Roar::Decorator@ base class. Pretty cool: roar-rails “already supports decorators”:https://github.com/apotonick/roar-rails/tree/e49e965badb03fe255d594a91b4ddcd937ebf4b9#using-decorators.
h3. Speed Is Relative.
Several “on-going discussions”:http://charlie.bz/blog/things-that-clear-rubys-method-cache debate whether heavy usage of @extend@ slows down your app. I did a “quick benchmark”:https://gist.github.com/apotonick/5351023 myself where I render an @Album@ instance containing 10,000 @Song@ instances which results in 10,000 calls to @extend@ when rendering the album.
Album.new(songs: 10000.times.collect { Song.new }). extend(AlbumRepresenter). to_json
I did the same with a @Decorator@ class.
alb = Album.new(songs: 10000.times.collect { Song.new }) AlbumDecorator.new(alb).to_json
Here are the results. I leave it up to you to judge these metrics.
# time with extend : 0.780000 # time with decorate : 0.600000
It looks as if @Decorator@ is faster – go figure! 😉
h3. Using Decorator With Hypermedia.
There’s a tiny pitfall when using a decorator on a hypermedia-consuming object in roar.
class SongRepresenter < Roar::Decorator include Roar::Representer::JSON::HAL include Roar::Decorator::HypermediaConsumer property :title link(:self) { song_url(self } end
Be sure to include the @Roar::Decorator::HypermediaConsumer@ module which will propagate parsed links to the represented object by calling it’s @link=@ writer. You have to provide that as well.
class Song attr_accessor :links end
Now, consuming an incoming representation will set links on the client object.
SongRepresenter.new(song).from_json("..hypermedia json") song.links[:self] #=> "http:songs/1"
h3. Conclusion
Go and use decorators and tell us how you like it. Never shall anyone anymore complain about dirty extendings!
}}}
Beautiful cuddle
LikeLike
Using it and loving it 🙂
Still would like to see this happen though:
Instead of:
Something like:
The idea being that I’d like to be able to initialize decorated class any way I like without needed accessors/writers on exposed/decorated properties.
Other than that, great work 🙂
Thanks for your time!
LikeLike
This is great, but it seems like respond_with in roar-rails is outputting differently than the decorators own to_json does? Decorator.to_json looks correct, but respond_with is missing things like links.
Also, it seems like defining methods on the representers doesn’t work the way it did with modules, when you want to add a “virtual attribute” via representer, on modules you could call property and then define that property as a method in the module, but the same approach on Decorator’s raises an undefined method error.
LikeLike
Hey Brian, please be sure to updated roar and roar-rails to the latest versions. #respond_with should work, if not, please file a ticket on github.
The virtual attributes in the representer module don’t work anymore since we do not extend the represented object (I just did what you guys were asking for!!!!) and hence can’t add any methods.
What you can do is use a :getter lambda.
LikeLike
Thanks Nick, didn’t see info on getters, makes sense. Will try and confirm/find a repro you can do on respond_with, on latest version, maybe it’s something weird I’m doing. Appreciate all your work, great to have an alternative to extend!
LikeLike
Vanja , do you mean the representing decorator should pass the attributes into the represented object’s constructor rather than using setters (when parsing)? We could introduce a strategy.
How would the rendering work, then? Still by calling readers?
LikeLike