Having in the previous article prepared data types for car suits and card ranks, I can now combine them to provide a playing card class, using Stevan Little's Moxie module (version 0.04, so definitely early days.) The goal is to provide an object-oriented paradigm to the Perl 5 programming language which is more sophisticated, more powerful and less verbose than manually bless()-ing hashes. To achieve that goal it needs to be faster and light-weight compared to Moose. Currently, Moxie.pm and and MOP.pm are add-on modules, but eventually, when they are more complete, when the wrinkles have been ironed out, and when they have gained acceptance and a community of users, they might be merged into the Perl core.
One significant feature of Moxie is that it reduces boilerplate code. You don't have to specify
At the moment there is no mechanism to declare or to verify the type of an attribute, but I suspect that is inevitable at some point. It is not possible, at least currently, to have an attribute share a name with its
The
The initalizer,
In this case, I provide a method to express the card as a string, and a method to print that representation. The
Two
Finally, a routine is provide to generate a deck of cards. It is convenient to provide it at this level, so that knowledge of and access to Suits and Ranks are limited to this one class.
One significant feature of Moxie is that it reduces boilerplate code. You don't have to specify
warnigns
or strict
. As well, the features
or the perl you are using are enabled, among them say
, state
, signatures
, and post_deref
.
A Simple Moxie Class
package Card {
use Moxie;
use Suits;
use Ranks;
extends 'Moxie::Object';
has _rank => sub { };
sub rank : ro(_rank);
has _suit => ( default => sub { });
sub suit : ro(_suit);
sub BUILDARGS : init_args( suit => _suit,
rank => _rank
);
# ...
}
A package provides the namespace for a class. The crucial steps are to use the Moxie module; to declare we are a subclass of Moxie::Object, and potentially also of a some class we have defined; and to declare attributes and accessors. An attribute must specify a subroutine to provide a default value, in case it has not been initialized. In the example above I demonstrate both styles of doing this.
At the moment there is no mechanism to declare or to verify the type of an attribute, but I suspect that is inevitable at some point. It is not possible, at least currently, to have an attribute share a name with its
accessor
. The solution is to mark the attribute with a leading underscore, the conventional indicator for a private attribute. In fact the result of the code is a conventional blessed hash object, so it is possible to access the attribute directly: $card->{_suit}
, but this is definitely not a best practice. If you're going to do that, why bother with Moxie in the first place?The
accessor
can be read-only
, write-only
, or read-write
. The underlying hash element remains readable and writable. Besides the accessor
, it is possible to define a predicate
, to detect whether the attribute has been assigned or not, and a clearer
, to reset an attribute to undefined
. As well, a method can be marked to delegate
its operation to the underlying attribute, invoking a method of the same name.The initalizer,
BUILDARGS
is shown with mandatory arguments. This is inevitable in this case since the attributes only have read-only
accessors. An uninitialized object would remain so forever. But in cases where an attribute can be initialized in the constructor or can accept the default value, a '?' can be appended to the pair tag, to indicate the tag may or may not be present in the initializer list or pairs.Completing the Class
my $FMT = "%2s%s";
sub format ( $self, $format = '' ) {
my $old_format = $FMT;
if ( $format eq ''
&& 0 == length ref $self )
{
$format = $self;
}
$FMT = $format
if $format;
return $old_format;
}
sub to_string ($self) {
return sprintf $FMT, $self->rank(), $self->suit();
}
sub print ($self) {
say $self->to_string();
}
sub compare_by_rank ( $self, $card ) {
return Ranks::compare( $self->rank(), $card->rank() ) # increasing rank
|| Suits::compare( $self->suit(), $card->suit() ) # increasing ruit
}
sub compare_by_suit ( $self, $card ) {
return Suits::compare( $self->suit(), $card->suit() ) # increasing ruit
|| Ranks::compare( $self->rank(), $card->rank() ) # increasing rank
}
sub gen_deck {
my @cards;
for my $suit ( Suits::suits() ) {
for my $rank ( Ranks::ranks() ) {
push @cards, Card->new( suit => $suit, rank => $rank );
}
}
return \@cards;
}
Moxie can't do all the work, you have to write some code yourself. But once you no longer have to deal with repetitive implementation details, it becomes easier to focus on the important aspects of your class. And smaller code is easier to understand.In this case, I provide a method to express the card as a string, and a method to print that representation. The
to_string()
method will be important in other classes, as the basis for outputing a single card out of a set of cards. I'm using a class attribute $FMT
, and I provide a setter/getter method, format()
. This can be invoked as an object method, $card->format()
, or as a class routine, Card::format()
. If an argument is provided, that value is assigned to $FMT
. In either case, the old value of the variable is returned to the user. While it might be possible to use a true attribute to specify the format for outputing a card, it would be irritating to have to asign each card of the deck with its format.Two
compare
routines combine the routines provided in the Suits and Ranks modules. A hand should be displayed sorted. For games like Fish or Canasta which focus on collecting cards of a rank, you want cards sorted by rank and then by suit. Games like bridge sort cards by suit and then by rank. The actual selection of the appropriate compare routine will take in another class representing a collection of cards.Finally, a routine is provide to generate a deck of cards. It is convenient to provide it at this level, so that knowledge of and access to Suits and Ranks are limited to this one class.
Comments