使用 Moose 进行对象组合的最佳方法是什么?
只是关于驼鹿最佳实践的初学者问题:
从简单的“点”示例开始,我想构建一个“线”对象,由两个点组成并具有长度属性,描述起点和终点之间的距离。
{
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);
感谢您的任何答复!
Just a beginners question on Best Practice with Moose:
Starting on the simple "point" example I want to build a "line" - object, consisting of two points and having a lenght attribute, describing the distance between starting and ending point.
{
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;
The code above works as expected.
Now my questions:
Is this the best way to solve the problem /to do simple object composition?
Is there another way to create the line with something like this (example does not work!) (BTW: Which other ways do exist at all?):
>
my $line2 = Line->new( start->x => 1, start->y => 1, end => Point->new( x => 2, y => 2 ) );
- How can I trigger an automatic recalculation of length when coordinates are changed? Or does it make no sense to have attributes like length which can "easily" derived from other attributes? Should those values (length) better be provided as functions?
>
$line->end->x(3);
$line->end->y(3);
$len = $line->length;
- How can I make something like this possible? What's the way to change the point at once - instead of changing each coordinate?
>
$line2->end(x => 3, y =>3);
Thanks for any answers!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
在不知道你要做什么的情况下回答这个问题太主观了,而且问题也过于简单化。但我可以说你所做的没有任何问题。
我要做的更改是将计算两点之间距离的工作移至 Point 中。然后其他人就可以利用。
我要注意的第一件事是,通过前面的对象,您不会节省太多打字时间......但就像我说的,这是一个简单的示例,所以让我们假设创建对象是乏味的。有很多方法可以得到你想要的,但一种方法是编写 BUILDARGS 转换参数的方法。手册中的示例有点奇怪,这里有一个更常见的用法。
还有第二种方法可以通过类型强制来实现,这在某些情况下更有意义。请参阅下面关于如何执行
$line2->end(x => 3, y =>3)
的答案。奇怪的是,有一个触发器!当属性更改时,将调用属性上的触发器。正如@Ether指出的,您可以添加更清晰到
length
,然后触发器可以调用它来取消设置length
。这并不违反length
是只读的。现在,每当设置
start
或end
时,它们都会清除length
中的值,从而在下次调用时重新构建它。这确实带来了一个问题...如果
start
和end
被修改,length
将会改变,但是如果直接改变Point对象会怎样?$line->start->y(4)
?如果您的 Point 对象被另一段代码引用并且他们更改了它怎么办?这些都不会导致长度重新计算。你有两个选择。首先是使length
完全动态化,这可能成本高昂。第二种是将Point的属性声明为只读。您无需更改对象,而是创建一个新对象。那么它的值就无法更改,并且您可以安全地缓存基于它们的计算。该逻辑延伸至线和多边形等。
这也使您有机会使用享元模式。如果 Point 是只读的,则每个坐标只需要一个对象。
Point->new
成为一个工厂,要么生成一个新对象,要么返回一个现有对象。这可以节省大量内存。同样,这种逻辑延伸到线和多边形等。是的,将
length
作为属性确实有意义。虽然它可以从其他数据派生,但您希望缓存该计算。如果 Moose 有一种方法可以显式声明length
纯粹是从start
和end
派生的,因此应该自动缓存和重新计算,那就太好了,但事实并非如此。实现这一点的最简单的方法是使用 类型强制。
您定义一个子类型,它将哈希引用转换为 Point。它是
最好在 Point 中定义它,而不是在 Line 中,以便其他类可以
当他们使用积分时使用它。
然后将
start
和end
的类型更改为Point::OrHashRef
并开启强制转换。现在
start
、end
和new
将接受哈希引用并将它们默默地转换为 Point 对象。它必须是哈希引用,而不是哈希,因为 Moose 属性仅采用标量。
什么时候使用类型强制以及什么时候使用
BUILDARGS
?一个好的经验法则是,如果 new 的参数映射到属性,则使用 type
强迫。然后
new
并且属性可以一致地起作用,并且其他类可以使用该类型来使它们的Point属性具有相同的行为。一切都在这里,并进行了一些测试。
That's too subjective to answer without knowing what you're going to do with it, and the problem is overly simplistic. But I can say there's nothing wrong with what you're doing.
The change I'd make is to move the work to calculate the distance between two points into Point. Then others can take advantage.
First thing I'd note is you're not saving much typing by foregoing the object... but like I said this is a simplistic example so let's presume making the object is tedious. There's a bunch of ways to get what you want, but one way is to write a BUILDARGS method which transforms the arguments. The example in the manual is kinda bizarre, here's a more common use.
There is a second way to do it with type coercion, which in some cases makes more sense. See the answer to how to do
$line2->end(x => 3, y =>3)
below.Oddly enough, with a trigger! A trigger on an attribute will be called when that attribute changes. As @Ether pointed out, you can add a clearer to
length
which the trigger can then call to unsetlength
. This does not violatelength
being read-only.Now whenever
start
orend
are set they will clear the value inlength
causing it to be rebuilt the next time it's called.This does bring up a problem...
length
will change ifstart
andend
are modified, but what if the Point objects are changed directly with$line->start->y(4)
? What if your Point object is referenced by another piece of code and they change it? Neither of these will cause a length recalculation. You have two options. First is to makelength
entirely dynamic which might be costly.The second is to declare Point's attributes to be read-only. Instead of changing the object, you create a new one. Then its values cannot be changed and you're safe to cache calculations based on them. The logic extends out to Line and Polygon and so on.
This also gives you the opportunity to use the Flyweight pattern. If Point is read-only, then there only needs to be one object for each coordinate.
Point->new
becomes a factory either making a new object OR returning an existing one. This can save a lot of memory. Again, this logic extends out to Line and Polygon and so on.Yes it does make sense to have
length
as an attribute. While it can be derived from other data, you want to cache that calculation. It would be nice if Moose had a way to explicitly declare thatlength
was purely derived fromstart
andend
and thus should automatically cache and recalculate, but it doesn't.The least hacky way to accomplish this would be with type coercion.
You define a subtype which will turn a hash ref into a Point. It's
best to define it in Point, not Line, so that other classes can make
use of it when they use Points.
Then change the type of
start
andend
toPoint::OrHashRef
and turn on coercion.Now
start
,end
andnew
will accept hash refs and turn them silently into Point objects.It has to be a hash ref, not a hash, because Moose attributes only take scalars.
When do you use type coercion and when do you use
BUILDARGS
? A goodrule of thumb is if the argument to new maps to an attribute, use type
coercion. Then
new
and the attributes can act consistently and other classes can use the type to make their Point attributes act the same.Here it is, all together, with some tests.
这与其说是驼鹿问题,不如说是面向对象设计问题。但在这些术语中,有一些有趣的事情需要指出:
length
方法是一种性能优化。与所有优化一样,由此引入的额外复杂性必须通过分析来证明其合理性。回到驼鹿特定的问题。 Moose 不提供额外的构造函数形式。另一方面,它不会阻止您提供自己的构造函数形式,因此:
提供更方便且受约束的构造函数是相当常见的做法。 Moose 的开放式接口非常适合一般情况,但收紧它们是降低整体复杂性的好方法。
This is much less a Moose question than an object-oriented design question. But in those terms, there are a few things that are interesting to point out:
length
method with an attribute is a performance optimization. Like all optimizations, the additional complication thereby introduced must be justified by profiling.Back to Moose-specific questions. Moose doesn't provide additional constructor forms. On the other hand, it doesn't prevent you from providing your own constructor forms, thus:
Providing more-convenient and -constrained constructors is fairly common practice. Moose's wide-open interfaces are great for the general case, but tightening them is a good way to reduce overall complexity.