Является ли это лучшим способом решения проблемы, связанной с составлением простого объекта?
Слишком субъективно, чтобы ответить, не зная, что вы собираетесь с ним делать, и проблемучрезмерно упрощенНо я могу сказать, что нет ничего плохого в том, что вы делаете.
Изменение, которое я сделаю, - это переместил работу, чтобы вычислить расстояние между двумя точками в точку.Тогда другие могут воспользоваться.
# 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;