使用 Moose 进行对象组合的最佳方法是什么?

发布于 2024-12-29 02:36:39 字数 1555 浏览 3 评论 0原文

只是关于驼鹿最佳实践的初学者问题:

从简单的“点”示例开始,我想构建一个“线”对象,由两个点组成并具有长度属性,描述起点和终点之间的距离。

{
  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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

疧_╮線 2025-01-05 02:36:39

这是解决简单对象问题的最佳方法吗
组成?

在不知道你要做什么的情况下回答这个问题太主观了,而且问题也过于简单化。但我可以说你所做的没有任何问题。

我要做的更改是将计算两点之间距离的工作移至 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) 的答案。

如何在以下情况下触发自动重新计算长度:
坐标改变了?

奇怪的是,有一个触发器!当属性更改时,将调用属性上的触发器。正如@Ether指出的,您可以添加更清晰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
);

现在,每当设置 startend 时,它们都会清除 length 中的值,从而在下次调用时重新构建它。

这确实带来了一个问题...如果startend被修改,length将会改变,但是如果直接改变Point对象会怎样? $line->start->y(4)?如果您的 Point 对象被另一段代码引用并且他们更改了它怎么办?这些都不会导致长度重新计算。你有两个选择。首先是使 length 完全动态化,这可能成本高昂。

第二种是将Point的属性声明为只读。您无需更改对象,而是创建一个新对象。那么它的值就无法更改,并且您可以安全地缓存基于它们的计算。该逻辑延伸至线和多边形等。

这也使您有机会使用享元模式。如果 Point 是只读的,则每个坐标只需要一个对象。 Point->new 成为一个工厂,要么生成一个新对象,要么返回一个现有对象。这可以节省大量内存。同样,这种逻辑延伸到线和多边形等。

是的,将 length 作为属性确实有意义。虽然它可以从其他数据派生,但您希望缓存该计算。如果 Moose 有一种方法可以显式声明 length 纯粹是从 startend 派生的,因此应该自动缓存和重新计算,那就太好了,但事实并非如此。

我怎样才能使这样的事情成为可能? $line2->end(x => 3, y => 3);

实现这一点的最简单的方法是使用 类型强制
您定义一个子类型,它将哈希引用转换为 Point。它是
最好在 Point 中定义它,而不是在 Line 中,以便其他类可以
当他们使用积分时使用它。

use Moose::Util::TypeConstraints;
subtype 'Point::OrHashRef',
    as 'Point';
coerce 'Point::OrHashRef',
    from 'HashRef',
    via { Point->new( x => $_->{x}, y => $_->{y} ) };

然后将 startend 的类型更改为 Point::OrHashRef 并开启强制转换。

has 'start' => (
    isa             => 'Point::OrHashRef',
    is              => 'rw',
    required        => 1,
    coerce          => 1,
);

现在 startend 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 的参数映射到属性,则使用 type
强迫。然后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;

Is this the best way to solve the problem to do simple object
composition?

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.

# How do I do something like this?
my $line2 = Line->new(
    start->x => 1, start->y => 1,
    end => Point->new( x => 2, y => 2 )
);

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.

# 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;
}

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.

How can I trigger an automatic recalculation of length when
coordinates are changed?

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 unset length. This does not violate length being read-only.

# 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
);

Now whenever start or end are set they will clear the value in length causing it to be rebuilt the next time it's called.

This does bring up a problem... length will change if start and end 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 make length 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 that length was purely derived from start and end and thus should automatically cache and recalculate, but it doesn't.

How can I make something like this possible? $line2->end(x => 3, y => 3);

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.

use Moose::Util::TypeConstraints;
subtype 'Point::OrHashRef',
    as 'Point';
coerce 'Point::OrHashRef',
    from 'HashRef',
    via { Point->new( x => $_->{x}, y => $_->{y} ) };

Then change the type of start and end to Point::OrHashRef and turn on coercion.

has 'start' => (
    isa             => 'Point::OrHashRef',
    is              => 'rw',
    required        => 1,
    coerce          => 1,
);

Now start, end and new will accept hash refs and turn them silently into Point objects.

$line = Line->new( start => { x => 1, y => 1 }, end => Point->new( x => 2, y => 2 ) );
$line->end({ x => 3, y => 3 ]);

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 good
rule 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.

{
    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;
江南月 2025-01-05 02:36:39

这与其说是驼鹿问题,不如说是面向对象设计问题。但在这些术语中,有一些有趣的事情需要指出:

  1. 线具有值语义,这意味着具有不同点的两条线实际上是不同的线。读写点属性对于线没有意义。这些应该是只读属性;如果您需要一条线来包含不同的点,那么实际上您需要一条不同的线。
  2. 点,同理。
  3. 对于给定的线,其长度是恒定的并且完全可以从其点属性导出。将线的长度作为属性使事情变得复杂:它使得构造不可能的线成为可能,并且(当与读写点属性结合时)打开了一致性错误的大门。让长度成为普通方法更自然,更不易出错。
  4. 使用属性支持 length 方法是一种性能优化。与所有优化一样,由此引入的额外复杂性必须通过分析来证明其合理性。

回到驼鹿特定的问题。 Moose 不提供额外的构造函数形式。另一方面,它不会阻止您提供自己的构造函数形式,因此:

sub new_from_coords {
  my ($class, $x1, $y1, X2, $y2) = @_;

  return $class->new(
    start => $class->_make_point($x1, $y1),
    end => $class->_make_point($x2, $y2),
  );
}

sub _make_point {
  my ($class, $x, $y) = @_;

  return Point->new(x => $x, y => $y);
}

my $line = Line->new_from_coords(2, 3, 6, 7);

提供更方便且受约束的构造函数是相当常见的做法。 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:

  1. Lines have value semantics, meaning that two lines that have different Points are in fact different Lines. Read-write Point attributes don't make sense for Lines. These should be read-only attributes; if you need a Line to contain a different Point, you in fact need a different Line.
  2. Points, similarly.
  3. For a given Line, its length is constant and completely derivable from its Point attributes. Making the Line's length an attribute complicates matters: It makes it possible to construct an impossible Line and (when combined with read-write Point attributes) opens the door to consistency bugs. It is more natural and less error-prone to make length an ordinary method.
  4. Backing the 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:

sub new_from_coords {
  my ($class, $x1, $y1, X2, $y2) = @_;

  return $class->new(
    start => $class->_make_point($x1, $y1),
    end => $class->_make_point($x2, $y2),
  );
}

sub _make_point {
  my ($class, $x, $y) = @_;

  return Point->new(x => $x, y => $y);
}

my $line = Line->new_from_coords(2, 3, 6, 7);

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文