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

时间:2012-01-27 10:32:03

标签: perl oop 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;

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

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

  • 有没有其他方法可以用这样的东西创建一行(示例不起作用!)(顺便说一下:还有哪些方法可以存在?):

>

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

感谢您的回答!

2 个答案:

答案 0 :(得分: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 )
);

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

现在,只要设置了endlength,他们就会清除length中的值,导致它在下次调用时重建。

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

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

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

是的,将length作为属性是有意义的。虽然它可以从其他数据派生,但您希望缓存该计算。如果Moose有办法明确声明start纯粹是从end$line2->end(x => 3, y => 3);派生出来的话,那将会很好,因此应该自动缓存并重新计算,但事实并非如此。

  

我怎样才能做出这样的事情? use Moose::Util::TypeConstraints; subtype 'Point::OrHashRef', as 'Point'; coerce 'Point::OrHashRef', from 'HashRef', via { Point->new( x => $_->{x}, y => $_->{y} ) };

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

start

然后将endPoint::OrHashRef的类型更改为has 'start' => ( isa => 'Point::OrHashRef', is => 'rw', required => 1, coerce => 1, ); 并启用强制。

start

现在endnew $line = Line->new( start => { x => 1, y => 1 }, end => Point->new( x => 2, y => 2 ) ); $line->end({ x => 3, y => 3 ]); 将接受哈希引用并将它们静默转换为Point对象。

BUILDARGS

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

您何时使用类型强制,何时使用new?一个好的 经验法则是如果参数新映射到属性,则使用类型 强迫。然后{ 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; 和属性可以一致地行动,其他类可以使用该类型使其Point属性行为相同。

在这里,一起进行一些测试。

{{1}}

答案 1 :(得分:0)

这远不是Moose问题,而是面向对象的设计问题。但在这些方面,有一些有趣的事情需要指出:

  1. 行具有值语义,这意味着具有不同点的两行实际上是不同的行。读写点属性对于行没有意义。这些应该是只读属性;如果你需要一个Line来包含一个不同的Point,你实际上需要一个不同的Line。
  2. 点数,类似。
  3. 对于给定的Line,其长度是常量,并且可以完全从其Point属性派生。使行的长度成为一个属性使问题变得复杂:它可以构造一个不可能的行,并且(当与读写点属性结合时)打开了一致性错误的大门。使长度变成普通方法更自然,更不容易出错。
  4. 使用属性支持length方法是性能优化。与所有优化一样,由此引入的额外复杂性必须通过分析来证明。
  5. 回到Moose特定的问题。 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的开放式界面非常适合一般情况,但收紧它们是降低整体复杂性的好方法。