用Moose做对象组合的最佳方法是什么?

hop*_*sch 4 oop perl moose

关于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;
Run Code Online (Sandbox Code Playgroud)

上面的代码按预期工作.现在我的问题:

  • 这是解决问题/做简单对象组合的最佳方法吗?

  • 是否有另一种方法来创建这样的行(示例不起作用!)(顺便说一下:还有哪些方法存在?):

>

my $line2 = Line->new( start->x => 1, start->y => 1, end => Point->new( x => 2, y => 2 ) );
Run Code Online (Sandbox Code Playgroud)
  • 如何更改坐标时触发长度的自动重新计算?或者,拥有像"长度"这样可以"轻松"从其他属性派生的属性是没有意义的吗?这些值(长度)应该更好地作为函数提供吗?

>

$line->end->x(3);
$line->end->y(3);
$len = $line->length;
Run Code Online (Sandbox Code Playgroud)
  • 我怎样才能做出这样的事情呢?什么是一次改变点的方法 - 而不是改变每个坐标?

>

$line2->end(x => 3, y =>3);
Run Code Online (Sandbox Code Playgroud)

谢谢你的回答!

Sch*_*ern 6

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

如果不知道你将要做什么,这太主观了,而且问题过于简单化了.但我可以说你正在做的事情没有错.

我所做的改变是移动工作来计算两点之间的距离为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 )
);
Run Code Online (Sandbox Code Playgroud)

我要注意的第一件事是你不是通过前面的对象来节省很多打字......但就像我说这是一个简单的例子所以让我们假设使对象变得单调乏味.有很多方法可以获得你想要的东西,但有一种方法是编写一个可以转换参数的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;
}
Run Code Online (Sandbox Code Playgroud)

还有第二种方法可以使用类型强制来实现,在某些情况下更有意义.请参阅下面的解答$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
);
Run Code Online (Sandbox Code Playgroud)

现在,每当startend设置,他们将在明确的价值length使其在下一次它被称为重建.

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

第二种是将Point的属性声明为只读.您可以创建一个新对象,而不是更改对象.然后,它的值无法更改,您可以安全地根据它们缓存计算.逻辑延伸到Line和Polygon等等.

这也让您有机会使用Flyweight模式.如果Point是只读的,那么每个坐标只需要一个对象. Point->new成为新工厂或返回现有工厂的工厂.这可以节省大量内存.同样,这个逻辑延伸到Line和Polygon等等.

是的,拥有length属性是有意义的.虽然它可以从其他数据派生,但您希望缓存该计算.这将是很好,如果穆斯有办法明确声明,length纯粹是源于startend,因此应当自动缓存,并重新计算,但事实并非如此.

我怎样才能做出这样的事情呢? $line2->end(x => 3, y => 3);

实现这一目标的最简单的方法是使用类型强制.您定义了一个子类型,它将散列引用转换为Point.最好在Point中定义它,而不是Line,以便其他类在使用Points时可以使用它.

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

然后改变的类型startendPoint::OrHashRef并打开胁迫.

has 'start' => (
    isa             => 'Point::OrHashRef',
    is              => 'rw',
    required        => 1,
    coerce          => 1,
);
Run Code Online (Sandbox Code Playgroud)

现在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 ]);
Run Code Online (Sandbox Code Playgroud)

它必须是散列引用,而不是散列,因为Moose属性只接受标量.

你什么时候使用类型强制,什么时候使用BUILDARGS?一个好的经验法则是,如果新映射到属性的参数,则使用类型强制.然后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;
Run Code Online (Sandbox Code Playgroud)