Overwriting object attributes is the best way to do this with Moose?

Let's see if the predictions of the SO request robot come true, explicitly released based only on the name of the question:

The question you ask seems subjective and is likely to be closed.

Using Perl / Moose, I would like to bring inconsistency between the two methods presented by trading articles. Let the article have a name , quantity and price . The first way this seems to be is the quantity given for any numerical value, including decimal values, so you can have 3.5 meters of rope or cable. The second one I have to interact with is, alas, inflexible, and requires quantity be an integer. So I have to rewrite my object in order to set quantity to 1 and include the actual value in name . (Yes, itโ€™s a hack, but I wanted this example to be simple.)

So the story here is that one property value affects the other property values.

Here's the working code:

 #!perl package Article; use Moose; has name => is => 'rw', isa => 'Str', required => 1; has quantity => is => 'rw', isa => 'Num', required => 1; has price => is => 'rw', isa => 'Num', required => 1; around BUILDARGS => sub { my $orig = shift; my $class = shift; my %args = @_ == 1 ? %{$_[0]} : @_; my $q = $args{quantity}; if ( $q != int $q ) { $args{name} .= " ($q)"; $args{price} *= $q; $args{quantity} = 1; } return $class->$orig( %args ); }; sub itemprice { $_[0]->quantity * $_[0]->price } sub as_string { return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_, qw/quantity name price itemprice/; } package main; use Test::More; my $table = Article->new({ name => 'Table', quantity => 1, price => 199 }); is $table->itemprice, 199, $table->as_string; my $chairs = Article->new( name => 'Chair', quantity => 4, price => 45.50 ); is $chairs->itemprice, 182, $chairs->as_string; my $rope = Article->new( name => 'Rope', quantity => 3.5, price => 2.80 ); is $rope->itemprice, 9.80, $rope->as_string; is $rope->quantity, 1, 'quantity set to 1'; is $rope->name, 'Rope (3.5)', 'name includes original quantity'; done_testing; 

I wonder, however, if there is a more perfect idiom for this in the Muse. But maybe my question is all subjective and deserves a quick close. :-)

UPDATE based on perigrin answer

I adapted the perigrin code example (minor bugs and syntax 5.10) and tagged my tests at its end:

 package Article::Interface; use Moose::Role; requires qw(name quantity price); sub itemprice { $_[0]->quantity * $_[0]->price } sub as_string { return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_, qw/quantity name price itemprice/; } package Article::Types; use Moose::Util::TypeConstraints; class_type 'Article::Internal'; class_type 'Article::External'; coerce 'Article::External' => from 'Article::Internal' => via { Article::External->new( name => sprintf( '%s (%s)', $_->name, $_->quantity ), quantity => 1, price => $_->quantity * $_->price ); }; package Article::Internal; use Moose; use Moose::Util::TypeConstraints; has name => isa => 'Str', is => 'rw', required => 1; has quantity => isa => 'Num', is => 'rw', required => 1; has price => isa => 'Num', is => 'rw', required => 1; my $constraint = find_type_constraint('Article::External'); =useless for this case # Moose::Manual::Construction - "You should never call $self->SUPER::BUILD, # nor"should you ever apply a method modifier to BUILD." sub BUILD { my $self = shift; my $q = $self->quantity; # BUILD does not return the object to the caller, # so it CANNOT BE USED to trigger the coercion. return $q == int $q ? $self : $constraint->coerce( $self ); } =cut with qw(Article::Interface); # need to put this at the end package Article::External; use Moose; has name => isa => 'Str', is => 'ro', required => 1; has quantity => isa => 'Int', is => 'ro', required => 1; has price => isa => 'Num', is => 'ro', required => 1; sub itemprice { $_[0]->price } # override with qw(Article::Interface); # need to put this at the end package main; use Test::More; my $table = Article::Internal->new( { name => 'Table', quantity => 1, price => 199 }); is $table->itemprice, 199, $table->as_string; is $table->quantity, 1; is $table->name, 'Table'; my $chairs = Article::Internal->new( name => 'Chair', quantity => 4, price => 45.50 ); is $chairs->itemprice, 182, $chairs->as_string; is $chairs->quantity, 4; is $chairs->name, 'Chair'; my $rope = Article::Internal->new( name => 'Rope', quantity => 3.5, price => 2.80 ); # I can trigger the conversion manually. $rope = $constraint->coerce( $rope ); # I'd like the conversion to be automatic, though. # But I cannot use BUILD for doing that. - XXX # Looks like I'd have to add a factory method that inspects the # parameters and does the conversion if needed, and it is always # needed when the `quantity` isn't an integer. isa_ok $rope, 'Article::External'; is $rope->itemprice, 9.80, $rope->as_string; is $rope->quantity, 1, 'quantity set to 1'; is $rope->name, 'Rope (3.5)', 'name includes original quantity'; done_testing; 

I agree that this provides a better separation of concerns. On the other hand, I'm not sure if this is the best solution for my purpose, as it adds complexity and does not provide for automatic conversion (for which I will have to add more code).

+4
source share
1 answer

Based on the information you provided in the comments, you are actually simulating two different but related to each other. You are faced with ugliness, trying to keep these two things as one class. You end up not properly separating your problems and have ugly submit logic.

You need to have two classes with a common API (the role will provide this) and a set of constraints that are easy to translate between them.

At first the API is really direct.

  package Article::Interface { use Moose::Role; requires qw(name quantity price); sub itemprice { $_[0]->quantity * $_[0]->price } sub as_string {  return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,  qw/quantity name price itemprice/; } } 

Then you have a class to represent your internal articles, again, this is pretty trivial.

  package Article::Internal { use Moose; has name => ( isa 'Str', is => 'rw', required => 1); has [qw(quantity price)] => ( isa => 'Num', is => 'rw', required => 1); # because of timing issues we need to put this at the end with qw(Article::Interface); } 

Finally, you have a class to represent your external articles. In this case, you must override some methods from the interface in order to deal with the fact that your attributes will be specialized [^ 1].

  package Article::External { use Moose; has name => ( isa 'Str', is => 'ro', required => 1); has quantity => ( isa => 'Int', is => 'ro', required => 1); has price => (isa => 'Num', is => 'ro', required => 1); sub itemprice { $_[0]->price } # because of timing issues we need to put this at the end with qw(Article::Interface); } 

Finally, you define a simple enforcement process to translate between the two.

 package Article::Types { use Moose::Util::TypeConstraints; class_type 'Article::Internal'; class_type 'Article::External'; coerce 'Article::Exteral' => from 'Article::Internal' => via { Article::External->new( name => $_->name, quantity => int $_->quantity, price => $_->quantity * $_->price ); } } 

You can force this coercion manually with

 find_type_constraint('Article::External')->coerce($internal_article); 

In addition, MooseX :: Types can be used for this last piece to provide cleaner sugar, but I decided to stick with pure Musa here.

[^ 1]: You may have noticed that I added attributes to the read-only article. From what you said, these objects should be โ€œconsume onlyโ€, but if you need attributes for writing, you need to define a compulsion to count in order to deal with storing only integers. I will leave this as an exercise for the reader.

+4
source

All Articles