重写对象属性-使用Moose的最佳方法?

| 让我们看看显然仅基于问题标题发布的SO问题输入机器人的预测是否将实现:   您要提出的问题似乎很主观,很可能已经解决。 使用Perl / Moose,我想弥合两种表示商人文章的方式之间的不匹配。令文章具有
name
quantity
price
。表示此问题的第一种方法是将数量设置为任何数字值,包括十进制值,因此您可以使用3.5米的绳索或电缆。我必须与之交互的第二个元素是a,不灵活,并且要求ѭ1是整数。因此,我必须重写对象以将
quantity
设置为1,并将实际数量包括在
name
中。 (是的,这是一个hack,但我想使示例保持简单。) 因此,这里的故事是一个属性的值会影响其他属性的值。 这是工作代码:
#!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;
但是,我想知道在Moose中是否有更好的成语来做到这一点。但是也许我的问题是主观的,应该迅速结束。 :-) 根据perigrin的答案进行更新 我已经改编了perigrin的代码示例(较小的错误和5.10语法),并将测试标记在其结尾:
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;
我同意这可以更好地分离关注点。另一方面,我不认为这对我来说是一个更好的解决方案,因为它增加了复杂性,并且没有提供自动转换(为此我必须添加更多的代码)。     
已邀请:
根据您在注释中提供的信息,您实际上是在建模两个不同但相关的事物。您曾经遇到过试图将这两件事保持为一个类的丑陋。您最终无法正确分离关注点,并拥有难看的调度逻辑。 您需要两个带有通用API的类(一个Role会强制执行)和一组强制以轻松地在两者之间进行转换。 首先,API确实很简单。
 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/;
        }
 }
然后,您有一个Class来代表您的内部Articles,同样,这很简单。
 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);
 }
最后,您有一堂课来代表您的外部文章。在这一步中,您必须重写接口中的某些方法,以处理您的属性将被专门化的事实[^ 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);
 }
最后,您定义一个简单的强制例程在两者之间进行转换。
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
         );
    }
}
您可以使用以下方法手动触发此强制:
find_type_constraint(\'Article::External\')->coerce($internal_article);
另外,MooseX :: Types可以用于最后一部分以提供更清洁的糖,但是我选择在这里坚持使用纯Moose。 [^ 1]:您可能已经注意到,我已将“外部”文章中的属性设为只读。从您所说的来看,这些对象应该是“仅使用”,但是如果您需要可写的属性,则需要在数量上定义一个强制变量,以确保仅存储整数。我会将其作为练习留给读者。     

要回复问题请先登录注册