Is this the best way to solve a problem to make a simple Composition object?
It is too subjective to answer, not knowing what you are going to do with it, and the problem is too simplified. But I can say that there is nothing wrong with what you do.
The change I will make is to move the work to calculate the distance between two points in a point. Then others can take advantage.
# How do I do something like this? my $line2 = Line->new( start->x => 1, start->y => 1, end => Point->new( x => 2, y => 2 ) );
The first thing I would notice is that you do not save a lot of typing data, the above about the object ... but, as I said, this is a simplified example, so let me assume that the object is tedious. There are tons of ways to get what you want, but one way is to write a BUILDARGS method that converts the arguments. The example in the manual is rather strange, here is more common use.
# 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; }
There is a second way to do this with a type of coercion, which in some cases makes more sense. See the answer on how to make $line2->end(x => 3, y =>3) below.
How can I cause automatic length recalculation when coordinates are changed?
Oddly enough, using a trigger! When this attribute is changed, the attribute trigger is called. As @Ether noted, you can add clearer to length , which can then trigger a trigger to cancel length . This does not violate read-only 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',
Now that start or end , they clear the value in length , causing it to rebuild on the next call.
This causes a problem ... length will change if start and end are changed, but what if Point objects are directly modified using $line->start->y(4) ? What if your Point object refers to another piece of code and they change it? None of them will recalculate the length. You have two options. First, make length fully dynamic, which can be costly.
Second, declare Point attributes read-only. Instead of changing the object, you create a new one. Then its values ββcannot be changed, and you can safely cache calculations based on them. The logic extends to Line and Polygon, etc.
It also gives you the option to use the Flyweight template. If Point is read-only, then for each coordinate there should be only one object. Point->new becomes a factory, either by creating a new object, or by returning an existing one. This can save a ton of memory. Again, this logic extends to Line and Polygon, etc.
Yes, it makes sense to have length as an attribute. Although it can be obtained from other data, you want to cache these calculations. It would be nice if Moose had a way to explicitly state that length was purely derived from start and end and therefore should automatically cache and recount, but that is not the case.
How can I do something like this? $line2->end(x => 3, y => 3);
The worst way to achieve this is through a type of coercion . You define a subtype that will turn the hash ref into a dot. It is best to define it in Point rather than in Line so that other classes can use it when they use points.
use Moose::Util::TypeConstraints; subtype 'Point::OrHashRef', as 'Point'; coerce 'Point::OrHashRef', from 'HashRef', via { Point->new( x => $_->{x}, y => $_->{y} ) };
Then change the start and end type to Point::OrHashRef and enable coercion.
has 'start' => ( isa => 'Point::OrHashRef', is => 'rw', required => 1, coerce => 1, );
Now start , end and new will take hash links and quietly turn them into Point objects.
$line = Line->new( start => { x => 1, y => 1 }, end => Point->new( x => 2, y => 2 ) ); $line->end({ x => 3, y => 3 ]);
It should be a hash referee, not a hash, because only the scalars accept the attributes of the Laws.
When do you use type coercion and when do you use BUILDARGS ? A good rule of thumb is if the argument of the new cards is an attribute, use the coercion type. Then new , and attributes can act sequentially, and other classes can use this type to make their Point attributes act the same.
Here it is, all together, with some tests.
{ 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;