This project is archived and is in readonly mode.
AttributeDecorator, a new take on aggregation.
Reported by Eloy Duran | September 1st, 2008 @ 12:01 PM | in 3.1
This is our AttributeDecorator plugin (http://github.com/Fingertips/att... turned into a patch, as was discussed on rails-core the last week. See the comments by Manfred and me for more in depth info on the rational: http://groups.google.com/group/r...
It allows you to add reader and writer methods decorating one or more attributes on an ActiveRecord model. It's comparable to composed_of, but has a much more concise implementation and a lot less confusing configuration options.
From the documentation:
class CompositeDate
attr_accessor :day, :month, :year
# Gets the value from Artist#date_of_birth= and will return a CompositeDate instance with the :day, :month and :year attributes set.
def self.parse(value)
day, month, year = value.scan(/(\d+)-(\d+)-(\d{4})/).flatten.map { |x| x.to_i }
new(day, month, year)
end
# Notice that the order of arguments is the same as specified with the :decorates option.
def initialize(day, month, year)
@day, @month, @year = day, month, year
end
# Here we return the parsed values in the same order as specified with the :decorates option.
def to_a
[@day, @month, @year]
end
# Here we return a string representation of the value, this will for instance be used by the form helpers.
def to_s
"#{@day}-#{@month}-#{@year}"
end
# Returns wether or not this CompositeDate instance is valid.
def valid?
@day != 0 && @month != 0 && @year != 0
end
end
class Artist < ActiveRecord::Base
attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
validates_decorator :date_of_birth, :message => 'is not a valid date'
end
# Option examples:
attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
attribute_decorator :gps_location, :class_name => 'GPSCoordinator', :decorates => :location
attribute_decorator :balance, :class_name => 'Money'
attribute_decorator :english_date_of_birth, :class => (Class.new(CompositeDate) do
# This is a anonymous subclass of CompositeDate that supports the date in English order
def to_s
"#{@month}/#{@day}/#{@year}"
end
def self.parse(value)
month, day, year = value.scan(/(\d+)\/(\d+)\/(\d{4})/).flatten.map { |x| x.to_i }
new(day, month, year)
end
end)
The patch also deprecates composed_of and a small refactor of code I came across in activerecord/lib/active_record/reflection.rb.
Eloy
Comments and changes to this ticket
-
Michael Koziarski September 2nd, 2008 @ 08:13 AM
- Assigned user set to Michael Koziarski
-
Joel November 17th, 2008 @ 11:27 AM
This looks pretty neat, will this trigger changed? on the attribute (which composed_of does not seem to do atm)
-
Eloy Duran November 17th, 2008 @ 07:29 PM
Updated the patch for current HEAD.
@Joel: I assume you found out that it does in fact trigger the "dirty" status?
-
Joel November 18th, 2008 @ 04:09 AM
Indeed I did.
I had missed a commit of a plugin that busted it tho =)
-
Pascal Ehlert December 8th, 2008 @ 08:34 AM
In the example above, you say .to_s is for form helpers.
I couldn't check it out, yet, but is that true? The problem I've always had with my own solution to this problem is that form helpers were actually using attribute_before_typecast for reasons I never fully understood so that all formatting was gone.
Anyway, this looks nice and much more sophisticated than the existing solution so +1 for the patch.
-
Eloy Duran December 8th, 2008 @ 09:00 AM
@Pascal: You are right, so there is indeed an implementation of the foo_before_type_cast method (where
foo' is the decorator), which calls the #to_s method on the decorator instance to get the value for the form field.
-
Jeremy Kemper December 8th, 2008 @ 08:33 PM
- Milestone cleared.
- State changed from new to open
Nice! The API feels a bit wordy, though, like a specification rather than a declaration.
Consider
attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
versus
compose :date_of_birth, :of => [:day, :month, :year], :as => CompositeDate
or
view :date_of_birth, :as => ComposedDateAttribute.new(:day, :month, :year)
-
Manfred Stienstra December 9th, 2008 @ 10:34 AM
Jeremy, your compose example doesn't read really well when you're decorating one attribute. I guess we can meet halfway? How about this:
class Artist < ActiveRecord::Base view :location, :as => Location, :decorating => :gps_location view :date_of_birth, :as => CompositeDate, :decorating => [:day, :month, :year] validate_view :location validate_view :date_of_birth, :message => 'is not a valid date' end
-
Eloy Duran December 10th, 2008 @ 08:48 PM
Updated the API according to what Manfred suggested, did some cleaning and updated for edge.
-
Brennan Dunn December 10th, 2008 @ 10:42 PM
What about aiming toward something a bit more like this plugin I recently wrote: http://github.com/brennandunn/at...
The reason being is that I think, for the purpose of adding helper functionality to attributes, the extension model implemented by AR's association proxy/named_scope is more concise and easy to work with.
Example:
class Person < ActiveRecord::Base with_attribute :name do def has_middle_name? self.split(' ').size == 3 end def surname self.split(' ').last end end end @person = Person.create :name => 'John C. Doe' @person.name.has_middle_name? # => true @person.name.surname # => "Doe" # and of course, @person.name # => "John C. Doe"
Thoughts?
-
Manfred Stienstra December 11th, 2008 @ 02:49 PM
Yesterday, we were talking to Jeremy on IRC and I promised to show some examples of how the API would function.
In short, we're trying to find a way to compose attributes on an ActiveRecord class in such a way that it supports:
- Conversions: Attributes <=> Value object <=> Form data
- Validation
Jeremy raised two problems: using existing classes and assigning instances through the attribute writer. Basically the following:
class Artist < ActiveRecord::Base view :date_of_birth, :as => DateDecorator, :decorating => [:day, :month, :year] end artist = Artist.new(:date_of_birth => '24-05-1980') artist.date_of_birth += 10.days
From what I understand he thought that meant changing the API slightly and have an object do all the conversions:
view :date_of_birth, :as => ComposedDateAttribute.new(:day, :month, :year)
The conversion class could look something like this:
class ComposedDateAttribute attr_accessor :attributes def initialize(*attributes) @attributes = attributes.flatten end # Attributes => Value object def instantiate(record) Date.new(*attributes.reverse.map { |attribute| record.send(attribute) }) end # Value object => Form data def form_value(date) date.strftime("%Y-%m-%d") end # Form data => Value object def parse(string) Date.parse(string) end # Value object => Attributes def attributes(date) [date.year, date.month, date.day] end # Validation def valid?(string) string =~ /\d{4}-\d{2}-\d{2}/ end end
The upside is that you get an actual Date object, the downside is that it looks like crap and becomes even weirder if you want to use the same class as the value object. Maybe I misunderstood Jeremy's intentions?
As far as I can see this can be solved with the current implementation:
class DateDecorator < Date # Attributes => Value object def initialize(day, month, year) super(year, month, day) end # Value object => Form data def to_s strftime("%Y-%m-%d") end # Form data => Value object def self.parse(value) case value when Date new(value.day, value.month, value.year) when CompositeDate value else # Assume it's a String day, month, year = value.scan(/(\d\d)-(\d\d)-(\d{4})/).flatten.map { |x| x.to_i } new(day, month, year) end end # Value object => Attributes def to_a [day, month, year] end # Validation def valid? day != 0 && month != 0 && year != 0 end end
The upside of this solution is that the class makes sense: it's an actual decorator. The downside is that strict class checking code might break. I guess the method names should change to be more expressive so this use case makes more sense.
Finally, Jeremy says that he "[doesn't] think it should be the value object's responsibility to understand how it's composed from the database." I completely agree, that's why we use a decorator object to sit 'around' the value object to facilitate the conversions.
-
Jeremy Kemper December 11th, 2008 @ 09:45 PM
Combining the value object and it's parse/dump behavior is more confusing to me. It's a poor separation of concerns that makes working with existing classes cumbersome.
I suggest
class ComposedDateView def initialize(year, month, day) @y, @m, @d = year, month, day end def load(record) Date.new(record.send(@y), record.send(@m), record.send(@d)) end def parse(string) Date.parse(string) end def dump(date) { @y => date.year, @m => date.month, @d => date.day } end end
Then we can do nice things like share attribute types as plugins, subclass composers to change behavior without having to change the value object code, and refactor YAML serialization as a view.
The form value and validation stuff aren't really necessary. We can already handle that with ValueObject#to_s and existing model validations.
-
Jeremy Kemper December 11th, 2008 @ 09:53 PM
YAML example:
class YamlView def initialize(attr) @attr = attr end def load(record) parse(record.send(@attr)) end def parse(string) YAML.load(string) if string end def dump(object) { @attr => is_yaml?(object) ? object : YAML.dump(object) } end protected def is_yaml?(object) object.is_a?(String) && object =~ /\A---/ end end
-
Manfred Stienstra December 12th, 2008 @ 10:41 AM
I have to admit, when you limit the responsibility of the view class to just loading, parsing, and dumping it looks ok.
I don't agree on the ValueObject#to_s thing though, form helpers currently use
#{attribute_name}_before_type_cast
on the record instead ofto_s
to get the value to put back into the form on validation errors. So that still has to be addressed somehow.If you like, I can set up an example project to explore your proposal.
-
Eloy Duran December 13th, 2008 @ 11:02 AM
@Manfred: I think that'd be a good idea. Maybe include our real use case that it all was based on?
-
Michael Koziarski February 1st, 2009 @ 02:04 AM
- Assigned user changed from Michael Koziarski to Jeremy Kemper
- Milestone set to 2.x
Removing from 2.3, I'd still like to get this in but we're close enough now that this shouldn't be 'blocking'
Assigning to jeremy
-
Manfred Stienstra May 26th, 2009 @ 11:33 AM
- Milestone cleared.
I've been working on this again as part of a new application. I've been abstracting some of the composed, formatted and decorated attribute accessors. Now I basically do the following:
views :fee, :as => Fee.new(:fee_in_cents)
class Fee < ActiveRecord::AttributeView def load(fee_in_cents) if fee_in_cents.blank? or fee_in_cents.zero? '' elsif (fee_in_cents % 100).zero? "%d" % (fee_in_cents / 100) else "%.2f" % (fee_in_cents / 100.0) end end def parse(input) return nil if input.blank? input = input.gsub(/[^\.^\,^\d]/, '') whole, cents = input.split(/\,|\./) (whole.to_i * 100) + cents.to_i end end
I've solved the problem with forms and before_type_cast. I'm considering a few things now:
- Should we add a format method to the view so you also get a
formatted_fee accessor?
- Should we allow people to specify which accessors get defined (ie. :only_define => [:getter, :formatter])
Current implementation on: http://gist.github.com/118033
-
Chris Hapgood June 25th, 2009 @ 05:11 PM
Manfred's example feels like a natural evolution in Rails. Jeremy hits the key point: it factors out the view behavior into a kind of Presenter, but since it also accommodates reversing the Presenter (to parse input) it might be called an attribute agent. It could allow much of the existing composed_of behavior to remain since the construction of value objects from DB attributes is orthogonal to view behavior (although the :converter option might be merged into these attribute agents).
Once you've added support for these agents, why not take the next step and instantiate simple agents for EVERY model attribute and eliminate the troublesome call to before_type_cast by the AR form helper altogether? My impression is that before_type_cast has become the de facto presenter for AR attributes and it doesn't feel right.
You would also expose an ideal entry point for i18n (parsing/presenting of dates, times, floats, etc.).
-
Michael Koziarski June 26th, 2009 @ 06:11 AM
I can't see a reason to allow configuration for the different methods to be defined. If they really care you can always either override them after the fact or undef_method if you want to lose them.
-
Jeremy Kemper May 4th, 2010 @ 06:18 PM
- Milestone set to 3.1
-
Aaron Patterson September 22nd, 2010 @ 10:45 PM
- Assigned user changed from Jeremy Kemper to Aaron Patterson
- Importance changed from to Low
Create your profile
Help contribute to this project by taking a few moments to create your personal profile. Create your profile »
<h2 style="font-size: 14px">Tickets have moved to Github</h2>
The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>
People watching this ticket
Attachments
Referenced by
- 892 composed_of constructor and converter options I appreciate the efforts of Eloy to provide a complete re...
- 2675 Support for multiparameter attribute assignment on virtual attribute writers If it was because of the composed_of replacement that I w...
- 2675 Support for multiparameter attribute assignment on virtual attribute writers I'd prefer to fix it 'right' rather than just default to ...