重写对象属性 - 使用Moose做到最好的方法吗?

时间:2011-05-24 09:50:43

标签: perl moose

让我们看看SO问题输入机器人预测,显然是基于问题标题发布的,是否会实现:

  

您提出的问题似乎是主观的,可能会被关闭。

使用Perl / Moose,我想弥合商家文章代表的两种方式之间的不匹配。让文章有namequantityprice。表示的第一种方式是将数量设置为任何数值,包括十进制值,因此您可以使用3.5米的绳索或电缆。第二个,我必须与之接口,唉,不灵活,并且需要quantity为整数。因此,我必须重写我的对象以将quantity设置为1,并在name中包含实际数量。 (是的,这是一个黑客,但我想让这个例子保持简单。)

所以这里的故事是一个属性的值会影响其他属性的值。

这是工作代码:

#!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;
然而,我想知道在穆斯是否有更好的成语。但也许我的问题都是主观的,值得快速结束。 : - )

根据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;

我同意它提供更好的关注点分离。另一方面,我不相信这是一个更好的解决方案,因为它增加了复杂性并且没有提供自动转换(为此我必须添加更多代码)。

1 个答案:

答案 0 :(得分:4)

根据您在评论中提供的信息,您实际上是在建模两个不同的但相关的东西。你曾经遇到过将这两件事作为一个单独的类而难以捉摸的问题。你最终没有正确地分离你的顾虑,并且拥有丑陋的调度逻辑。

你需要有两个带有通用API的类(一个角色会强制执行此操作)和一组强制转换,以便在两者之间轻松转换。

首先,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/;
        }
 }

然后你有一个代表你的内部文章的类,再次这是非常微不足道的。

 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]:您可能已经注意到我已将外部文章中的属性设置为只读。根据你所说的,这些对象应该是“仅消费”,但是如果你需要属性是可写的,你需要在数量上定义强制来处理以确保只存储整数。我将把它作为练习留给读者。

相关问题