只是关于 Moose 最佳实践的初学者问题:
从简单的“点”示例开始,我想构建一个“线” - 对象,由两个点组成并具有 lenght 属性,描述起点和终点之间的距离。
{
package Point;
use Moose;
has 'x' => ( isa => 'Int', is => 'rw' );
has 'y' => ( isa => 'Int', is => 'rw' );
}
{
package Line;
use Moose;
has 'start' => (isa => 'Point', is => 'rw', required => 1, );
has 'end' => (isa => 'Point', is => 'rw', required => 1, );
has 'length' => (isa => 'Num', is => 'ro', builder => '_length', lazy => 1,);
sub _length {
my $self = shift;
my $dx = $self->end->x - $self->start->x;
my $dy = $self->end->y - $self->start->y;
return sqrt( $dx * $dx + $dy * $dy );
}
}
my $line = Line->new( start => Point->new( x => 1, y => 1 ), end => Point->new( x => 2, y => 2 ) );
my $len = $line->length;
上面的代码按预期工作。
现在我的问题:
>
my $line2 = Line->new( start->x => 1, start->y => 1, end => Point->new( x => 2, y => 2 ) );
>
$line->end->x(3);
$line->end->y(3);
$len = $line->length;
>
$line2->end(x => 3, y =>3);
感谢您提供任何答案!
最佳答案
Is this the best way to solve the problem to do simple object composition?
在不知道你将用它做什么的情况下回答这个问题太主观了,而且这个问题过于简单化了。但我可以说你所做的没有任何问题。
我所做的改变是将计算两点之间的距离的工作移动到 Point.然后其他人可以利用。
# How do I do something like this?
my $line2 = Line->new(
start->x => 1, start->y => 1,
end => Point->new( x => 2, y => 2 )
);
我要注意的第一件事是,通过前面的对象并没有节省太多打字的时间……但是就像我说的这是一个简单的例子,所以让我们假设制作对象是乏味的。有很多方法可以得到你想要的东西,但一种方法是写一个 BUILDARGS转换参数的方法。手册中的例子有点奇怪,这里有一个更常见的用法。
# Allow optional start_x, start_y, end_x and end_y.
# Error checking is left as an exercise for the reader.
sub BUILDARGS {
my $class = shift;
my %args = @_;
if( $args{start_x} ) {
$args{start} = Point->new(
x => delete $args{start_x},
y => delete $args{start_y}
);
}
if( $args{end_x} ) {
$args{end} = Point->new(
x => delete $args{end_x},
y => delete $args{end_y}
);
}
return \%args;
}
还有第二种方法可以通过类型强制来实现,这在某些情况下更有意义。看答案怎么做
$line2->end(x => 3, y =>3)
以下。How can I trigger an automatic recalculation of length when coordinates are changed?
奇怪的是,有一个触发器!当该属性更改时,将调用该属性上的触发器。正如@Ether 指出的,您可以添加 clearer至
length
然后触发器可以调用它来取消设置 length
.这不违反length
只读。# You can specify two identical attributes at once
has ['start', 'end'] => (
isa => 'Point',
is => 'rw',
required => 1,
trigger => sub {
return $_[0]->_clear_length;
}
);
has 'length' => (
isa => 'Num',
is => 'ro',
builder => '_build_length',
# Unlike builder, Moose creates _clear_length()
clearer => '_clear_length',
lazy => 1
);
现在每当
start
或 end
设置它们将清除 length
中的值导致它在下一次被调用时被重建。这确实带来了一个问题...
length
如果 start
会改变和 end
被修改了,但是如果 Point 对象直接用 $line->start->y(4)
改变会怎样? ?如果您的 Point 对象被另一段代码引用并且他们更改了它怎么办?这些都不会导致长度重新计算。你有两个选择。首先是制作length
完全动态的,这可能是昂贵的。二是声明Point的属性为只读。您不是更改对象,而是创建一个新对象。然后它的值不能改变,你可以安全地缓存基于它们的计算。逻辑扩展到 Line 和 Polygon 等。
这也让您有机会使用享元模式。如果 Point 是只读的,那么每个坐标只需要一个对象。
Point->new
成为制造新对象或返回现有对象的工厂。这可以节省大量内存。同样,此逻辑扩展到 Line 和 Polygon 等。是的,拥有
length
确实有意义作为属性。虽然它可以从其他数据派生,但您希望缓存该计算。如果 Moose 有办法明确声明 length
就好了纯粹来自 start
和 end
因此应该自动缓存和重新计算,但它没有。How can I make something like this possible?
$line2->end(x => 3, y => 3);
最简单的方法是使用 type coercion .
您定义了一个子类型,它将把散列引用转换为一个 Point。它是
最好在 Point 中定义,而不是 Line,以便其他类可以
当他们使用积分时使用它。
use Moose::Util::TypeConstraints;
subtype 'Point::OrHashRef',
as 'Point';
coerce 'Point::OrHashRef',
from 'HashRef',
via { Point->new( x => $_->{x}, y => $_->{y} ) };
然后更改
start
的类型和 end
至 Point::OrHashRef
并开启强制。has 'start' => (
isa => 'Point::OrHashRef',
is => 'rw',
required => 1,
coerce => 1,
);
现在
start
, end
和 new
将接受散列引用并将它们静默地转换为 Point 对象。$line = Line->new( start => { x => 1, y => 1 }, end => Point->new( x => 2, y => 2 ) );
$line->end({ x => 3, y => 3 ]);
它必须是一个散列引用,而不是一个散列,因为 Moose 属性只接受标量。
什么时候使用类型强制,什么时候使用
BUILDARGS
?一个好的经验法则是,如果 new 的参数映射到属性,请使用类型
强制。然后
new
并且属性可以一致地运行,其他类可以使用该类型使它们的 Point 属性运行相同。这里有一些测试。
{
package Point;
use Moose;
has 'x' => ( isa => 'Int', is => 'rw' );
has 'y' => ( isa => 'Int', is => 'rw' );
use Moose::Util::TypeConstraints;
subtype 'Point::OrHashRef',
as 'Point';
coerce 'Point::OrHashRef',
from 'HashRef',
via { Point->new( x => $_->{x}, y => $_->{y} ) };
sub distance {
my $start = shift;
my $end = shift;
my $dx = $end->x - $start->x;
my $dy = $end->y - $start->y;
return sqrt( $dx * $dx + $dy * $dy );
}
}
{
package Line;
use Moose;
# And the same for end
has ['start', 'end'] => (
isa => 'Point::OrHashRef',
coerce => 1,
is => 'rw',
required => 1,
trigger => sub {
$_[0]->_clear_length();
return;
}
);
has 'length' => (
isa => 'Num',
is => 'ro',
clearer => '_clear_length',
lazy => 1,
default => sub {
return $_[0]->start->distance( $_[0]->end );
}
);
}
use Test::More;
my $line = Line->new(
start => { x => 1, y => 1 },
end => Point->new( x => 2, y => 2 )
);
isa_ok $line, "Line";
isa_ok $line->start, "Point";
isa_ok $line->end, "Point";
like $line->length, qr/^1.4142135623731/;
$line->end({ x => 3, y => 3 });
like $line->length, qr/^2.82842712474619/, "length is rederived";
done_testing;
关于perl - 使用 Moose 进行对象组合的最佳方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9031939/