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

标签 perl oop moose

只是关于 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 指出的,您可以添加 clearerlength然后触发器可以调用它来取消设置 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 中的值导致它在下一次被调用时被重建。

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

    二是声明Point的属性为只读。您不是更改对象,而是创建一个新对象。然后它的值不能改变,你可以安全地缓存基于它们的计算。逻辑扩展到 Line 和 Polygon 等。

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

    是的,拥有 length 确实有意义作为属性。虽然它可以从其他数据派生,但您希望缓存该计算。如果 Moose 有办法明确声明 length 就好了纯粹来自 startend因此应该自动缓存和重新计算,但它没有。

    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的类型和 endPoint::OrHashRef并开启强制。
    has 'start' => (
        isa             => 'Point::OrHashRef',
        is              => 'rw',
        required        => 1,
        coerce          => 1,
    );
    

    现在 start , endnew将接受散列引用并将它们静默地转换为 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/

    相关文章:

    arrays - 通过改变数据结构来加速在 Perl 中的数组搜索

    perl - 尝试从使用 tkx (Tcl/tk) 的 Perl 脚本创建独立应用程序

    perl - 为什么我必须加载 Perl 类才能使用我从 YAML 反序列化的对象?

    web-services - 分布式服务如何比分布式对象更好?

    perl - 除了Catalyst之外,是否还有任何其他现代(Moose/PSGI)Web框架?

    perl - 您如何在 Perl 或任何其他编程语言中对 CJK(亚洲)字符进行排序?

    java - Java 数组是类实例吗?

    java - 如何从 SQLiteOpenHelper 访问 StartActivityForResult()?

    perl - 将 MX::Declare 方法定义为属性触发器

    perl - 将 MooseX::Method::Signatures 导入调用者的作用域