Perl Best Practices
15.11. Lvalue Accessors
Don't use lvalue accessors. Since Perl 5.6, it has been possible to specify a subroutine that returns a scalar result as an lvalue, which can then be assigned to. So another popular approach to implementing attribute accessor methods has arisen: using lvalue subroutines, as in Example 15-8. Example 15-8. Another way to implement accessor methods
# Provide access to the name attribute... sub name :lvalue { my ($self) = @_; return $name_of{ident $self}; } sub rank :lvalue { my ($self) = @_; return $rank_of{ident $self}; } # Serial numbers are read-only, so not lvalue... sub serial_num { my ($self) = @_; return $serial_num_of{ident $self}; }
The resulting code is certainly much more concise. And, perhaps surprisingly, the return to a single accessor per attribute doesn't reinstate the problems of uncertain intention leading to invisible errors, because the accessors would now be used differently, with a clear syntactic distinction between storing and retrieving: # Create the new military record... my $dogtag = Dogtag->new( {serial_num => 'AGC10178B'} ); # Store attribute values... $dogtag->name = {surname=>'MacArthur', first_name=>'Dee'}; $dogtag->rank = 'General' ; # Retrieve attribute values... print 'Your new commander is: ', $dogtag->rank(), $SPACE, $dogtag->name( )->{surname}, "\n"; print 'Her serial number is: ', $dogtag->serial_num( ), "\n";
And, now, if overgeneralization again leads to a misguided attempt to update the serial number: $dogtag->serial_num( ) = $division_code . $old_serial_num;
the compiler will again detect and report the problem: Can't modify non-lvalue subroutine call at rollcall.pl line 99
This certainly looks like a viable alternative to separate getting and storing. It requires less code and handles the psychology just as well. Unfortunately, lvalue methods are less reliable and less maintainable. They're unreliable because they remove all your carefully crafted encapsulation from around the object, by granting direct and unrestricted access to its attributes. That is, a call such as $obj->name( ) is now identical to a direct access like $name_of{$obj}. So you can no longer guarantee that your Dogtag objects store their name information under the correct keys, or even in a hash at all. For example, the set_name( ) method in Example 15-7 ensures that both names are passed and then stored in a hash in the appropriate attribute entry, so a misuse like this: $dogtag->set_name('Dee MacArthur');
throws an immediate exception: Usage: $obj->set_name($new_surname, $new_first_name) at 'promote.pl' line 33 But using the equivalent lvalue name( ) accessor from Example 15-8 doesn't do any data validation; it just returns the attribute storage, with which client code can then have its wicked way: $dogtag->name = 'Dee MacArthur';
That string is assigned directly to the internal $name_of{ident $dogtag} attribute, which is supposed to store only a hash reference. So any other methods that rely on $name_of{ident $self} being a hash reference: # much later... $dogtag->log_orders($orders);
are going to produce unexpected and hard-to-debug errors, because the object's internal state is no longer as expected: Can't use string ("Dee MacArthur") as a HASH ref while "strict refs" in use at 'promote.pl' line 702
Lvalue accessors also make it very much harder to extend or improve your class. Get/set accessors retain control over how attributes are accessed, so if you need to add some sanity checking when military ranks are updated, that's relatively easy to accommodate. For example, you might create a look-up table of known military ranks and a utility subroutine to verify that its argument is a known rank (or die trying):
# Create look-up table of known ranks... Readonly my @KNOWN_RANKS => ( # Enlisted... Commissioned... 'Private', 'Lieutenant', 'PFC', 'Captain', 'Corporal', 'Colonel', 'Sergeant', 'General', # etc. etc. ); Readonly my %IS_KNOWN_RANK => map { $_ => 1 } @KNOWN_RANKS; # Utility subroutine to vet new "rank" values.... sub _check_rank { my ($rank) = @_; return $rank if $IS_KNOWN_RANK{$rank}; croak "Can't set unknown rank ('$rank')"; } It would then be trivial to modify the set_rank( ) accessor from Example 15-7 to apply that check every time a dogtag's rank attribute is updated: sub set_rank { my ($self, $new_rank) = @_; # New rank now checked first... $rank_of{ident $self} = _check_rank($new_rank); return; }
On the other hand, there's no way to add this same check to the lvalue rank( ) accessor from Example 15-8, except by resorting to a tied variable (which is not an acceptable solutionsee Chapter 19). |