Aggregation helps to make your models richer: for example if you’re dealing with money why deal with integer and decimal attributes when you can use something like the Money class? Thanks to various patches the shortcomings of the original composed_of implementation have been addressed, but there is still one that for some reason remains to be fixed: aggregate class constructors.
The problem is simple: composed_of assumes that the aggregate class can be instantiated using a simple new constructor that accepts arguments in the order defined by the :mapping option. In many cases this works, but if your aggregate class doesn’t conform then you’re left to manually initialise your aggregations (in an after_initialize callback for example). Not very elegant.
Here is an example from a recent project I’ve been working on:
class Visitor < ActiveRecord::Base
composed_of :ip_addr,
:class_name => 'IPAddr',
:mapping => %w(ip to_i)
end
Here the composed_of aggregation maps the ip attribute (an integer) to the IPAddr class from the Ruby standard library. Much of the code that uses this attribute wants to treat it as an IP address rather than a raw integer so it makes sense to represent it as one in the model. Unfortunately this won’t work properly as the IPAddr constructor requires a second parameter to be passed to the constructor as shown below:
IPAddr.new(1024768100)
=> ArgumentError: unsupported address family
IPAddr.new(1024768100, Socket::AF_INET)
=> #<IPAddr: IPv4:61.20.184.100/255.255.255.255>
Sadly previous attempts to allow composed_of to be more flexible with constructors have failed to make it into core. Personally I don’t think that too few people requesting an enhancement is a good enough reason not to apply it anyway. Perhaps if composed_of was more flexible in the first place more people would use it!
Hopefully though it’ll be third time lucky as I’ve just submitted my own patch to improve composed_of. If the core team still don’t feel motivated to apply it then I guess I’ll just release it as a plugin instead. It takes a slightly different approach then the previous patches:
It adds a new
:constructoroption that takes a symbol or a proc that will be used to instantiate the aggregate object. It is optional and if not used then the existing behaviour of callingnewwith the mapped attributes will be used.It deprecates the use of a block to provide a method of converting values assigned to the aggregate attribute. The use of a block didn’t feel particularly consistent with the rest of Rails where typically symbols or procs are passed as options (a good example being the
:ifand:unlessoptions for validations). Of course passing a block will still function, so existing code won’t break, but it will raise a deprecation warning. This change leads on to…It adds a new
:converteroption that also takes a symbol or proc that will be used when the aggregate attribute is assigned a value that is not an instance of the aggregate class. This replaces the old block syntax and makes it easier to do things like callingcomposed_offrom within awith_optionsblock. If both the:converteroption and a block are passed tocomposed_ofthen the:converteroption will take precedence.
Here’s the IP address example from earlier updated to take advantage of the new options:
class Visitor < ActiveRecord::Base
composed_of :ip_addr,
:class_name => 'IPAddr',
:mapping => %w(ip to_i),
:constructor => Proc.new { |value| IPAddr.new(value, Socket::AF_INET) },
:converter => Proc.new { |value| value.is_a?(Integer) ? IPAddr.new(value, Socket::AF_INET) : IPAddr.new(value.to_s) }
end
Passing a symbol to either the :constructor or :converter options provides an easy way to use a class where the constructor is not named new but still expects arguments in the order defined by the :mapping option. For example if your aggregate class uses a parse method for instantiation you could do this:
class MyClass < ActiveRecord::Base
composed_of :aggregate_value,
:class_name => 'AggregateClass',
:mapping => %w(value to_s),
:constructor => :parse,
:converter => :parse
end
I think these simple changes make aggregation with composed_of much more powerful without breaking existing code. If you’d like to see composed_of get the love it deserves, take a look at my patch and give it your support.


4 comments
Comment on Giving composed_of some much needed lovin’ by Rob Anderton
August 27th, 2008 @ 10:51 – permalink
Comment on Giving composed_of some much needed lovin’ by Rob Anderton
September 11th, 2008 @ 11:25 – permalink
Comment on Giving composed_of some much needed lovin’ by nachokb
October 22nd, 2008 @ 19:19 – permalink
Comment on Giving composed_of some much needed lovin’ by Rob Anderton
October 23rd, 2008 @ 22:58 – permalink
Leave a reply
You can use Markdown in your comment as well as plain HTML. You can use
<filter:jscode lang="ruby">and</filter:jscode>tags to surround code blocks (supported languages are css, html, javascript and ruby). Your email address will not be published.If your comment doesn’t appear immediately after posting it could have been marked as spam. Don’t worry: we regularly check for and approve incorrectly filtered comments so you shouldn’t have to wait too long for it to be shown.