Perl 重写对象属性-使用Moose的最佳方法?
让我们看看,显然是基于问题标题发布的所谓问题输入机器人预测是否会实现: 你问的问题似乎是主观的,很可能是封闭的 使用Perl/Moose,我想在表示商家文章的两种方式之间架起一座不匹配的桥梁。让一篇文章有Perl 重写对象属性-使用Moose的最佳方法?,perl,moose,Perl,Moose,让我们看看,显然是基于问题标题发布的所谓问题输入机器人预测是否会实现: 你问的问题似乎是主观的,很可能是封闭的 使用Perl/Moose,我想在表示商家文章的两种方式之间架起一座不匹配的桥梁。让一篇文章有名称、数量和价格。第一种表示方法是将“数量”设置为任何数值,包括十进制值,因此可以有3.5米长的绳索或电缆。第二个,我必须与之交互,唉,它是不灵活的,需要数量为整数。因此,我必须重写我的对象,将数量设置为1,并将实际数量包含在名称中。(是的,这是一个hack,但我想让示例保持简单。) 所以这里的
名称
、数量
和价格
。第一种表示方法是将“数量”设置为任何数值,包括十进制值,因此可以有3.5米长的绳索或电缆。第二个,我必须与之交互,唉,它是不灵活的,需要数量为整数。因此,我必须重写我的对象,将数量
设置为1,并将实际数量包含在名称
中。(是的,这是一个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;
然而,我想知道是否有一个更好的成语在驼鹿身上这样做。但也许我的问题都是主观的,应该迅速结束。:-)
根据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的类(一个角色将执行此操作)和一组强制,以便在这两个类之间轻松转换
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
);
}
}
首先,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来提供更清洁的糖,但我选择了纯驼鹿
[^1]:您可能已经注意到,我已将外部文章中的属性设置为只读。根据您所说的,这些对象应该是“仅使用”的,但是如果您需要属性是可写的,那么您需要定义数量强制,以确保只存储整数。我将把它作为一个练习留给读者。我很困惑,为什么你不能总是将数量存储为num,并在必要时使用一个方法将num作为int返回?@perigrin-该类的目的是符合一个总是要求数量
为整数的服务,永远不允许它成为其他任何东西。另一方面,我希望尽可能方便地使用它,这意味着我应该能够将它与它应该工作的系统中的数据一起使用。因此,它是从我们的文章数据到他们的文章数据的桥梁(或转换器)。转换只需以一种方式进行,从我们的表示到他们的表示。@perigrin-关于你的问题,这里的要点是,作为quantity
的十进制数将影响多个属性,或者,正如我上面所写的,“一个属性的值影响其他属性的值。”因此,如果我存储原始值,然后,我必须在输出上有一个转换方法(到外部服务),但为了简单起见,我的想法是在输入上有一个转换,我不在乎这样丢失信息,因为类的目的是单向数据自适应。谢谢,@perigrin。我已经更新了我的问题,将您的代码示例和我的代码示例合并在一起。如上所述,我认为这个Moose::Role
解决方案通过提供关注点分离而增加了相当多的复杂性,在本例中我实际上不需要这样做。但是,看到一种不同的方法并比较两者是很有启发性的。公平地说,我的建议是正确的。不同但相似仍然不同;)