What is the best way to compose objects with Moose?

Just a beginner question on best practice with moose:

Starting with a simple β€œpoint” example, I want to build a β€œline” - an object consisting of two points and having a length attribute that describes the distance between the start and end points.

{ 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 above code works as expected. Now my questions are:

  • Is this the best way to solve a problem / make a simple composition of an object?

  • Is there any other way to create a string with something like this (the example does not work!) (BTW: What other methods do exist at all?):

<P β†’
 my $line2 = Line->new( start->x => 1, start->y => 1, end => Point->new( x => 2, y => 2 ) ); 
  • How can I initiate an automatic recalculation of length when changing coordinates? Or is there no point in having attributes such as lengths that are β€œeasily” derived from other attributes? Should these values ​​(length) be better provided as functions?
<P β†’
 $line->end->x(3); $line->end->y(3); $len = $line->length; 
  • How can I do something like this? What is the way to change the point right away - instead of changing each coordinate?
<P β†’
 $line2->end(x => 3, y =>3); 

Thanks for any answers!

+4
source share
2 answers

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', # Unlike builder, Moose creates _clear_length() clearer => '_clear_length', lazy => 1 ); 

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; # 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; 
+6
source

This is much less a Muse issue than an object-oriented design. But there are some interesting notes in these terms:

  • Lines have semantics of values, which means that two lines having different points are actually different Lines. The read and write point attributes do not make sense for strings. They should be read-only attributes; if you need a line to contain another point, you really need another Line.
  • The points are similar.
  • For a given string, its length is constant and fully inferred from its Point attributes. Creating a string length attribute complicates the situation: it allows you to build an impossible Line and (in combination with the attributes of the read and write points) opens the door for sequence errors. It is more natural and less error prone to make length the usual method.
  • Backing up the length method with an attribute is a performance optimization. Like all optimizations, the additional complication introduced in this way should be justified by profiling.

Back to topics related to Muse. Elk does not provide additional constructor forms. On the other hand, this does not stop you from providing your own constructor forms, thus:

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

Providing more convenient and -built constructors is a fairly common practice. Moose's wide open interfaces are great for the general case, but tightening them up is a good way to reduce overall complexity.

0
source

Source: https://habr.com/ru/post/1393334/


All Articles