![]() ![]() |
![]() ![]() |
![]() ![]() | |
© 1997 The McGraw-Hill Companies, Inc. All rights reserved. Any use of this Beta Book is subject to the rules stated in the Terms of Use. |
The last chapter introduced quite a bit of the syntax of objects. These included the definition of Perl object variables and methods, the syntax of inheritance and overloading, building objects out of ordinary references, and the difference between a class and an object.
Just having the syntax isn't enough when dealing with object oriented programming. You need to be patient, and to experiment. You also need to look at lots of examples, and when possible, get your feet wet in programming your own. It is quite easy to get frustrated, so be prepared for this. However, there is a bright side to all of the this. There are a lot of similarities that you will run across when you program objects; certain functions and variables are used over and over and they do the same basic thing in every single object.
We have dubbed this the common object. The purpose of this chapter is to give a good feel of how objects might look when you get into the full swing of things, without actually worrying about making anything useful.
The focus of this chapter is the creation of a 'mock class': one that is complicated enough to show how an actual class might work, but not such that we have to worry about real issues. The example that we have chosen to illustrate objects is a board game.
First we will give an overview of some more important class terms, as well as reviewing some of the important terms from the chapter before. Then, we will consider one of the foundation classes that will be the start of this game, the Piece, which we will implement as if we were actually seriously considering making the full game.
We will then look at the following five types of element in the class Piece:
1) Common Class Data
2) Common Object Data
3) Common Object Methods (ones in pretty much every class)
4) Usually Common Object Methods (ones in most classes)
5) Common Private Object Methods
Finally, we will give some code snippets on how to use these methods to manipulate the Piece object, and some ways of combining the low level methods that you will find in pretty much any object into higher level ones. But first, an overview of what we are to cover.
As stated in the last chapter, any object that you are going to see is going to consist of the following five items:
1) a constructor or constructors - one or more functions that construct an object, give it form and return a reference to that object.
2) a destructor - the function that is called for the destruction of the module. Since memory management is done by Perl itself, you only need to destroy things that are used by the object (temporary files, etc.) This is optional.
3) object data - the data that the object holds. Usually in a hashkey, as in $self->{data}.
4) object methods - the functions that are associated with the object.
5) object algorithms - the algorithms associated with the object; how the object is implemented.
When we talk about the common object, we are talking about methods and data. These methods and data are so common that they come up over and over again in the course of writing an object. Once you start writing lots of objects, these common methods are so ingrained in your psyche that you will say to yourself: 'oh - that needs a get method' or 'oh - that needs a store method'. You will then add the appropriate method to your object in order to solve that particular problem.
Sound fanciful? Well, let's start with a fairly abstract problem. Let's consider making a board game, and consider the first object that we would have to make in that game, a Piece.
But first, let's go over more definitions that we are going to use in the examples below.
These definitions will figure prominently in the next examples. We have gone over some of them briefly in the last chapter but consider this a review.
Class Methods are methods that are shared between objects. For example, there might be a Class method named reset which goes through all of the objects that have been initialized by it and resets them back to some, initial state.
Class Data is data which is shared between objects. Each object inside the class has access to that data. This is used to make common configurations to share between all objects.
Accessor Methods are a special type of method which is used to access the class or object's data.
Mutator Methods are used to set a class or object's data.
A Private Method is one that is supposed to be internal to the object or class and is not meant to be called from the outside.
Likewise, Private Data is data that is meant for consumption of the object only.
Perl's policy towards Class Data and Class Methods is pretty much consistent through all of the object oriented world. However, Perl has a very unique way of handling Private Methods and Data: there is no syntax for private methods or data.
In other words, methods and data are private, not by enforcement, but by convention. In C++ you could say:
class A
{
private:
int xx;
int yy;
A();
int myfunction();
public:
int method();
}
in which myfunction() can only be called by objects in class A, and nowhere else. But the equivalent class header in Perl would be defined thus:
package A;
sub new
{
my ($type) = @_;
# rest of object
}
sub method { }
sub _myfunction { }
Notice the '_' in front of _myfunction(). This indicates that _myfunction() should not be called from the outside world. However, Perl isn't going to give you a compile time error if you do in fact call _myfunction().
It is an unwritten contract between the person who writes the clients and the person who writes the modules to agree not to tread on each other's toes. The unwritten contract we have on my current project is that '_' means "Private! Hands off."
Why this non-strictness? Well, the non-strictness lets you break 'good programming' rules (like respecting other people's wishes for a variable to be private) on a temporary basis to get an instant productivity gain. It is usually a bad idea to break these rules, since by doing so you create silent traps in the code and it will break as soon as the private interface changes, but can be worthwhile, if you know what you are doing.
With that review over, here we go on to do some real design. We start with the example overview, and then go on to do a partial implementation of it.
For our example, we will consider a mock-game called Strategio. It is almost exactly like the Milton Bradley game Stratego, except that we will be using the flexibility that the computer provides us to make the rules more complicated, have the pieces change in the middle of the game, and so on.
Furthermore, Strategio is used in this example since it is a relatively simple game. It has a square board and pieces can be defined by generic attributes (such as 'attacking_rank', 'movement', 'defending_rank', and 'position'). For the sake of this example, we can create the Strategio pieces in the abstract, showing how to use the methods that we create, and how we might use them in the game program itself.
Stratego is a Milton Bradley board game that pits two players against each other in an attempt to capture the opponent's flag. Each player maneuvers pieces around the board either moving or attacking on alternating turns. The board itself consists of a 10x10 grid similar to a chess board. Game pieces each have a rank and when an attack occurs, the piece with the highest rank wins. During play each player alternates either moving or attacking using one of the 40 game pieces each initially gets. The pieces consist of Marshall, General, Colonels, Majors, Captains, Lieutenants, Sergeants, Miners, Scouts, Spy, Bombs, and a Flag.
When pieces of equal rank attack, both lose. A losing piece (or pieces) is permanently removed from the board. A player may either move or attack on their turn, but not both. Any piece except bombs or the flag can move. A move can be made to any adjoining empty square, forward, backward, or sideways, but not diagonally. All pieces except miners can move only one square. Miners can move any number of empty squares in a single direction. An attack can (but is not required) between any two adjoining opposing pieces. The spy is the lowest ranked piece, but will win when attacking a marshal. Bombs destroy any piece which attacks it except miners. Miners which attack bombs cause the bombs to be removed from the board.
The pieces are constructed such that only the piece's owner can see the name and rank of each piece when placed on the board. Each player initially places all of their pieces on their own side of the board, again similar to chess. However, the players can place any of their pieces in any location, unlike the constraints put on chess pieces. Pieces can move one (and only one) square either forward, backwards, or sideways, but cannot move diagonally. The only exceptions are Scouts, Bombs, and the Flag. Scouts can move any number of "open" squares in a single direction. Bombs and the Flag, cannot move at all once placed on the board at the beginning of the game.
Pieces are positioned in such a way that the player can only see his or her pieces; the opponent's is sheltered from view.
These are the rules, then, for the Pieces of Stratego. Notice that there are a few constraints here:
1) pieces that are captured are removed from the board permanently
2) pieces remain constant throughout the game.
In our 'mock' Strategio game we will remove these constraints. Pieces will be able to be promoted, to go up a rank if they capture so many enemy pieces, demoted if they retreat from battle. Recruits are another possibility, in which a side that is losing can gain more pieces and build more.
Keep these rules in mind while we are making the design. Understanding these rules of play is necessary in designing the object Piece.
What is the best way to go about implementing objects for the game? Remember, there are three major parts to designing an object:
1) visualizing the data behind the object
2) visualizing the methods behind the object
3) visualizing the interface behind the method.
Lets look at each of these in turn.
I like to start a design by thinking about the object's data. It tends to be a simple part of the design (although not the simplest) and it also tends to help me picture the object in my mind.
I start thinking of the attributes for the piece, and how they might be stored. I can think of several off the top of my head:
the type of piece it is
how much the piece moves
the piece side (blue or red)
the rank of the piece
the coordinates of the pieces
Furthermore, we have a couple of wrinkles in the 'problem domain', as computer programmers like to call it. You may have notices in the design specifications that there were a few special rules on how pieces attacked:
1) Bombs, when attacked, always kill their opponent unless it is a miner.
2) Spies always lose when they are attacked. Spies who attack always lose unless their opponent is an admiral.
What is the best way to handle these exceptions? In a way, it would be really nice if we could say something like:
$piece = { 'type' => 'bomb',
'movement' => 1,
'side' => 'blue',
'rank' => -1,
'xcoord' => 3,
'ycoord' => 5 }
in which each element in the hash corresponds to the list we made above. Attacking would then become a function of comparing the ranks between two pieces. If they are on opposing sides, the one with the lowest rank wins. Unfortunately, a bomb is always of rank -1, always invincible. So we need to somehow make the rank conditional on the piece that is attacking.
In other words, the piece's rank depends on something else that it has no idea about; another piece's rank. Object oriented programming has a specific technique for solving this, one that we have seen before. To get that other piece's rank, we rely on a callback. We do something like this:
$piece = { 'type' => 'bomb',
'movement' => 1,
'side' => 'blue',
'rank' => sub {
$rank= -1 unless $_[0]->type() eq 'miner';
$rank = 99999 if $_[0]->type() eq 'miner';
return($rank);
},
'xcoord' => 3,
'ycoord' => 5
}
The bomb's rank depends on the bolded subroutine. This way, we separate what we cannot know from the object itself. If we then said something like:
my $callback = $piece->{'rank'};
&$callback($someotherpiece);
in which $someotherpiece was another piece of opposing rank, this translates into:
$rank = -1 unless $someotherpiece->type() eq 'miner';
$rank = 99999 if $someotherpiece->type() eq 'miner';
return($rank);
which is exactly what we need. This is a way of representing data based on a condition that we don't know. After all, one time we call this, rank may turn out to be -1. Other times, 99999. The point is, we don't know.
Will this handle the other condition that the Piece object has to handle?
2) Spies always lose when they are attacked. Spies who attack always lose unless their opponent is an admiral.
The way it stands, I guess we could accommodate this. The key point though, is that spies have a different rank whether they are attacking or defending. Instead of trying to stuff this second point into the same callback and make it more complicated, split up the rank into two types of ranks, a defending rank and an attacking rank:
$spy_piece =
{
'type' => 'spy',
'movement' => 1,
'side' => 'blue',
'attacking_rank' =>
sub {
my $attacking_rank;
$attacking_rank = 99999 unless($_[1]->type() eq 'admiral');
$attacking_rank = -1 if ($_[1]->type() eq 'admiral');
},
'xcoord' => 3,
'ycoord' => 5
}
When the spy attacks anything but the admiral, its rank resolves to:
$attacking_rank = 99999 unless($_[1]->type() eq 'admiral');
meaning, of course that the spy's rank is the lowest of the low unless the piece it is attacking is an admiral. If the piece is an admiral, then the row:
$attacking_rank = -1 if ($_[1]->type() eq 'admiral');
gets evaluated instead and the spy is then all powerful and the admiral is captured.
The main point to get out of this is to start simple with the data, and then think of worst case scenarios to flesh in the details. We started with a simple hash as the object, and then looked at two special cases in our 'problem domain' to see whether they would be problems. They were, which forced us to split up the rank into two pieces, which we then supplemented with callbacks to provide the functionality we needed.
Special cases, or exceptions to the rule, will always be problematic. That is the reason behind the old 80/20 rule. Eighty percent of the work will take twenty percent of the time. These are the easy cases. On the other hand, twenty percent of the work will take eighty percent of the time. These are the special cases, like the ones we saw above.
Now that we have the data designed, we have to deal with the methods which manipulate that data. The secret to successful method design is to think of simple methods first, and then combine these methods to make more complicated things happen.
For example, we have probably isolated three methods: attack(), move(), and promote(). But we shouldn't need to directly implement these methods. Just as bronze is made of copper and tin, our move function is going to be composed of set() and get().
attack() and move()are composite methods. set() and get() are our mutator() and accessor() methods. They are simple.
So let's think of the methods that we can have. The bulk of this chapter will be implementing the simple methods, and we will consider implementing the complex ones at the end:
new() - the function that actually creates the object
configure() - the function that sets low-level data in the subroutine
set() - the function that sets high-level data and data that is commonly changing in the subroutine
get() - the function that gets data out of the hash
These are the low, low level functions. We then can define some more functions, such as common things to do with our pieces:
store() - store pieces to disk
retrieve() - retrieve pieces from disk
debug() - print out a helpful trail to debug
copy() - copy a given piece to another piece.
sprintf() - 'pretty print' our piece, perhaps in something like a piece report
grep() - look for an attribute for a given piece
These are the actions to take in an application. We thus make a small code skeleton and put them all in so we can see how much work we have cut out for ourselves:
package Piece;
sub new { }
sub configure { }
sub set { }
sub get { }
sub store { }
sub retrieve { }
sub debug { }
sub copy { }
sub sprintf { }
sub grep { }
We are now ready to consider the basic interface issues that we are going to face and then start coding.
The best design for methods is to keep things simple at first. Try not to rush and think of the actual things that you are going to do at the end, but instead of the building blocks that you are going to use to get there. You may want to consider only the simple, necessary basics first. As we shall see, new, configure, set, and get will get us a long way.
Now that we have our code skeleton, it is the time to think of how to fill it. To do that, we think of exactly how we want to end up using this module. What is the easiest way of doing this?
First, consider the constructor, the function that is going to be creating our object. We somehow have to make a viable piece in one function statement, yet it would be a real drag to have to say:
my $piece = new Piece(
{ 'type' => 'bomb', 'attacking_rank' => 'NA',
'movement' => '0', 'piece_side' => 'blue'
'defending_rank' =>
sub {
$defending_rank= -1 unless $_[0]->type()eq 'miner';
$defending_rank = 99999 if $_[0]->type() eq 'miner';
return($defending_rank);
'xcoord' => 3,
'ycoord' => 5
}
);
in which we define the whole object then and there, on the command line! After all, bombs are always (at least starting out) going to have a movement of zero, a type of 'bomb', and a special attacking and defending rank.
These details should be handled by the object not the constructor. Therefore, we simplify the constructor to only including the things that always change, that make sense to be variable:
my $piece = new Piece('bomb', 'blue', 3,5);
defines a bomb that is blue, on the square 3,5. Thus the constructor is simplified so the client does not have to worry about it. As far as implementing this constructor, we don't worry how to do it, for now. We simply decide what we would like to use, and then evaluate whether it is easy or not to program.
For configure and set, the easiest way to implement is with something like this:
$piece->configure({'attribute1' => 'this', 'attribute2' => 'that'});
$piece->set({'attribute1' => 'this', 'attribute2' => 'that'});
configure and set, again, are going to be used to change the object with something like a 'key' => 'value' pair relationship. The above would set 'attribute1' to 'this', and 'attribute2' to 'that'.
By putting all of the usage into one hash, rather than saying something like:
$piece->configure('attribute1' => 'this', 'attribute2' => 'that');
we free up the other arguments for other uses later on.
Remember, we think of usage right now, and then think of how to program it! Most of the time, you can program pretty much any interface you want with Perl; sometimes the interfaces are easy, and sometimes difficult.
You need to have an idea of how the interface to your class will look; after that you can start implementing. Usually I implement top down. I think of how a given object will work from an abstract level, and then work down to actually implement it. In this case, the main goal we have in the interface is to hide all of the details of the Piece object from the client, or user of the module. Once we have that down, the interface of the rest of the module should pretty much fall into place.
So now, lets go ahead and implement each of the public functions above, going in the order they appear in the module. We will start first with something that is not actually in the class, but necessary to make the simple interface that we want: class data.
The first thing in the object will be a $_config variable. It is hash reference that the contains sensible defaults for the user which can be used to take some of the drudgery out of typing a lot of information. Remember, we want to avoid the horror of:
my $piece = new Piece(3,5,
{ 'type' => 'bomb', 'attacking_rank' => 'NA',
'movement' => '0', 'piece_side' => 'blue'
'defending_rank' =>
sub {
$defending_rank= -1 unless $_[0]->type()eq 'miner';
$defending_rank = 99999 if $_[0]->type() eq 'miner';
return($defending_rank);
}
);
which creates a piece on the coordinates 3 x 5 of type bomb, with a strength of 'special', and a movement of 'zero'. The special abilities are a code reference (callback) which makes bombs have the ultimate rank (-1, lower being better) unless the attacking piece is a miner (in which case the rank is 99999, and can be disarmed).
What a great waste of typing! It is error prone and way too tiresome for me. Since we are probably going to be defining more than one bomb, if we force the clients of our module to type all this, we will end up with lots of bugs.
In this example, we are only going to have one type of bomb. "Bomb-like" behavior is internalized into the class itself. We can say:
Listing 17.1 Piece.pm (header and default config)
1 package Piece;
2
3 use strict;
4 use Carp;
5 use Data::Dumper;
6 use FileHandle;
7
8 my $_config =
9 {
10 'bomb' => {
11 'attacking_rank' => 'NA',
12 'movement' => 0,
13 'defending_rank' =>
14 sub {
15 my $defending_rank;
16 $defending_rank=-1 unless ($_[0]->type() eq 'miner');
17 $defending_rank =99999 if ($_[0]->type() eq 'miner');
18 return($defending_rank);
19 }
20
21 },
22 'admiral' => {
23 'attacking_rank' => 1,
24 'movement' => 1,
25 'defending_rank' => 1
26 },
27
28 'scout' => {
29 'attacking_rank' => 9,
30 'movement' => 100,
31 'defending_rank' => 9
32 },
33 'spy' => {
34 'attacking_rank' =>
35 sub {
36 my $attacking_rank;
37 $attacking_rank = 99999 unless($_[1]->type() eq 'admiral');
38 $attacking_rank = -1 if ($_[1]->type() eq 'admiral');
39 }
40 }
41 };
42 my $_pieceNo = 1;
43
This makes the class more complex, but as a result we get the interface that we want, and we can say:
my $piece = new Piece('bomb', 'red',3,5);
to define a bomb. By adding a configure hash at the end to our original design, we can customize our pieces at the command line. In the unlikely event that we need another type of bomb, as in a bomb that could move, the code could be modified so that a call could be:
my $piece = new Piece('bomb','red' ,3,5, { movement => 1 });
in which the hash at the end of the constructor supplements the default at top.
Note that we are doing something a bit tricky here. We are defining some of our attributes, defending_rank and attacking_rank, as a code reference, or callback (for the spy and the bomb, lines 14 through 19, lines 35 through 39). This is because we can't put a regular value here. The value in some cases depends on the type of the piece that is attacking. This is why we need to make the value dependent. When anything attacks a bomb except a miner, the bomb is invincible, killing anything. (-1 rank). When a miner attacks a bomb he disarms it ( the bomb gets a defending rank of 999999 ).
We thus leave the rank to be figured out at the time which it attacks a piece or defends against attack. This will become important when we write an attack function.
We use the $_config attribute to fill in the object data below.
The $_config defined in the class header is usually used to set object data in the objects themselves. In the object there usually is a statement*:
$self->{config} = $_config;
Except of course, in the case where there is no such statement. |
which copies $_config (or a portion of it) into the object itself. This is usually then modified, somehow, by the caller of the object, such as:
%{$self->{config}} = (%{self->{config}}, $config);
in which $config is a variable which comes from the user of the module. This will be more clearly seen when we talk about the constructor, new(), and the private functions which are used to validate the configuration data (_validate(), _fill()) discussed below in 'Common Object Methods'.
All we have to do now is fill out the meat of the object, namely the methods associated with it. Let's start with the function that is going to build our object in the first place, the constructor new, and continue downward.
Based on the data above, the constructor becomes:
Listing 17.2 Piece.pm (constructor)
44 sub new
45 {
46 my ($type, $piece_type, $piece_side, $xcoord, $ycoord, $config) = @_;
47
48
49 (print(STDERR "Incorrect Args!\n", Carp::longmess()), return(undef))
50 if (@_!= 5 && @_ != 6);
51 $config = $config || {};
52 %$config = (%$config,
53 piece_side => $piece_side,
54 piece_no = $_pieceNo
55 );
56 my $self = bless {}, $type;
57 $self->_fill
58 (
59 {
60 'type' => $piece_type,
61 'xcoord' => $xcoord,
62 'ycoord' => $ycoord,
63 },
64 $config
65 );
66
67 $self->_validate();
68 $self->_recordDebug('after') if ($self->{'debug'} eq 'on');
69 # retrofitted for function debug() see debug method below.
70 return($self);
71 }
72
The idea behind this subroutine is one of validation. First, the constructor checks that the user has given the correct arguments (lines 49 and 50) and we make a blank $config hash in the case that the user has not defined it. It is standardized so that has a default value if the user has not defined it (line 51).
The object then calls a couple of private functions that will be exceedingly important: _fill() and _validate(). These functions are important enough that they deserve a big side note:
The constructor that we listed is short, mainly because of the two functions that we have defined: _fill() and _validate(). Why make such a big fuss over these functions? Because they will make our life quite a bit easier. Many of the bugs in object oriented programs are due to either: 1) creating an incomplete object 2) creating an object with incorrect data. Take for example an object that accepts a dollar amount as a piece of data, and the code that said something like:
or that there was code which used a filehandle:
and you had forgotten to actually make the filehandle, or make the dollar amount or the filehandle did not open correctly. Of course the code would blow up. The _fill() and _validate() functions make sure the object is a has all of the information it needs, validates the fact that the user of the object opened everything up correctly, and didn't, say, define the wrong attribute. If, for example, there was no xcoordinate or ycoordinate, the code would surely blow up somewhere. This is a conscious design choice. Some programmers do without functions to watch their backs, and instead sprinkle validation into the code itself. I prefer to do validation through functions because it makes the rest of the code clean, and as you will see, we will reuse _fill() and _validate(). For now, we define these hazily as the functions that will actually assign the object's values for us, and then validate that we have a good object. And, in the case of _fill(), we define how the function is going to be called: 1) argument 1 will consist of all the rapidly changing attributes, such as the piece's coordinates, or the attributes that have effect over other attributes, like the type of the piece (which determines how it moves, attacks, etc.). We will use this argument for our set() function below 2) argument 2 will consist of all the fixed, or slowly changing attributes like the color of the piece, and how the piece attacks. These will be filled in by the $_config hash reference we defined above. We will use this argument for our configure() method below. However, we will actually implement them last, because _fill() and _validate() get complicated, and we don't want to have to re-implement them because we oversaw something that they needed to do. |
Once we have created the object, we may want to change it. The first step is to define a configure method, whose job is to change the configuration hash inside the object to be something different. In this case, if we wanted to make a spy, we could say:
my $spy = new Piece('spy', 3, 5);
which would, again, create a spy on square 3, 5. But then, if we decided that we wanted this spy to assassinate miners instead of admirals, we could say, afterwards:
$spy->configure( { 'attacking_rank' =>
sub {
$attacking_rank = 999999 unless($_->type() eq 'miner');
$attacking_rank = -1 if ($_->type() eq 'miner');
$attacking_rank;
}
)
);
which then goes in and changes the attacking rank of the spy to take out miners instead of admirals. If we wanted to, say, make the spy invincible in attacking everybody, we could:
$spy->configure( { 'attacking_rank' => -10 } );
which would make the spy even be able to assasinate bombs! Here is the configure function in Listing 17.3:
Listing 17.3 - configure
73
74
75 sub configure
76 {
77 my ($self, $config) = @_;
78
79 $self->_recordDebug('before') if ($self->{'debug'} eq 'on');
80 # retrofitted for function debug() see debug method below.
81
82 $self->_fill( {}, $config);
83 $self->_validate();
84
85 $self->_recordDebug('after') if ($self->{'debug'} eq 'on');
86 }
Now, in this example, the configure method won't be used very frequently, since usually pieces have set attributes that fall into 'types' like bombs or lieutenants, and so forth. We just include it for completeness. The next function, set(), will be used more.
set() is the opposite of configure(). set() is used to change the more common of the attributes, or to change the 'higher' attributes (like what type of piece it is). For example:
my $obj = new Piece('admiral', 2,1);
$obj->set({xcoord => ($obj->get('xcoord') - 1)} );
is an awkward way of making and moving an admiral one step to the left, and:
my $obj = new Piece('admiral', 2, 1);
$obj->set( {'type' => 'spy'} );
turns a newly created admiral into a spy, giving the admiral all of the spy's attributes. Here is the set() method:
Listing 17.4 - set()
87 sub set
88 {
89 my ($self, $config) = @_;
90
91 $self->_recordDebug('before') if ($self->{'debug'} eq 'on');
92 # retrofitted for function debug() see debug method below.
93
94 $self->_fill( $config, {});
95 $self->_validate();
96
97 $self->_recordDebug('after') if ($self->{'debug'} eq 'on');
98 # retrofitted for function debug() see debug method below.
99 }
Again, set is merely a wrapper around _fill() and _validate(). What the user passes in ($config) is directly passed to _fill() and then the results are validated.*
Some people dislike having two Mutator methods which alter different parts of the object; I like it because you could inadvertently configure the wrong 'type' of thing. For example, if there was a set method that could do anything, you could inadvertently say:
and not know that attack_rank is something that you are not supposed to set very often. Other people like to have a different Mutator method for each attribute, something like:
but that seems to be the opposite extreme. |
These two Mutator methods will be the only ones that our object has. We must now define the opposite, the accessor methods which actually retrieve data for us. We shall have one named get().
(e) get()
Accessor methods are extremely important for the object that you are going to write. Since objects in Perl are simply references, the strong tendency one will have is to say something like:
my $obj = new Piece('spy',1,2);
print $obj->{'type'};
to directly print out the type of the object. Don't do this! If you want to stay sane, always say:
my $obj = new Piece('spy',1,2);
print $obj->get('type');
This 'extra level of indirection' (given by $obj->get('type')) separates the object from the client, so that the object's programmer is free to change how the object works internally.
Anyway, the main accessor method for our object will be get().
Unlike the design of mutator functions, I like to have one main accessor method that can get everything, and several, small accessor methods that are specialized to get important things. We have already seen one: $obj->type() gets the type of object $obj. We used it in making a configuration file.
So get() is, as one might expect, is the opposite of set(). But it is more generic than set, being able to access everything:
Listing 17.5 - get()
100 sub get
101 {
102 my ($self, $element) = @_;
103 my $config = $self->{config};
104 if (defined ($self->{$element}))
105 {
106 return($self->{$element});
107 }
108 elsif (defined ($config->{$element}))
109 {
110 return($config->{$element});
111 }
112 }
113
Now, even if our object changes and we add more elements either to config or the hash itself, this function will still work. It is resistant to change. If you can program in general terms like this, avoiding making references to specific elements inside the object, by all means do so. The best bugs are the ones that you prevent by careful programming.
These are the four most common methods that you will see in objects:
new(): the constructor which actually creates the object.
configure(): a function to configure the more 'hidden' attributes of the object, ones that are usually left alone.
set(): a function to configure the more 'changeable' parts of the object, ones that often change.
get(): a function to get any attribute that the object has (the ones you want the client to see).
These four methods also made great use of two different pieces of data:
$_config: a piece of Class data that sat up at the top of the module, defining the most common way that pieces could be set up;
$self->{config}: a piece of object data that was used to hold the base attributes of the object, ones that usually weren't for outside consumption.
Now, if that was all there was to programming objects, life would get boring pretty fast. There are an infinite number of methods out there (it's your task to find them!) but let's take it to the next level and look at some other methods that are common.
You will find the above four methods in pretty much any object; they follow the basic principle of most, if not all, object design: think simple.
In other words, you can accomplish a lot even given only the simplest methods: the set(), get(), configure(), and new() methods. Even though these methods represent simple concepts, they are extremely adaptable to many diverse situations.
The simpler the method, the more likely that you will find it in many objects. The more complex the more specialized.
On that note, here are six more common - but not as common - methods that you will find in lots of objects, and they can be used to make your life extremely easy in the quest for a good interface.
They are: copy(), store(), retrieve(), debug(), sprintf() and grep(). Once you feel comfortable with the simple methods, try adding these to your classes.
copy() is used to make an actual, physical copy of an object. We have already seen something like this when we overloaded the equals operator (chapter 16), saying:
use overload '=' => \©
This code aliased '=' to the function copy(), so if you said:
my $obj = new Vector();
my $obj2 = new Vector();
$obj2 = $obj;
this would copy $obj2 into $obj.
Now, if you are going to do any copying of objects, you are going to want to create a copy() method. Why? Remember that Perl always makes a reference to an object, and keeps the actual object in memory only to be accessed via that reference. If you say something such as:
my $obj = new Vector();
my $obj2 = $obj;
where you haven't overloaded '=', then you've made a copy of the reference, not the data structure itself. And if you say:
$obj2->set(1,2,3);
to set $obj2 to the coordinate '1,2,3', then guess what, you've set $obj as well!
So here's a quick copy method, that works on almost 95% of objects. In fact, it will work on any class that is made simply out of data, and also code references. If you are doing fancy things like manipulating files you can't do this, but if you are dealing with simple data, like we are with our Piece class, this works just fine:
Listing 17.6 - copy()
114
115 sub copy
116 {
117 my ($self) = @_;
118 my $text = Dumper($self);
119 my $VAR1;
120 eval ("$text");
121 $VAR1->_fill();
122 $VAR1->_validate();
123 return($VAR1);
124 }
Think a moment about what this is doing. When you say 'my $text = Dumper($self);' in line 118, you make a string that looks something like:
$VAR1 = bless ( {
ycoord => 1,
piece_no => 1
# ....
}, 'Piece' );
and store it into the variable $text. Then, in line #120, you eval it. This actually creates a valid object of type Piece. Since you are doing a direct assignment, $VAR1 is an exact duplicate of $self. Well, almost exact. Since _fill() is purely vaporware anyway, we sweep another couple of issues into our _fill() function, which is probably by now getting a little complicated. This was as anticipated, so this is OK (I guess)...
Another common thing you are going to want to do in Perl object oriented programming is to store objects to memory. This is called making the object persistent. This means that the object stays around after the program itself is gone.
In our case, we can get the same, 95% solution by, again, using the Data::Dumper module, and then sweeping the issues about restoring the module under _fill() and _validate().
Listing 17.7 - store()
125
126 sub store
127 {
128 my ($self, $filename) = @_;
129
130 my $fh = new FileHandle("> $filename")
131 || (print(STDERR "Couldn't open $filename for writing!\n"), return());
132
133 print $fh Dumper($self);
134 close($fh);
135 }
136
This works by using the Data::Dumper trick, but this time we store everything to a provided file. When we retrieve the object, that it isn't going to be perfect, and some holes will be there. To use this method, say something:
my $obj = new Piece('spy',1,2);
$obj->store('filename');
There are a couple of other object storing packages on CPAN that you can use as well. FreezeThaw is one (included on the CD), and ObjectStore is another. (If you are lucky enough to have 'ObjectStore', the object oriented data base management system, you can then store your objects, and not only restore them in Perl, but restore them in C++ and Java as well... ObjectStore will also allow you to store C++ and Java objects, and retrieve them in Perl!)
We now have to make a complementary function to store(), a retrieve() method which works to retrieve the stored function from disk.
Since retrieve actually creates an object out of a file, we implement it as another constructor. Instead of saying:
my $piece = new Piece(.....);
we say
my $piece = retrieve Piece("mypiece");
where mypiece is the name of a file that has been created under store(), and $piece becomes a blessed, piece reference.
_fill() and _validate() is again used to 'fill in some holes' in the object (which I will talk about soon.. honest!):
Listing 17.8 - retrieve()
137 sub retrieve
138 {
139 my ($type, $filename) = @_;
140
141 my $self = {};
142 my $fh = new FileHandle("$filename")
143 || (print("Couldn't open $filename for writing!\n"), return());
144
145
146 local($/) = undef;
147 my $text = <$fh>;
148 my $VAR1;
149 close($fh);
150 eval("$text");
151
152 $self = $VAR1;
153 $self->_fill({},{});
154
155 $self->_recordDebug('after') if ($self->{'debug'} eq 'on');
156 $self->_validate();
157 $self;
158 }
We could therefore say:
my $obj = new Piece ('bomb', 'white', 0,0)
$obj->store('file');
my $obj2 = retrieve Piece('file');
to do a roundabout copy of $obj into $obj2.
debug() isn't strictly a method, more a practice. You can easily debug by putting a bunch of printf statements in. You can also implement debug as a flag, saying something such as:
my $obj = new Piece('miner', 0,0, { 'debug' => 1 });
However, sometimes you want something more permanent than this. The idea behind debug is to leave a trace of important information so that later you can find out what is going on if there is an error.
If you implement debug as a flag, like this, usually it prints out everything: function entry, exit, etc.
This is one way to implement a trace. You can also say something such as:
my $obj = new Piece('miner', 0,0);
$obj->debug('on');
## do stuff ###
$obj->debug('off');
with the idea that, when you are doing stuff like this, you are saving everything you have done to a 'debug' member of the object. For instance:
Listing 17.9 - debug();
159
160 sub debug
161 {
162 my ($self, $type) = @_;
163
164 if ($type eq 'off') { $self->{'debug'} = 'off'; }
165 elsif ($type eq 'on') { $self->{'debug'} = 'on'; }
166 else { print "IMPROPER DEBUG CALL $type\n"; }
167 }
In order to make this work, we are going to have to retrofit every one of our methods already developed, putting in a function call at beginnings and endings. However, if you go back and look at the methods that we have already implemented, notice that we were being real sneaky again and already did this with a function called _recordDebug().
For example, we retrofitted set as shown:
Listing 17.10 - set() retrofitted
167 sub set
168 {
169 my ($self, $config) = @_;
170
171 $self->_recordDebug('before') if ($self->{'debug'} eq 'on');
172
173 $self->_fill( $config, {});
174 $self->_validate( $config, {});
175
176 $self->_recordDebug('after') if ($self->{'debug'} eq 'on');
177 }
_recordDebug() is a private method that is used to save what the object looked like when the method was entered, and when the method was left. It also saves which method called which method. It looks like what is in Listing 17.11:
Listing 17.11 - _recordDebug()
168 sub _recordDebug
169 {
170 my ($self, $flag) = @_;
171
172 my $debugstuff = $self->{'debug'};
173 my $no = @$debugstuff;
174
175 if ($flag eq 'before')
176 {
177 $debugstuff->[$no][0] = "Before: \n" . Carp::longmess();
178 $debugstuff->[$no][1] = Dumper($self);
179 }
180 elsif ($flag eq 'after')
181 {
182 $debugstuff->[$no-1][2] = "After: \n" . Carp::longmess();
183 $debugstuff->[$no-1][3] = Dumper($self);
184 }
185 }
Now, all we have to do is print out the 'debug' as so:
my $obj = new Piece('miner', 0,0);
$obj->debug('on');
## do stuff ###
$obj->debug('off');
my $debugstuff = $obj->get('debug');
print Dumper($debugstuff);
and we can get a direct trace of exactly what is going on in our Piece object, albeit in great gory detail. In fact, it might be in too great a detail. We may want to replace our _recordDebug() function with:
Listing 17.12 - _recordDebug()
sub _recordDebug
{
my ($self, $flag) = @_;
my $debugstuff = $self->{'debugstuff'};
my $no = @$debugstuff;
if ($flag eq 'before')
{
$debugstuff->[$no][0] = "Called By:\n" . Carp::longmess();
$oldself = $self->copy();
}
elsif ($flag eq 'after')
{
$oldself->{'debugstuff'} = undef;
my $newself = $self->copy();
$newself->{'debugstuff'} = undef;
$debugstuff->[$no-1][1] = "After: ". Diff::checkData($self, $oldself);
}
}
Note that we are reusing both the copy method that we developed earlier, and the Diff::checkData function that we developed in Chapter 15. This lets us see exactly which elements of the Piece object changed, which may speed up our bug tracking time quite a bit.
sprintf() performs exactly like its Perlish equivalent. It prints out to a formatted string some or all of the information contained in an object. Unlike debug(), it 'prettifies' its output for purpose of being printed out, or being used in the program itself. We may, for example want to make a summary of all the pieces on the map. It may look something like:
Piece #1: Admiral Coordinates # X = 4 Y = 5
Piece #2: Spy Coordinates # X = 1 Y = 2
and so forth. sprintf() is often quite easy to implement, since it usually is a simple wrapper around a bunch of native Perl sprintf() calls.
Here is sprintf for our Piece class:
Listing 17.13 - sprintf()
186
187 sub sprintf
188 {
189
190 my ($self) = @_;
191 return(sprintf("Piece #%.3d: %.15s Coordinates # X = %.3d Y = %.3d\n"));
192 }
193
When we now say
my $piece = new Piece('miner', 2,4);
my $text = $piece->sprintf();
then $text will contain the required text, looking something like:
Piece #1: miner Coordinates # X = 2 Y = 4
You can then use this text in any reporting function that you might like.
One more example of somewhat common functions: the grep function. grep() is a very successful function inside Perl. Why not make it work for objects?
Say you want to find out whether something is a bomb or not. Well, you could define a function:
$obj->isBomb();
and in an array of Pieces you could say something like:
foreach $piece (@pieces)
{
push (@stack, $piece) if $piece->isBomb();
}
but this seems kludgey, since you often want your criteria to be more complicated than simple things like this.
This is the perfect place for grep. You could rewrite the above foreach loop like:
foreach $piece (@pieces)
{
push(@stack, $piece) if ($piece->grep(sub { $_[0]->type() eq 'bomb' });
}
Here, grep takes another callback in the first position, and then executes it on the object $piece to return back whether or not $piece is of type bomb. In this case it is not that much of an improvement, and you could get the same results by saying:
foreach $piece (@pieces)
{
push (@stack, $piece) if ($piece->type() eq 'bomb');
}
but to tell you the truth, we can't make an effective demonstration of grep simply because the Piece example is too small. But we shall put it to good use later, since grep can take any subroutine that Perl can muster, whereas an if clause cannot.
Here is Piece's version of grep():
Listing 17.14 - grep()
194 sub grep
195 {
196 my ($self, $callback) = @_;
197
198 return (&$callback($self));
199 }
200
You can take this code and adapt it for your particular situation.
These six functions complement the most common methods quite well. Below are their summaries:
copy() - copies an object because Perl uses references to store objects.
store() - stores an object to disk. Relevant modules for this are Data::Dumper, FreezeThaw, and ObjectStore (which is not included on the book's CD because it is platform specific). Data::Dumper's usage was shown above.
retrieve() - does the opposite of store() and gets modules back from disk.
debug() - stores debug information in the object so we can retrieve it later.
sprintf() - format prints the object into a scalar so we can use it later in some sort of method.
grep() - returns whether or not a given object has a certain characteristic. Used with code references.
As a last point, any one of these methods can have a class equivalent. Hence, we can say
my @bomblist = Piece::grep( sub { $_[0]->type() eq 'bomb } );
with the understanding that Piece has, registered, a list of pieces which have been constructed. We don't have enough space to go through this in the detail that it deserves; go to the CD that comes with this book for more information!
There are two common, private methods that come up again and again. They are listed last because you usually write them last since they support pretty much all of the methods above, and each one of the methods above have a 'common assumption' that these two methods are going to work.
Since they support pretty much all of the methods in the class, they tend to get complicated. They do much of the chore of verifying that what the user entered is correct, and shuffle bits of data around inside the object. These methods are _fill() and _validate(). Although we used them up in other code, we implement them now. In order to understand _fill() and _validate(), we need to take a moment and describe persistence.
We alluded to 'holes' in the code several times in the preceding sections. In the copy() and retrieve() functions we used $self->_fill() and $self->_validate(). One might think that because Data::Dumper dumps out any Perl data structure, that our copy() function would be as simple as:
my $text = Dumper($self);
my $VAL1;
eval("$text");
return($VAL1);
where eval creates the object $VAL1 which is a direct copy of $self, and return() returns it to the main routine.
Alas, the copy() and retrieve() functions are not this simple. Unfortunately for us, at this point, Perl does not 'keep around' the text of the code that it keeps in code references. Test this yourself. If you say:
my $a = { 'a' => sub { print "HERE!\n" } };
and then say:
print Dumper($a);
you will get text that looks like:
$VAR1 = {
a => sub { "DUMMY" }
};
What has happened is that the text of the code went away, becoming an internal thing to Perl that looks like:
CODE(0xafe5c)
which you may recognize as a reference to code.
Thus, there are two elements that lose their value when your code exits:
1) code references and the like
2) elements outside of Perl's domain, such as database handles.
These two things are the challenges that persistent programming offers us. Note that if we didn't use code references, our copy(), store(), and retrieve() functions would be a lot simpler.
In this case, we use the function _fill() to fill in the gap, which we talk about next.
Fill's job is to make an object that is reasonable from the rest of the class' point of view. We saw fill in many places: the constructors new() and restore(), and inside the copy() and set() functions.
Here it is, implemented to work with all of these methods.
The fact that it is so complicated may be a clue that we want to supplement our design with inheritance; the result is in Listing 17.15:
Listing 17.15 - Piece.pm (_fill())
201 sub _fill
202 {
203 my ($self, $elemhash, $config) =@_;
204 $self->{'debugstuff'} = $self->{'debugstuff'} || [];
205 $elemhash = $elemhash || {};
206 $config = $config || {};
207 my $status = 0;
208 $status += _returnIfWrongTypeandDefined($elemhash, 'argument 1', 'HASH');
209 $status += _returnIfWrongTypeandDefined($config, 'argument 2', 'HASH');
210
211 return(undef) if ($status > 0);
212
213 %$self = (%$self, %$elemhash);
214
215 my $piece_type = $elemhash->{'type'} || $self->{'type'} ||
216 print(STDERR "Couldn't get a piece type!\n", Carp::longmess());
217
218 my $default_config = $_config->{$piece_type} || {};
219 if (!defined ($self->{'config'}) || defined ($elemhash->{'type'})
220 {
221 %{$self->{'config'}} = %{$default_config};
222 }
223 else
224 {
225 my $key;
226 foreach $key (keys(%$default_config))
227 {
228 if (!defined $self->{'config'}->{$key}
229 || $self->{'config'}->{$key} =~ m"CODE")
230 {
231 $self->{'config'}->{$key} = $default_config->{$key};
232 }
233 }
234 }
235
236 if (ref($config) eq 'HASH')
237 {
238 %{$self->{'config'}} = (%{$self->{'config'}}, %$config);
239 }
240
241
242
243 }
although we shall see how to simplify this in Chapter 20 when we get more to inheritance.
Now let's take this very slow. There are three major areas to this function:
1) type checking and default setting: Lines #204 - #206 set arguments that the user hasn't entered. The effect of line #206:
$config = $config || {};
is to insure that $config gets at least an empty hash reference. The fancifully named private function _returnIfWrongTypeandDefined() in line #208 and #209 is responsible for finding out if the user has entered in the right types to the _fill() function. If the user entered something other than a hash reference, this returns a 1. However, it also lets the user enter in something like:
$self->_fill();
without any arguments for convenience's sake. If the user enters something that is not a hash reference, then
211 return(undef) if ($status > 0);
will short circuit the function before continuing.
2) filling commonly accessed object elements. Now remember, in our design, there were two possible hashes that you could pass to _fill(). Lines 213 through 222 deal with the assignment of the elements which are the most common. Line 213:
%$self = (%$self, %$elemhash);
is a common pattern that you are going to see. It says, basically, 'set all of %$self's elements that are in %$elemhash, overriding the values that are already there.' If you started with a hash that looked like:
$self == { 1 => 2, 2 => 3};
and then said
%$self = (%$self, {1 => 4, 3 => 2});
you would end up with a hash that looked like:
$self == { 1 => 4, 2 => 3, 3 => 2 };
in which the 3 => 2 was appended and 1 => 4 replaced 1 => 2.
This statement handles most of the common cases. However, there is one special case that we need to take care of. Suppose we say:
$self->_fill({'type' => 'spy' }, {});
If we make a piece a different type, then that piece has to copy all of the attributes that the new piece type has. Hence, in lines 218-223 we do this. If $elemhash->{'type'} is defined, we override $self->{'config'} to be:
%{$self->{'config'}} = %{$_config->{$elemhash->{'type'}}};
and this gives the piece the proper elements.
3) filling in the configuration hash. The rest of the hash is dedicated to the second form of _fill() where you say:
$self->_fill({}, $configure);
where configure, as you may remember, is infrequently set attributes like 'movement' and 'xcoord'. In this case, we have the problem that we mentioned above, the problem of 'holes' in the data set. If you say:
my $piece2 = $piece->copy();
and $piece has any code references in it at all, then they will get dropped on the floor. The code snippet:
226 foreach $key (keys(%$default_config))
227 {
228 if (!defined $self->{'config'}->{$key}
229 || $self->{'config'}->{$key} =~ m"CODE")
230 {
231 $self->{'config'}->{$key} = $default_config->{$key};
232 }
233 }
prevents this from occurring. It looks for code references ($self->{'config'}->{$key} =~ m"CODE") and assumes that they are bad, replacing them with a good code reference in line 231.
Whew! As we said, this might be a good place for inheritance to take some of the complexity out of our function. In chapter 20, in fact with inheritance, we will rewrite this fill function to be a little bit more reasonable.
All that's left now is having to write our supporting private routine _returnIfWrongTypeandDefined:
244
245 sub _returnIfWrongTypeandDefined
246 {
247 my ($element, $stuff, $type) = @_;
248
249 if (ref($element) eq $type || !defined($element))
250 {
251 return(0);
252 }
253 else
254 {
255 print(STDERR "$stuff has to be of type $type\n", Carp::longmess());
256 return(1);
257 }
258 }
259 1;
which is fairly straightforward, if awkwardly named.
Since they can get so complicated, you may, at first, postpone implementing a _fill() method, and instead let the object 'grow' naturally by spreading bits and pieces of _fill() throughout the code. Then, when you are ready, take all of these pieces and consolidate them into one function. This is twice as much as being up front about it, but it is a good learning exercise.
Validate's job is to make sure that fill has done its job: to take the object that has been created and make sure that no unknown pieces of information enter into it. Validation methods are often ignored, but you will save a lot of time in the useful life of your class if you are up front about implementing them.
Like _fill() , _validate() tends to get complicated fast, with a little bit of fancy footwork to implement. Here is the _validate() method for our Piece class:
Listing 17.16 Piece.pm (_validate())
260 sub _validate
261 {
262 my ($self) = @_;
263
264
265 my $type = $self->{'type'};
266 my $config = $self->{'config'};
267 my @errors;
268
269 push (@errors, "Bad piece type :$type:!\n")
270 if (!defined ($_config->{$type}));
271
272 my ($key) = '';
273 my $typeconfig = $_config->{$type} || {};
274
275 foreach $key (keys %$config)
276 {
277 push (@errors, "invalid key :$key:!\n")
278 if (!defined ($typeconfig->{$key}));
279 }
280 print (STDERR @errors, Carp::longmess()) if (@errors);
281 }
282
There is an error stack here that keeps track of all the inconsistencies found in $self->{'config'}. Since $_config contains what should be in a correct object, all we have to do is make sure that each element in $self->{'config'} is also in $_config. We do this in lines #275-#279:
foreach $key (keys %$config)
{
push (@errors,... if (!defined ($typeconfig->{$key}));
}
keeping this error stack handy in case our object has more than one problem. In line #280 we then print out all of the accumulated errors.
Again, this is getting a bit complicated, and you may want to spread your validations throughout the code instead, and then consolidate them into a _validate() function later.
In general, all of the stuff that you are doing internal to the module and that is done in more than one place should be consolidated into a private method for your objects to call. In general, put a '_' in front of every private method that you have (saying 'hands off') and then proceed to call them as you would normal methods.
The two methods listed above, _fill() and _validate(), are particularly common in modules, but there are a whole slough of private methods. In fact, in many modules, there are more private methods than ones available to the outside world!
Private methods tend to use less generic names than public methods. Some of the ones below are named _execCode() and _isaFlag(), and you can usually recognize them because they do less generic operations than public methods do.
If you find that you are calling these private methods over and over again, then go ahead and make them public. After all, Perl isn't imposing the private boundaries for you. You are.
OK, a couple more notes. It would not be really fair of us if we didn't give an idea of how this module would be used, and we really can't give the big picture; there are just too many bits missing from the project that we defined.
We will go into more detail about the big picture about the Stratego problem in Chapters 19 and 20 when we talk about inheritance and polymorphism. |
However, we can give an idea of some pseudo-code which we might use to test out our pieces. Here are a couple.
For example:
my @pieces = ();
while (chop($line = <STDIN>))
{
my ($type, $xcoord, $ycoord) = split(/:/, $line);
push (@pieces, new Piece($type, $xcoord, $ycoord);
}
tests out the constructor, giving a list of pieces in @pieces given by typing on the command line.
And:
foreach $piece (@pieces)
{
print $piece->sprintf($piece);
}
prints out all of the configuration information for the pieces that have been created.
We really should go ahead and define a couple of the Piece specific functions, such as move() and attack(). Let's go ahead and do that, just for the sake of showing how it is done. We have defined some basic methods, and attack() and move() should merely be composites of the simple methods.
Just like blue and yellow form green, so get() and set() form move:
Listing 17.17 Piece.pm (move())
283 sub move
284 {
285 my ($self, $newxcoord, $newycoord) = @_;
286
287 my $xcoord = $self->get('xcoord');
288 my $ycoord = $self->get('ycoord');
289 my $movement = $self->get('movement');
290
291 my $xval = abs($newxcoord - $xcoord);
292 my $yval = abs($newycoord - $ycoord);
293
294 (print(STDERR "Cannot move to $newxcoord $newycoord\n"), return(undef))
295 if ($yval > 0 && $xval > 0);
296
297 if (($xval <= $movement) && ($yval <= $movement))
298 {
299 $self->set( { 'xcoord' => $newxcoord });
300 $self->set( { 'ycoord' => $newycoord });
301 }
302 else
303 {
304 print(STDERR "Cannot move to $newxcoord $newycoord!Too far!\n");
305 return(undef);
305 }
306 }
Here, we assume that the xcoordinate and ycoordinate are going to be entered in the function call, and then we go through some calculations (lines #287-295) to figure out if where we are moving is at a right angle.
If it is, then we go ahead and check if the movement attribute is greater than how far we are to move (line #297). If we can legally move, then go ahead and set the new attributes. Otherwise, return an error.
Finally, here is the attack function. It uses those code references that complicated the design so much:
307 sub attack
308 {
309 my ($self, $enemy) = @_;
310
311 my $attack_rank = $self->get('attacking_rank');
312 my $defend_rank = $enemy->get('defending_rank');
313
314 if (ref($attack_rank) eq 'CODE')
315 {
316 $attack_rank = &$attack_rank($self, $enemy);
317 }
318 if (ref($defend_rank) eq 'CODE')
319 {
320 $defend_rank = &$defend_rank($self, $enemy);
321 }
322 return("CAPTURED!") if ($defend_rank < $attack_rank);
323 return("VICTORIOUS!") if ($attack_rank < $defend_rank);
324 return("STALEMATE!") if ($attack_rank == $defend_rank);
325 }
In other words, both $self and $enemy are Piece references; we call get() on both to get their attacking defending rank, respectively. Here comes the tricky part.
If the attacking or the defending rank is a code reference, this indicates that we are going to use the code reference to find out what the rank is. Hence line #314 to line #318. The call:
$defend_rank = &$defend_rank($self, $enemy);
is one that you probably couldn't do in any other language (at least that I know of). It calls the code reference that is $defend_rank, using the Piece references $self and $enemy as arguments. As soon as it gets a result, the $defend_rank ceases to be a code reference, and becomes a number instead.
We then compare the numbers to get a result, which we return (either 'CAPTURED!', 'VICTORIOUS!', or 'STALEMATE!').
If we were to really implement this object, of course, this code would need a more modifications. First, there is no provision for two pieces not occupying the same square. A board object might be in order to be a traffic handler.
Second, we would probably want move() and attack() to be linked in some way, such that when the pieces move into a square, they automatically attack any pieces that may be there. Again, a board object might be in order here.
Anyway, for now, that is it for the sample object. In the next chapter, let's take this object a step further, and consider both the first design decision that we are going to have to face: whether to make what we are to program a module or an object, and on good ways of turning procedural code into objects.
![]() ![]() |
![]() ![]() |
![]() ![]() |
COMPUTING MCGRAW-HILL | Beta Books | Contact Us | Order Information | Online Catalog
HTML conversions by Mega Space.
This page updated on October 14, 1997 by Webmaster.
Computing McGraw-Hill is an imprint of the McGraw-Hill Professional Book Group.
Copyright ©1997 The McGraw-Hill Companies, Inc. All Rights Reserved.
Any use is subject to the rules stated in the
Terms of Use.