Каков наилучший способ сделать композицию объектов с помощью Moose? - PullRequest
4 голосов
/ 27 января 2012

Просто вопрос для начинающих о лучшей практике с 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 ]

6 голосов
/ 27 января 2012

Является ли это лучшим способом решения проблемы, связанной с составлением простого объекта?

Слишком субъективно, чтобы ответить, не зная, что вы собираетесь с ним делать, и проблемучрезмерно упрощенНо я могу сказать, что нет ничего плохого в том, что вы делаете.

Изменение, которое я сделаю, - это переместил работу, чтобы вычислить расстояние между двумя точками в точку.Тогда другие могут воспользоваться.

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

Теперь, когда установлены start или end, они очищают значение в length, вызывая его восстановление в следующий раз, когда оно будет установлено.call.

Это действительно вызывает проблему ... length изменится, если start и end будут изменены, но что, если объекты Point будут изменены непосредственно с $line->start->y(4)?Что если на ваш объект Point ссылается другой фрагмент кода, и они его изменяют?Ни один из них не приведет к пересчету длины.У вас есть два варианта.Первый - сделать length полностью динамическим, что может быть дорогостоящим.

Второй - объявить атрибуты Point доступными только для чтения.Вместо того, чтобы менять объект, вы создаете новый.Тогда его значения не могут быть изменены, и вы можете кешировать вычисления на их основе.Логика распространяется на Линии и Полигоны и т. Д.

Это также дает вам возможность использовать шаблон Flyweight.Если точка только для чтения, то для каждой координаты должен быть только один объект.Point->new становится фабрикой, либо создающей новый объект, либо возвращающей существующий.Это может сэкономить много памяти.Опять же, эта логика распространяется на Линии и Полигоны и т. Д.

Да, имеет смысл иметь length в качестве атрибута.Хотя он может быть получен из других данных, вы хотите кэшировать эти вычисления.Было бы неплохо, если бы у Moose был способ явно объявить, что length был просто получен из start и end и поэтому должен автоматически кэшировать и пересчитывать, но это не так.

Как я могу сделать что-то подобное?$line2->end(x => 3, y => 3);

Наименее хакерский способ сделать это - принуждение типа .Вы определяете подтип, который превратит ссылку на хэш в Point.Лучше определить его в Point, а не в Line, чтобы другие классы могли использовать его, когда используют Point.

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

Затем измените тип start и end на Point::OrHashRefи включите принуждение.

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

Теперь 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 ]);

Это должен быть хэш, а не хэш, потому что атрибуты 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;
0 голосов
/ 28 января 2012

Это гораздо меньше вопрос Moose, чем вопрос объектно-ориентированного проектирования. Но в этих терминах есть несколько интересных моментов, на которые стоит обратить внимание:

  1. Линии имеют семантику значений, что означает, что две линии с разными точками фактически являются разными линиями. Атрибуты точки чтения и записи не имеют смысла для линий. Это должны быть атрибуты только для чтения; если вам нужна Линия, содержащая другую Точку, то вам нужна другая Линия.
  2. Очки, аналогично.
  3. Для данной Линии ее длина постоянна и полностью выводится из атрибутов Point. Присвоение атрибуту длины линии усложняет дело: она позволяет построить невозможную линию и (в сочетании с атрибутами точки чтения-записи) открывает путь к ошибкам согласованности. Более естественно и менее подвержено ошибкам делать длину обычным методом.
  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 отлично подходят для общего случая, но их сжатие - хороший способ уменьшить общую сложность.

...