Just a beginners question on Best Practice with Moose:
Starting on the simple “point” example I want to build a “line” – object, consisting of two points and having a lenght attribute, describing the distance between starting and ending point.
{
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;
The code above works as expected.
Now my questions:
-
Is this the best way to solve the problem /to do simple object composition?
-
Is there another way to create the line with something like this (example does not work!) (BTW: Which other ways do exist at all?):
>
my $line2 = Line->new( start->x => 1, start->y => 1, end => Point->new( x => 2, y => 2 ) );
- How can I trigger an automatic recalculation of length when coordinates are changed? Or does it make no sense to have attributes like length which can “easily” derived from other attributes? Should those values (length) better be provided as functions?
>
$line->end->x(3);
$line->end->y(3);
$len = $line->length;
- How can I make something like this possible? What’s the way to change the point at once – instead of changing each coordinate?
>
$line2->end(x => 3, y =>3);
Thanks for any answers!
That’s too subjective to answer without knowing what you’re going to do with it, and the problem is overly simplistic. But I can say there’s nothing wrong with what you’re doing.
The change I’d make is to move the work to calculate the distance between two points into Point. Then others can take advantage.
First thing I’d note is you’re not saving much typing by foregoing the object… but like I said this is a simplistic example so let’s presume making the object is tedious. There’s a bunch of ways to get what you want, but one way is to write a BUILDARGS method which transforms the arguments. The example in the manual is kinda bizarre, here’s a more common use.
There is a second way to do it with type coercion, which in some cases makes more sense. See the answer to how to do
$line2->end(x => 3, y =>3)below.Oddly enough, with a trigger! A trigger on an attribute will be called when that attribute changes. As @Ether pointed out, you can add a clearer to
lengthwhich the trigger can then call to unsetlength. This does not violatelengthbeing read-only.Now whenever
startorendare set they will clear the value inlengthcausing it to be rebuilt the next time it’s called.This does bring up a problem…
lengthwill change ifstartandendare modified, but what if the Point objects are changed directly with$line->start->y(4)? What if your Point object is referenced by another piece of code and they change it? Neither of these will cause a length recalculation. You have two options. First is to makelengthentirely dynamic which might be costly.The second is to declare Point’s attributes to be read-only. Instead of changing the object, you create a new one. Then its values cannot be changed and you’re safe to cache calculations based on them. The logic extends out to Line and Polygon and so on.
This also gives you the opportunity to use the Flyweight pattern. If Point is read-only, then there only needs to be one object for each coordinate.
Point->newbecomes a factory either making a new object OR returning an existing one. This can save a lot of memory. Again, this logic extends out to Line and Polygon and so on.Yes it does make sense to have
lengthas an attribute. While it can be derived from other data, you want to cache that calculation. It would be nice if Moose had a way to explicitly declare thatlengthwas purely derived fromstartandendand thus should automatically cache and recalculate, but it doesn’t.The least hacky way to accomplish this would be with type coercion.
You define a subtype which will turn a hash ref into a Point. It’s
best to define it in Point, not Line, so that other classes can make
use of it when they use Points.
Then change the type of
startandendtoPoint::OrHashRefand turn on coercion.Now
start,endandnewwill accept hash refs and turn them silently into Point objects.It has to be a hash ref, not a hash, because Moose attributes only take scalars.
When do you use type coercion and when do you use
BUILDARGS? A goodrule of thumb is if the argument to new maps to an attribute, use type
coercion. Then
newand the attributes can act consistently and other classes can use the type to make their Point attributes act the same.Here it is, all together, with some tests.