solnic / virtus

[DISCONTINUED ] Attributes on Steroids for Plain Old Ruby Objects
MIT License
3.77k stars 228 forks source link

special treatment for ValueObjects #40

Closed senny closed 12 years ago

senny commented 12 years ago

It would be nice if virtus had like a submodule for building ValueObjects. On top of my head this could look something like:

class GeoLocation
  include Virtus::ValueObject

  attribute :latitude, Float
  attribute :longitude, Float
end

The Virtus::ValueObject module would set some sane defaults for building ValueObjects like:

place = GeoLocation.new(:latitute => 40.73216945026674, :longitude => -74.091796875)
the_same_place = GeoLocation.new(:latitute => 40.73216945026674, :longitude => -74.091796875)
place == the_same_place # => true

hash = {palce => 1}
hash[the_same_palce] # => 1

place.latitude = -74.091796875 # => ERROR: Attempt to call private method
blambeau commented 12 years ago

Agreed, building "true" values like this should be nice.

With respect to The Third Manifesto, which is somewhat related to this (through Veritas), this would provide the ability to create valid types (immutable sets of values) in an easy way.

Concerning,

place.latitude = -74.091796875 # => ERROR: Attempt to call private method

I definitely agree. The Third Manifesto goes one step further as it suggests something like this (I suppose that LatLng is actually GeoLocation ?)

place = LatLng.new(:latitute => 40.73, :longitude => -74.09)
place.latitude = -70,06   # ERROR
place.with_latitude(-70.06) == LatLng.new(:latitute => 40.73, :longitude => -70.06) # true
place.latitude # 40.73, that is, with_latitude is not an attr_writer at all.

i.e. with_XXX methods provide shortcuts to select values through single member updates.

senny commented 12 years ago

@blambeau I'm not sure if veritas should automatically generate these with_XXX methods, which return new ValueObjects based on self. I think this is more of a domain concern to provide the operations necessary to work with the value objects at hand. Sometimes this might be necessary but I guess we should not try to guess what the application needs.

Thanks for the LatLng hint ;) I fixed it in the Ticket description.

blambeau commented 12 years ago

@senny well with_XXX methods do not capture operations/operators to work with values but rather useful shortcuts for selectors. As your error message suggested the existence of private attr_writers, I just wanted to point out that IMHO the TTM is right in catching our attention that (when working with immutable values) with_xxx are actually very useful (even if kept private). They help building the operations/operators you are talking about. Just a 5 cents thought, though.

senny commented 12 years ago

@blambeau if kept private I think it could be a good addition to make the code clean. I was a little confused since your example suggested that they were public and I don't like the idea that a wealth of public methods is created in case you once need them ;)

blambeau commented 12 years ago

@senny strictly speaking, I think that TTM requires those operators to be automatically provided by the system and public. As I know that Veritas/Virtus are partly inspired by TTM, I just wanted to bring your attention here. Now, Veritas and Virtus are not building bricks for a valid D language (or do they @dkubb?), so I don't think conformance is actually required here. Anyway, knowing with_xxx (THE_xxx(value) := ... in TTM/TUTORIAL D) may help providing nice building blocks for implementing immutable types/values. I don't have a strong opinion about what should actually be supported by Virtus itself.

emmanuel commented 12 years ago

I really like the idea of a shortcut for Value Object (immutability and attribute-based equality). Virtus is certainly great for this use-case.

I think that this could be provide a decent starting point for value-object-like-equality. It's called Equalizable, but I've considered changing the name to AttributeEquality, since the whole purpose is to establish equality (#eql?, #==, and #hash) and #inspect based on attribute values, rather than identity.

emmanuel commented 12 years ago

(to be clear, I merely extracted Equalizable from dm-core, I didn't come up with it myself)

senny commented 12 years ago

@emmanuel where will such code live? Since aequitas and virtus are separate I guess virtus will have it's own implementation of Equalizable what are the thoughts about code-sharing between the DM-2 libraries?

emmanuel commented 12 years ago

A very good question. I'm not sure how best to share such 'support' code.

Something like Equalizable hardly seems like it merits its own gem, and yet it also feels wasteful to have a private implementation of such a primitive (value-object-definition) in each DM2-related gem.

Any ideas? /cc @solnic, @dkubb

dkubb commented 12 years ago

Fwiw, in Veritas I needed something similar to Equalizable, but the behaviour wasn't 100% the same since some of the semantics are defined in Relational Algebra, and I needed to respect those first., eg: https://github.com/dkubb/veritas/blob/master/lib/veritas/support/comparator.rb

Having gone through the process of doing this with Extlib, I'm not a huge fan of extracting things to a shared gem in the near term. Perhaps when it satisfies the rule of three, and we're using it 3 or more places, but for now I would focus on keeping the supporting libs in the "support" directory like we've done in Veritas/Virtus, and then revisit it later on.

solnic commented 12 years ago

Indeed that's a very good question.

I've been thinking about it many times because we already have a few cases where multiple libraries need common functionality. We will probably end up with a shared 'support' library. The best thing we can do is to make sure it's as tiny as possible and includes only truly essential shared functionality. We definitely don't want to create another ActiveSupport or Extlib though.

Time will tell what's actually required and we will figure it out later.

emmanuel commented 12 years ago

@senny, @blambeau — I've implemented Virtus::ValueObject in #53. Would you mind taking a look and let me know if that fits your needs? I think I've implemented everything I'm going to want for Conformitas, but I haven't started building on top of Virtus::ValueObject yet, so I'm not completely sure...

Also, I considered adding Virtus::ValueObject#with like this:

module Virtus::ValueObject
  def with(attribute_updates)
    self.class.new(attributes.merge(attribute_updates))
  end
end

What do you think about that?

solnic commented 12 years ago

closing this one since there's already a pending pull request with this feature