![]() ![]() |
![]() ![]() |
![]() ![]() | |
© 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. |
So far we have only considered objects by themselves, or as being used by scripts. However, on any large project, the objects that you create will not only be used inside scripts, but will also be used or intermingled with other objects. To this end, Object hierarchies are formed in a large project. Object hierarchies allow useful, higher level objects to be built out of lower level objects. These hierarchies may be several levels deep. This is the goal of object oriented programming.
For example, an 'application' object (like a spreadsheet, or a drawing program) might be built out of several screen objects and user input objects. Of course actual object hierarchies are more complicated than this.
The two main techniques at your disposal in building these object hierarchies are:
1) inheritance
and
2) layering
ed - the chapter title is inheritance and layering. the next paragraph seems to say that layering is not covered in this chapter... tks, md
The purpose of this chapter is to discuss the first of these techniques: inheritance. Inheritance is not a very well understood process and it takes quite a bit of finesse to use it well. I suggest that you understand not only what inheritance is, but when it is appropriate to use it (and when you should be using layering instead), if you are going to use inheritance in your code.
Inheritance is, above all, a design technique. Inheritance should be used to compose projects which are bigger than just 'making one object and then using it in a script' (as we have done in previous chapters). Since it is a design technique, this chapter will be heavy on the example side. The term inheritance itself is fairly straightforward, but the application of inheritance can be more complicated indeed.
ed - what is the "first major tool"? tks, md
First we shall look at the second major tool which is really helpful in understanding how inheritance works. This is the object diagram, the blueprint of the computer science world. Building object diagrams for your projects are essential in understanding how to put things together cleanly.
From there, we will briefly review inheritance, going into its definition, its plusses and minuses, and some of the circumstances in which you would want to use inheritance. Finally, we will look at examples where we are asked to scale up some code. We will ask ourselves: is inheritance the correct way to accomplish a given design? If so, how do we go about implementing it?
Readers that are already familiar with object oriented programming techniques will probably be familiar with these diagrams. Once you get into programming complicated projects, you will bump your head against logical diagrams which explain how you are going to do what you intend to do. We have already seen a class of diagrams that can be helpful to the software engineer. This was the functional diagram, which was covered in chapter 17. These were used to outline how functions call each other.
Object diagrams take that logic a step further. Object Diagrams are diagrams which describe how two or more objects are related to and dependent upon each other. In other words, object diagrams do the same thing as functional diagrams, only at a higher level of abstraction. They take functions and hide how they relate to each other, only showing where objects are dependent on other objects. There are several forms of object diagrams. I use a deviant of a Booch diagram which tends to give more detail than the classic, Booch diagram.*
Booch's methodology is a formal, C++-ish way of picturing object hierarchies. Booch has several books on the subject of OO design. |
To understand object diagrams, you really need to understand the concepts of ISA and HASA. We talk about these in conjunction with Object Diagrams below.
ISA is a term which denotes the type of relationship between two objects. It is used in close conjunction with inheritance. If something has an ISA relationship with something else, it probably is modeled using inheritance.
ed - second sentence below. type sounds like a technical term. what about saying "specific type" or "specific unit"? tks, md
The best way of getting a grip on what ISA means is to think in colloquial terms. An object ISA ("is-a") child of another object if it is a type of that object. Hence, a sundial ISA clock, a dog ISA mammal, and a lake ISA BodyOfWater. Say we were making a program that models the phylogeny, the 'tree of life', and we were looking at how to model crustaceans (crabs, whelks, etc.). The resulting Object Hierarchy might look something like this:
201.fig
Figure 20.1
Object Diagram showing crustaceans
Here a whelk ISA crustacean ISA arthropod, a crab ISA crustacean ISA arthropod, and a spider ISA arachnid ISA arthropod.
ed - plz finish your thought. tks, md
In each case these are sub
ed - probably need to make a stronger case of how ISA reflects inheritance. tks, md
Thinking about things this way, while not 100% effective, gives you a good idea of when to use inheritance. Inheritance by and large has to do with cutting up and designing things up along the lines of variants. If something is a variant of another thing, then it probably can be modeled in an ISA relationship, or an inheritance relationship.
HASA ("has-a") is also a term which denotes the relationship between two or more objects. While ISA is closely associated with inheritance, HASA is closely associated with layering.
Again, if you want to understand HASA, you should think colloquially. A CEO can be modeled as being a manager, which is in turn an employee, which is in turn a person. However, we can equally split up a person (or employee, or manager) by looking at their component parts and how they relate:
202.fig
Figure 20.2
Object Diagram Showing reduction of Person
Here, a person HASA skeletal system, a nervous system, and a cardiovascular system. In turn, a skeletal system has bones, joints, and ligaments. We can do the same thing with the nervous and cardiovascular systems.
This technique is called reducing a system to its working parts, and this is by far the most common way to make an object hierarchy. Although we have not pointed it out earlier, it has been common for use to have a HASA or layering relationship in the objects that we have used so far. When we say something like:
package MyPackage;
use FileHandle;
sub new
{my ($type, $filename) = @_;
my $self = bless {}, $type;
$self->{fd} = new FileHandle("$filename") || die "Couldn't open $filename!\n";
return($self);
}
we are in fact making a HASA relationship. MyPackage HASA FileHandle, and we store it inside the object itself.
We have pointed out here the second major type of diagram to scale up projects. This is the object diagram and it shows how objects themselves relate to each other.
Furthermore, object diagrams have two major types of relationships: ISA and HASA. ISA relationships have to do with inheritance. ISA points out a 'family relationship' between objects: if something ISA variant of something else (as quartz ISA rock) then chances are an inheritance relationship is appropriate.
HASA relationships have to do with layering, with analyzing something and breaking it into smaller parts. This is illustrated by a person HASA skeletal system, a car HASA motor, and a computer HASA CPU. If you see something like this, your first instinct should be to model your problem in terms of layering.
Now, let's go a little bit more into the details of inheritance. If you are interested in layering instead, check out the next chapter.
We have already talked a bit about the syntax of inheritance (see chapter 16 for more detail) but we have not gone into detail about when or why you would want to use inheritance. This chapter discusses appropriate usage of inheritance. But first, let's have a short review of inheritance and how it works.
Let's go over how inheritance works in the abstract. You can find the details of how it works technically in chapter 16. Remember, Inheritance is the process of making a class a particular version of another class, with the intention of having the parent class act as a default that supplies functionality if the child does not have it.
The example we gave in Chapter 16 was a clock. The parent class, Clock, kept all the information about how to actually get the time from the system. The child classes (System and Sundial) contained information on how to set the time.
The result is a simple relationship, something that looks like Figure 20.3:
203.fig
Figure 20.3
Inheritance Relationship for Clock Object
This diagram is, again, an Object Diagram. We have, however, gone a little bit further and added a couple of things that make going from this diagram to code.
Each object has two labels: the name of the object and the filename that corresponds to the object. We have also, for clarity's sake, included each method that each object defines. Finally, the arrow points to the parent in the child-parent relationship that is shown here.
The key word in this diagram is ISA. The Clock::Sundial ISA Clock, which in turn ISA UNIVERSAL object. You can translate this into the following, skeleton code, shown in Figure 20.4:
204.fig
Figure 20.4
Skeleton Code for Clock object
When this code is called inside a client:
use Clock::Sundial;
my $sd = new Clock::Sundial();
print $sd->get();
Perl will then 'follow the arrows' of the diagram, looking for get() inside of Clock::Sundial first, then traversing up to Clock next. If Perl cannot find get() here, it traverses up to the special object called UNIVERSAL looking for get(), finally giving up and dying if it cannot find the method there.
This is the bare bones of inheritance as we covered it in detail in chapter 16. However, there is one question that we failed to answer in that chapter: exactly what is the motivation for setting up an inheritance tree in this manner?
ed - this whole section needs help. the points that you are trying to make are buried in detail and very indirect. Please try to go through each point and make sure the important stuff is in the first or last sentence of the paragraph. thanks, md
Why then, go through this elaborate scheme of setting certain objects as being children of other objects, and then looking for functionality in parent objects if one can't find it in the child? The answer is that there are benefits and drawbacks. We will start with the drawbacks.
There are three major drawbacks to inheritance. These are the complexity that inheritance entails, the overhead that it involves, and the dependancies that inheritance can create between parent and children.
There is no doubt about it: making an inheritance tree in your code can make Perl much more difficult to debug and to program. Since the code for different objects are typically kept in different files, you tend to bounce around from file to file to debug your code. (Unless, of course, you keep all the objects in your inheritance hierarchy open yourself. In this case you still have to remember where each method is.)
For example, say that something is wrong with set(). The first step debug the code is to open Clock::Sundial to see where the bug is. The problem is that set() is not there, but was inherited from Clock. The next step is to look inside Clock. And if Clock did not have set(), you would next look inside UNIVERSAL. The deeper the inheritance tree, the worse the problem gets.
There is also the debugging problem of thinking that one piece of code was run when something else actually ran. Suppose that you accidentally put set() inside Clock::Sundial, so the two objects Clock, and Clock::Sundial looked something like this:
Clock.pm
package Clock;
sub set { print "Should be running this!\n"; }
Clock/Sundial.pm
package Clock::Sundial;
use Clock;
@ISA = qw(Clock);
# ... other code
sub set { print "Mistake"; }
Now, when you run a program which looks like
use Clock::Sundial;
my $a = new Clock::Sundial();
$a->set('Jul 15 1997');
Perl will run the call:
Clock::Sundial::set($a, 'Jul 15 1997');
and print out "Mistake". Why? Because Perl takes the following path to execute 'set':
1) First, it tries to run Clock::Sundial::set(),
2) Only if it can't find Clock::Sundial::set(), does it run Clock::set()
3) if it can't find Clock::set(), it tries UNIVERSAL::set() next
4) Perl then dies of an error.
ed - in the next sentence you say "since", but up above it is as if Perl did not find the function. Shoudl "since" be "when"? tks, md
Since Perl runs across a Clock::Sundial::set(), Perl executes this version. However, if you are not careful, you may be looking in Clock.pm instead, keep on changing Clock.pm's version of set(), wondering why your stupid computer is not responding to your code changes. Do not snigger; I have done this several times and it is a royal pain to track it down!
Just as object oriented programming has overhead compared to modular programming, programming with inheritance has an overhead over object oriented programming. Inheritance requires you to keep track of inheritance trees, create multiple files to hold child objects, worry about putting use statements in each one of the children, sync everything up, and of course test. The fact that all of this is going on at the same time makes it more difficult to 'focus' when you are setting up an inheritance tree.
ed - last sentence up above, need another word for focus. tks, md
The best way to show this overhead is by looking at the before and after effects in a diagram. Figure 20.5 shows how our code will change when we introduce inheritance into the picture:
205.fig
Figure 20.5
Before and after Inheritance Diagram
Not only are there more files to write, we have to create a directory, and make sure the three files work in a team to get the desired effect.
ed - might be nice to mention in the first or second sentence what is the drawback. I don't see it in the paragraph. tks, md
Most of the time, there is also a second more subtle drawback for inheritance. Notice that if you set up Clock::Sundial as a child of Clock so they can share functionality, this makes Clock::Sundial dependent on Clock.
Say that Clock has an element, time, which it uses when you call set():
Clock.pm:
package Clock;
sub set
{
my ($self, $time) = @_;
$self->{'time'} = $time;
}
Programming set() this way commits to having an element inside the object called time. If we now want to use the results of the function set() inside any of the other object which inherits from Clock, say Clock::System:
ed - this is a little unclear. what are you trying to say? tks, md
package Clock::System;
@ISA = qw (Clock);
sub get
{
my ($self) = @_;
return($self->{'time'});
}
In this case, the Clock::System object also has to have a time element in it. Without it, there would be no way of returning the time element as made by the Clock::set() method call!*
Notice however, that this lack of flexibility is from the point of view of the programmer and not the user. If you want, you can do something like:
This is dynamic type binding and it is a lot more difficult to do in C++ than in Perl! This facility in Perl makes inheritance a lot more practicable. |
Of course, we could program our own set() function inside Clock::System, something like:
package Clock::System;
@ISA = qw(Clock);
sub set
{
my ($self) = @_:
$self->{'mytime'} = time;
}
sub get
{
my ($self) = @_;
return($self->{'mytime'});
}
Since there is a separate set() function, we break the dependency that Clock::System has on the element time. We call the relevant element mytime instead. However, we also are not using inheritance a lot. In fact, we are avoiding inheritance for the sake of flexibility.
If one of your objects inherits from the other but you are rewriting your code in this way quite a bit, you should probably be splitting these objects into two separate, unrelated objects. Inflexibility in design can be a real killer, especially at the early stages in an object design.
So you might be wondering why people would even use inheritance in the first place. Some people indeed find the above drawbacks too much of a liability and never use inheritance. However, there are three positive benefits of inheritance that can outweigh the negatives.
You might think of this rule as the direct corollary of the 'loss of flexibility' rule given above. Every ounce of flexibility you lose by using inheritance, you gain in programming consistency.
Consider what we did to the Clock::System object, for example. We were forced to keep a certain structure for the objects that we created. Since Clock::set() used the element $self->{'time'}, Clock::System::get() also had to use the element $self->{'time'}.
Thus we can use inheritance to tie two different implementations together so that they don't diverge from another. Of course, if you then change the parent object so it uses $self->{'mytime'} itself, you then have to change all of your child objects to use $self->{'mytime'} as well.
ed - what do you mean by the word "true" in the next sentence? tks, md
This is the reason why most people use inheritance, and it is true. For a certain class of problems, inheritance does an admirable job of modeling a 'variation' relationship. It is difficult to discuss this except by example, and we shall get to these later on. The important thing to remember is that inheritance can model the subtype relationship well, but it doesn't necessarily have to.
Consider if you were to make an object that represented 'money'. You might have subtypes, such as Bonds, Stocks, Currency, and Gold, and your design might look something like Figure 20.6:
206.fig
Figure 20.6
Inheritance Relationship for Money Objects
The next step is to make a 'Money' parent object, and then several inherited objects which have attributes of their own. We could then define an object as:
my $a = new Money::Bonds::Savings('100.00');
to make one hundred dollars worth of Savings Bonds. However, this is a bit of a shortsighted design. What happens if we want to change our Bonds into currency? We are then stuck with the ignoble task of taking our object, destroying it as it currently stands, and then recreating it as Currency!
This would require us to have an additional converter object, which knows how to convert between different types of currency. We therefore have added a bunch of complexity that we do not need.
What went wrong here? What went wrong is that modeling money is a fluid problem, whereas inheritance imposes a certain rigidity on the problem domain. Here, the disadvantage of 'lack of flexibility' overwhelms the advantage of how inheritance can model subtypes. We shall see a lot of this give and take when we get to the examples below.
One of the common practices of beginner object oriented programmers is to have one, big object, which is infinitely configurable. In fact, we have already seen some objects like this in previous chapters: the LogObject object in chapter 18 was like this, the Piece object in Chapter 17, and also the Expect example in chapter 18.
All of these objects had one thing in common. They had a type attribute, which was really doing the inheritance work. When we defined the 'LogObject', we had the following private data at the top of the object:
8 my $_defaultConfig = { 'type' => 'regular', 'action' => 'append' };
9 my $_legal = { 'action' => { 'append' => '>> ','overwrite' =>'> ' },
10 'type' => { 'regular' => 1, 'stamped' => 1 }
11 };
The only purpose of the type information in this code is to get information from the user for the object. Hence, we had the write() function which looked something like:
44 sub write
45 {
46 my ($self, @text) = @_;
47 my $config = $self->{config};
48 my ($type, $fh) = ($config->{type}, $self->{filehandle});
49 if ($type eq 'regular') { $self->_writeRegular(@text); }
50 elsif ($type eq 'stamped') { $self->_writeStamped(@text); }
51 }
52
In other words, this is a big 'if-then' statement. It makes a call to a different private function (in bold) based on the type passed to the object, from the user.
ed - in the following sentence, what is not a bad thing? tks, md
This isn't necessarily a bad thing; for one, it puts all of your functionality in one place. Some folks would vehemently disagree with this statement, they would say the design becomes 'inflexible' because of this. I disagree. Because all the functionality is in one place, it is quite easy to trace, and can be easy to debug. This is easier to debug than if you were using inheritance this is!
ed - last sentence up above is a little confusing. plz help. tks, md
Realize, though, that this practice is not scaleable. If you do a lot of this, your code will tend to get tangled up in itself, and and the code can be long. I have seen modules 5000 lines long!*
**** Begin Box ****
Negative Effects of Monolithic Code.
There are two major waysin which monolothic code can cause problems:
1) modification of the code becomes difficult: If your code becomes monolithic, your code starts to control you, rather than you controlling your code.
Consider source control problems, for instance. If you have a 5000 line module and two different people need to make changes in it simultaneously, there will be a bit of tug-of-war. You will need to simultaneously check the code out, make changes, and then merge the changes. If you have ever done any merges, you know they are not fun. If two people change the same piece of code at the same time, then it's going to be a puzzle to figure out which one was the 'right change'.
2) your code can become entangled: Once your code gets to a certain length, it will be almost impossible to pull apart. Suppose for example that '_writeStamped()' calls '_writeRegular()', something like:
sub _writeStamped
{
my ($self, @text) = @_;
my $fh = $self->{filehandle};
my $script = $0; my $time = localtime();
print $fh "$script:$time ";
$self->_writeRegular();
}
This works because _writeStamped() simply writes the script name, the time the entry was made, and the output of _writeRegular() afterwards. However, the two lines of code that this saves you also creates a dependency of _writeStamped() on _writeRegular().
When you try to move _writeStamped() to its own file, you will find out that it depends on _writeRegular() and the function will simply not work. Also, when you try to change _writeRegular(), you also change _writeStamped() by default! This is almost worse, since it is a silent error, and will make your program die without telling you.
When objects get large, in the 1000-5000 line range, these interdependencies can choke your objects like lilies choke a stagnant pond. Figure 20.7 shows the dependency diagram, one that shows how functions call each other in our simple example (before and after)
207.fig
Figure 20.7
Dependency Diagrams for LogObject.
Note how this change has turned the dependency diagram from a strict hierarchy (in the first part of the diagram) into a triangle, a loop. This is what causes things to become much more immobile. The objects become like a house of cards; if one card moves or is displaced, the whole structure collapses.
Of course, you can simply avoid these inter-dependencies, by not using inheritance to help you. However, inheritance can be used as a blockade. With an inherited structure, _writeRegular() would be moved to its own file, so you cannot ever call it like this.
**** End Box ****
Think of inheritance as a tool for cleaving monolithic code into pieces that would otherwise stick together, and be difficult to break apart. By enforcing a strict hierarchy, inheritance makes it easier for you to better design. Although as always, take this comment with a bit of caution!
Inheritance is by no means a panacea; to think that you should always use inheritance is a mistake. In fact, you should weigh quite closely whether or not you should be using inheritance every single time your project reaches a growth point and you need to make a decision on how to scale up code. Remember that inheritance is not your only choice, layering is a quite reasonable alternative which we shall discuss in the next chapter.
In brief, here are the advantages and disadvantages of inheritance that we discussed up above.
Disadvantages:
1) Using inheritance makes a solution more complicated than if it were a one-object solution.
2) Inheritance has a larger overhead than a one object solution.
3) Inheritance lessens flexibility in structuring objects.
Advantages:
1) Inheritance enforces consistency on objects; it forces them to work with each other.
2) Inheritance can model relationships where one object is a variation of another (hydrogen ISA atom).
3) Inheritance can split up monolithic code.
Now, the process of actually using inheritance is a question of seeing if the advantages of inheritance fit more closely with your problem than the disadvantages. Next, we consider three different examples, and see how inheritance could be used.
ed - should this be titled "examples of scaling up code by inheritance"? tks, md
You are going to want to think things through well before you decide to scale up code by using inheritance. I generally go through a four step process in designing and/or implementing an inheritance solution. The four steps are:
1) define the problem. Defining the problem gives you a lot of information about what will and will not work, as well as giving ideas on how to implement. If the design looks like it might be a potential candidate for inheritance, I go on to step 2.
2) write the object diagram (ie: code skeleton) as if it were one object, and the equivalent object diagram as if it were inherited. This solidifies how you are going to implement the problem, but also gives you an idea about whether or not the object is a good candidate for inheritance. In an inherited solution, how much overlap will there be? Too much? Too little?
3) list down pro's and con's to inheritance for the problem. Here, be brutally honest, cataloging what are the benefits and the negatives of inheritance in this situation. The important part of this step is to have this honesty; it really does not pay to design and implement inheritance and find out that it was a bad idea (although everybody does it every once in a while).
4) make a decision. After doing all of this preparatory work, make a decision on what to do. If the answer is a clear 'yes', go on. If it is a 'maybe', look at other solutions. I usually wait a while for the answer to become clear. If it is a no, we will want to look at layering.
5a) if the decision is yes, implement inheritance. Finally, and only on a yes to #4, implement inheritance. This process should be made a lot easier by the object diagrams made in step 2.
5b) if the decision is no, look at layering. Often, the answer will be that inheritance is not the answer. In this case, consider layering, which we shall discuss next chapter.
So let's take a look at three examples and go step-by-step through the process of deciding whether or not something is fit for inheritance. Let's start with our 'Piece' example, the one we developed in chapter 17.
We developed the Piece class with the idea that it was going to be the foundation class for our game 'Strategio'. Hence, it has the potential to become pretty complicated. Should inheritance be used to scale up this particular class?
The way we wrote 'Piece.pm' had some difficult points. We had several different types of pieces stuffed into a single class. We could define an admiral by saying:
my $admiral = new Piece('admiral','red',3,5);
to define an admiral on the red side on the square 3,5, and we could say:
my $scout = new Piece('scout', 'black',3,2);
to define a scout on the black side on the square 3,2. However, this flexibility had a cost. The _validate() function, which we wrote to do some checking, was getting pretty complicated, as was _fill(). Can we get rid of some of this complexity by using inheritance by saying:
my $admiral = new Piece::Admiral('red',3,5);
instead, and letting inheritance take some of the job of validating the pieces (seeing if they are correct)?
ed - before and after what? tks, md
To determine the answer to this question, lay the Object Diagrams down side by side and compare them. Which functions do we put in the parent? Which ones in the child? Which ones don't we know about? The best that I could do is come up with is shown in Figure 20.8:
208.fig
Figure 20.8
Object diagram for Piece class
The one thing that is fairly apparent here is that there is not too much to inherit. The only function that we have included in our child objects is new(). Furthermore, we have to rename new() to _new() in the parent object.
ed - the following paragraph needs some help. what's the point? tks, md
Note that all of the work that we actually show would not be implemented in real life, just thought about. It is given as an attempt to 'split my brain open so you can see the contents' (a graphic metaphor, but it works).
Let's go back to the implementation of the Piece example. Notice that everything in the Piece object was dependent on one hash, the config hash.
For example, the get object was programmed like this:
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
which then went into the configuration hash to get a particular element. As it stands, there is a default configuration hash that looks like:
8 my $_config =
9 {
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 # REST OF HASH.
34 }
This has quite an impact on how we would implement inheritance. The only way to implement an inheritance tree without having to rewrite everything is to split the packages along data lines, and not along functional lines. If we do not do this, then we will have to change the entire strategy on how we write get, set, store, etc. Each of the individual child classes would have a header that looks like:
Listing 20.1 Piece::Admiral header
Piece/Admiral.pm
1 package Piece::Admiral;
2
3 use Piece;
4 @ISA = qw (Piece);
5 use strict;
6
7 my $_config = {
8 'attacking_rank' => 1,
9 'movement' => 1,
10 'defending_rank' => 1
11 };
We have taken the config hash from the parent, and turned it into a one-dimensional hash which contains only the attributes for admirals. Again, '@ISA' is used to show that Piece::Admiral inherits from the class Piece. We have to include that functionality by saying 'use Piece' at the beginning of the package.
When we are done with this, we have to have some way of actually conveying that information to the main Piece class, so it can store the information inside $self->{config}.*
An alternative is to write our own _fill() and _validate() functions inside each one of the nine objects, for a total of 18 functions. Since pieces only differ by data and not by functionality, these _fill() and _validate() functions will look exactly the same! |
We must write our own constructor for each class to pass $_config to the main parent class. This constructor might look something like this:
Listing 20.2 Piece::Admiral new() constructor
12 sub new
13 {
14 my ($type, $color, $xcoord, $ycoord, $config) = @_;
15 my $self = bless {}, $type;
16 $self->_new($color, $xcoord, $ycoord, $config, $_config);
17 $self;
18 }
The key line, in bold, is line 16. Here, we put $_config into a call of the old constructor, the one defined inside 'Piece.pm'. This is why we need to rename new inside Piece::Admiral. Since we already have a new function there, if we do not rename it, it won't be called. Anyway, it makes sense to rename it to be private since Piece::_new() is now private: it should never be called directly by any client that uses the Piece:: hierarchy.
Since we are now calling Piece::_new() in the clients, we need to reflect that change inside the parent object itself. We therefore change our constructor to look something like:
Listing 20.3 Piece::Admiral constructor after inheritance
44 sub _new
45 {
46 my ($self, $piece_side, $xcoord, $ycoord, $config, $_config) = @_;
47
48
49 (print(STDERR "Incorrect Args!\n", Carp::longmess()), return(undef))
50 if (@_ != 6);
51 $config = $config || {};
52 %$config = (%$config,
53 piece_side => $piece_side,
54 piece_no = $_pieceNo
55 );
56 # my $self = bless {}, $type; take out.
57 $self->_fill
58 (
59 {
60 'type' => $piece_type,
61 'xcoord' => $xcoord,
62 'ycoord' => $ycoord,
63 },
64 $config, $_config
65 );
66
67 $self->_validate($_config);
68 $self->_recordDebug('after') if ($self->{'debug'} eq 'on');
69 # retrofitted for function debug() see debug method below.
70 return($self);
71 }
72
The bolded arguments are new; the rest of the constructor can stay the same. Note that we have to change new() into a function which takes a class name as an argument, into one that takes an object. This is because the children are now calling the constructor as an object method and not a class method. We would have to go through and change _fill() and _validate() as well, since they have some dependencies on $_config. Since we have gone this far, we might as well show the new versions:
201 sub _fill
202 {
203 my ($self, $elemhash, $config $_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');
209 $status += _returnIfWrongTypeandDefined($_config, 'argument 3', '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'}) # removed piece stuff.
220 {
221 %{$self->{'config'}} = %{$_config}; # used to be default config
222 }
223 else
224 {
225 my $key;
226 foreach $key (keys(%$_config)) # used to be 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 }
ed - why is $default_config going away? tks, md
All we did is take out the code concerned with typing (lines 215 through 218), and substitute the variable $default_config - which is going away - for what we got from the child (ie: $_config). We do the same for _validate():
260 sub _validate
261 {
262 my ($self, $_config) = @_;
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 ($_config->{$key}));
279 }
280 print (STDERR @errors, Carp::longmess()) if (@errors);
281 }
282
The bolded lines are taken out; these lines have to do with the old way of doing things by looking up the correct values inside the old, global hash reference $_config.
What can be said about this solution so far? After we would be done with our inheritance implementation, there is the following drawbacks:
nine extra files
nine extra new() constructors inside the nine new files
nine extra class variables (one $_config per class)
one extra directory (Piece/)
a more complicated path to trace when debugging and coding
change three functions inside the central parent 'Piece' class
As for the plusses, we would have:
a slightly simpler _validate() and _fill().
So, based on this thought experiment (which we actually implemented to show what goes into making something use an inheritance hierarchy) we have come up with six negatives, and one shaky positive. It is not too difficult to determine the best decision.
The decision is: No, we don't want to make this problem use inheritance. Not only have we made things more complicated by using inheritance with nine files and nine extra functions to boot, but also we have to ask ourselves how the loss of flexibility we get by putting things into an inherited format will affect the solution of our problem.
We were, in fact, being a bit naive. When it comes down to it, in this case inheritance actually makes finding a solution ten times more difficult. To see why, I quote part of the domain example:
In our 'mock' Strategio game we will remove these constraints [rigidity of pieces, etc]. Pieces should have the ability 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.
What does 'promoted' or 'demoted' mean in the context of this example? It means that if we have constructed an object as so:
my $object = new Piece::General('red',3,1);
that we somehow have to be able to turn this piece into an Admiral (promote it) or turn it into a Colonel (demote it). Before, when we had everything in one object, we could say something like:
$object->set({'type' => 'admiral' });
to actually 'flip the switch' and make the object an admiral. However, the way it stands now, we have no way to do this promotion. We would have to dump the data structure out into a hash, and then 'rebless' the Piece::General to be a Piece::Admiral.
So, not only does inheritance make the project more complicated here, it actually doesn't fit the problem domain as well as a simple, one object solution did.
There are lots of things to be said about this example. We went through the process of making a before and after diagram, and then thought about the pro's and con's of making the Piece object an inheritance hierarchy.
Just for kicks, then, in thinking about these pro's and con's, we went ahead and implemented the object as it would look using inheritance. Finally, we decided that it was not the best idea to have the Piece class use inheritance after all.
However, there are some more abstract points to be made. I really expected the Piece object to be suitable for inheritance; in fact I designed the example with inheritance in mind! When it came to actually implement it, though, I thought carefully and in much detail as to what inheritance would require, including the benefits and drawbacks. All of the information pointed to inheritance being the wrong tool for the job, so I switched my outlook 180% degrees and decided not to force the issue.
I'm all for experiment, but I advise that you be one hundred percent honest with your own code, realizing that no code is perfect, and that mistakes in your code will always be plentiful. I hope I'm not preaching the obvious, but I have fallen in this trap many times. Try to save yourself by being impartial with your code.
Now let's take a look at a little bit more practical an example, borrowed from chapter 18. As you may remember, the LogObject was an object that spit out 'log files', which as we left it were of two types:
regular - which simply spit out the text as the user passed it to the LogObject function
stamped - a time-stamped type of log for the LogObject function. It not only tells when a log entry was made, but also shows which script made the entry.
As of now, we have the LogObject in one, centralized class. Does it make sense to split it up into several, smaller classes?
When we implemented the LogObject in chapter 18, we noted a couple of things. First, an average call to create a LogObject looked something like this:
my $object = new LogObject
(
'filename',
{ 'type' => 'regular', 'action' => 'append'}
);
Even when we wrote this down, we remarked that it was a little bit complicated. When we gave it an explicit type like this ('type' => 'regular'), the main code that was affected was the write function which looked something like this:
44 sub write
45 {
46 my ($self, @text) = @_;
47 my $config = $self->{config};
48 my ($type, $fh) = ($config->{type}, $self->{filehandle});
49 if ($type eq 'regular') { $self->_writeRegular(@text); }
50 elsif ($type eq 'stamped') { $self->_writeStamped(@text); }
51 }
52
Lines 49 and 50 show what we might want to do: split up our object so that our constructor call looks something like:
my $object = new LogObject::Regular('filename', { 'action' => 'append' });
Should we use inheritance to make this change, and if so, how?
As we did in the last example, let's take a look at the object diagrams that are generated from a before and after snapshot view of what inheritance will do. Figure 20.9 shows the results:
209.fig
Figure 20.9
Before and after shots of LogObject
As a part of looking towards the future, we have put in an extra object here: LogObjectInh::HTML. We name it LogObjectInh to differentiate from LogObject on the CD.
Now, by looking at this diagram, we can come up with quite a few observations about inheritance and how we would apply it to this problem. Note that this example falls pretty squarely into the 'inheritance hierarchy' concept. The child objects create one of their own functions, pretty much across the board, and this one function is then modified for each object, to model how the child differs from the parent.
Now, the question is: how does this work? After all, we need to justify that the design is sound; we have to be able to turn these pictures into code. Our main design point revolves around what write does.
Right now, our code for write looks like:
44 sub write
45 {
46 my ($self, @text) = @_;
47 my $config = $self->{config};
48 my ($type, $fh) = ($config->{type}, $self->{filehandle});
49 if ($type eq 'regular') { $self->_writeRegular(@text); }
50 elsif ($type eq 'stamped') { $self->_writeStamped(@text); }
51 }
52
with _writeStamped() and _writeRegular() both doing the job of writing out stuff to a file. Now suppose that we want to make it so we can write the text out to a database as well. We would have to write code that looks something like:
sub write
{
my ($self, @text) = @_;
my $config = $self->{'config'};
my ($type, $fh) = ($config->{type}, $config->{dbase}, $self->{filehandle});
if ($type eq 'regular' && $place eq 'file')
{
$self->_writeRegular('file', @text);
}
elsif ($type eq 'regular' && $place eq 'dbase')
{
$self->_writeRegular('dbase', @text);
}
elsif ($type eq 'stamped' && $place eq 'file')
{
$self->_writeStamped('file', @text);
}
elsif ($type eq 'stamped' && $place eq 'dbase')
{
$self->_writeStamped('file', @text);
}
}
This is a parody; please don't write like this at home! It gets worse, too, when we add the ability to print out html. This is written just to make a point: the above code is not doing a very good job of segregating out its tasks very well. The job of actually producing the text (_writeStamped() , _writeRegular()) is also in charge of deciding where the text is being printed (database or file).
We would like to make it so each write() function is in charge of producing the type of text that it wants to print out, and LogObjectInh::_write() - the central source - is in charge of where to print out our text. In other words, if we say:
ed - two things about this example: "Its" should be "It's" and i don't know how to do this in Perl. Also, can we take out the "my god!" many would find this offensive. how many examples is this phrase in? tks, md
my $object = new LogObjectInh::Regular('filename', { 'action' => 'append' } );
$object->write('my god! Its full of stars');
this then translates into:
LogObjectInh::Regular::write($object, 'my god! Its full of stars');
which should then call:
LogObjectInh::_write($object, 'my god! Its full of stars');
This function directs exactly where to print the text. Much cleaner! In fact, we can show this cleanliness by looking at another before and after diagram. This one is a functional diagram:
2010.fig
Figure 20.10
Functional Diagrams showing impact of inheritance
In short, in the 'after' diagram, _write() is used as a juncture point. All of the child subroutines that we will write go through it so if we want to quick, switch and write everything to email, or to a database, we can go ahead and do that.
We have gone through quite enough speculation up above. Now is the time to take what we have got up above and turn it into a concrete list; something we can base a decision on.
Looking at this, I get the following negatives:
multiple files: changing one file into four
increased complexity of debugging inheritance
The positives for using inheritance are:
cleaner design: We segregate the functionality well, so inheritance is doing the job of organizing our code for us.
more scaleable: We have already experimented with how (conceptually) we could add HTML support, and database support.
Furthermore, it gives us a chance to go ahead and rethink our design. Right now, we have something that looks like this for an interface.
my $object = new LogObject('filename',{'type' => 'regular','action' => 'append'});
$object->open();
$object->write('my god! Its full of lard');
which is to me just a tad on the non-user friendly side. Let's think of making it more along these lines:
my $object = new LogObject::Regular('> filename');
$object->write('my god! Its full of stars');
This is one step shorter, has no configuration hash in the constructor (so that we can save that for really important things, like where to output the data) and looks generally a lot easier to use.
Now let's go ahead and implement LogObject using inheritance. Looking at the above, it looks like the advantages outweigh the disadvantages, and it gives us a good chance to rethink our design.
Plus even if it turns out to not be as helpful as we had hoped, we still learn about objects and inheritance in the process. Whatever lessons we get from trying this out, we will probably learn in the first few weeks of usage. We can then take this experience and apply it to an even better solution later on. The only thing that we have to think about is how to implement this, and that is what we turn to next.
Actually implementing inheritance is an iterative process. Each person has his own method for implementation, but I generally take the following four steps in making an inheritance hierarchy:
ed- (below) what code? isn't this a new application in the abstract sense? tks, md
Lay out inheritance diagrams and at the same time look carefully at the code
Implement one (and only one) of the children. Since this child will be using functions inside the parent that have not been written yet, there is no way to test the code at this stage
Change the parent to use that child and test that change. For the sake of being able to back out, I usually name the parent code something different than it was before (LogObjectInh instead of LogObject).
Implement the rest of the children. One at a time, I test these children with the parent to make sure each of them is working correctly.
This way, I have a nice, incremental process that to work with. If I am not liking the results, I stop and reverse them, and start looking for other solutions. So let's go ahead and do this, and see where it gets us.
The first child that we should implement is LogObject::Regular since it is the simplest. It will give you a good starting point from which you can work off. In this case, the module is pretty easy:
Listing 20.4 LogObjectInh::Regular
LogObjectInh/Regular.pm
1 package LogObjectInh::Regular;
2
3 use LogObjectInh;
4 @ISA = qw (LogObjectInh);
5
6 sub write
7 {
8 my ($self, @text) = @_;
9 $self->_write("@text\n");
10 }
11 1;
Here, as we said earlier, the write function's job is merely to determine what to write. In this case, it is an easy job. It is merely the concatenation of all the text supplied by the user. The actual job is done by the central function LogObjectInh::_write(), not locally. Other than that, be careful to put the '@ISA' relationship in, and are sure to 'use LogObjectInh;'. These two steps are necessary to make the inheritance work transparently.
Since that was so easy, lets go ahead and implement the second child module LogObjectInh::Stamped. Usually I would integrate the child into the parent at this stage, but the two are so similar that it is probably a good thing to compare both of them one after another.
LogObjectInh::Stamped pretty much comes out of the same mold as our first child class; the only difference between it, and LogObjectInh::Regular is that LogObjectInh::Stamped 'pretty prints' its log text. It looks like this:
Listing 20.5 LogObjectInh::Stamped
LogObjectInh/Stamped.pm
1 package LogObjectInh::Stamped;
2
3 use LogObjectInh;
4 @ISA = qw (LogObjectInh);
5
6 sub write
7 {
8 my ($self, @text) = @_;
9 my $script = $0;
10 my $time = localtime();
11 $self->_write("$script:$time:@text\n");
12 }
13 1;
This is exactly the same as our other child class, except that we get the script name in line 9, the time in line 10, and then write it out along with the text passed from the user in line 11. We again use the central _write() function, gotten by inheritance, to do the actual printing. From the point of view of the child, we have no clue where the output is going.
Our job now is to make sure that the children work with the parent. The obvious function that needs to change is write(), for the reasons stated above. However, there are other things that need to change. The headers and the new() function need to change, since as of now they have 'typing' information whose job is being taken over by our inheritance hierarchy.*
We are also going to cheat and split the new() constructor up into two parts. This is to support a change necessary for LogObject::HTML. Usually this would be an iterative change, but our space is limited. |
Start with the headers, and the new function. They have changed quite a bit:
Listing 20.6 LogObjectInh headers and new()..
LogObjectInh.pm
1 package LogObjectInh;
2
3 use strict;
4 use Carp;
5 use FileHandle;
6 use Diff;
7 use Data::Dumper;
8
9 # my $_defaultConfig = { 'type' => 'regular', 'action' => 'append' };
10 # my $_legal = { 'action' => { 'append' => '>> ','overwrite' =>'> ' },
11 # 'type' => { 'regular' => 1, 'stamped' => 1 }
12 # };
13 #
14
15 my $_defaultConfig= {'action' => '>> ' };
16
17 sub new
18 {
19 my ($type, $filename, $config) = @_;
20 my $self = bless {}, $type;
21 $self->_new($filename, $config);
22 }
One thing to notice is that we have vastly simplified the class data ($_defaultConfig, and $_legal). Instead of four lines being devoted to configuration information, now we have one. This makes the module both cleaner and easier to understand.
Second, the new() constructor is now a simple wrapper around _new(). Why do this? The idea is that the _new() function will consist of all the 'common code': the code that every child class will use. And new() will simply be a wrapper that calls _new() - it is provided as a default constructor for the object.
If we then come across a child class which has different requirements for being constructed, we can have that class do something like:
1 package LogObjectInh::ObjectWithDifferentNew;
2 use LogObjectInh;
3 @ISA = qw (LogObjectInh);
4
5 sub new
6 {
7 my ($type, @args) = @_;
8 # do other things with the arguments
9 my $self = bless {}, $type;
10 # do even more...
11 $self->_new(@args);
12 # do even more....
13 }
The important line is line number 11. We do other things inside the child's version of the constructor, and then call the parent's constructor, so we get a consistent interface.
The next thing is the _new() function that actually does all of the work. Here it is, in Listing 20.7:
Listing 20.7 LogObjectInh::_new() private function
14 sub _new
15 {
16 my ($self, $filename, $config) = @_;
17 my (%fullconfig, $action);
18
19 confess "Config has to be a hash!\n" if
20 ($config && ref($config) ne 'HASH');
21
22 $config = $config || {};
23 if ($filename =~ m"(>*)") { $action = $1; }
24 $filename =~ s"[> ]""g;
25
26 $config = { %$_defaultConfig,
27 'action'=>$action,%$config } if ($action);
28 $config = { %$_defaultConfig, %$config } if (!$action);
29
30 $config = { 'action' => $action, %$config };
31 $self->{filename} = $filename;
32
33 %fullconfig = (%$config);
34 $self->{config} = \%fullconfig;
35 $self->_validate();
36 $self->open();
37 $self;
38 }
We are doing a couple of things here. First, we turn the function from one that takes a class name ($type) into an object method (using $self) in line 16. We do this to support variations of constructors in the children, as outlined above.
Second, take the opportunity to change the interface: lines 22-30 support the syntax:
my $file = new LogObjectInh::Regular('> filename');
Instead of looking at a hash attribute {'action' => 'append'}, simply look for '>' signs in our first argument. If we find any, then those are what we use to figure out whether or not we are going to append or not.
In fact, this uses exactly the same syntax as open(FD, "> filename"); and 'my $fh = new FileHandle("> filename"); uses so we get the added benefit that we have a consistent interface with the rest of our file code. Note also, the $self->open() in line 36. This makes it so we, the users, don't have to type this. (After all, what is the use of a closed LogFile?)
Now, all we have to do is write the functions which do the work (write, close, open):
Listing 20.8 LogObjectInh - close(), open(), write()
39 sub open
40 {
41 my ($self) = @_;
42 $self->{'closed'} = 0;
43 my ($config) = $self->{'config'};
44 my ($action, $filename) = ($config->{'action'}, $self->{'filename'});
45 my $fh=new FileHandle("$action $filename") || die "Couldn't open$filename";
46
47 $self->{filehandle} = $fh;
48 }
49
50 sub _write
51 {
52 my ($self, @text) = @_;
53 my $config = $self->{config};
54 my $fh = $self->{filehandle};
55
56 # if ($type eq 'regular') { $self->_writeRegular(@text); }
57 # elsif ($type eq 'stamped') { $self->_writeStamped(@text); }
58
59 print $fh "@text";
60 }
61
62 sub close
63 {
64 return(1) if ($self->{'closed'});
65 close($_[0]->{filehandle});
66 $self->{'closed'} = 1;
67 }
These three functions are almost exactly like they were before. open() is identical, as is close() except for one change: we have added a 'toggle' switch which prevents us from closing a file twice, which is more an enhancement than anything else. It is only _write() that is the oddball. Lines 55-56 (where we decided what write function to call based on the type) is history, and has been replaced by the simple statement in line 58.
Again, the idea is to make it so that _write (and open(), and close()) is the focus of attention rather than having the functionality spread all over the place in different subroutines. All we have to do is make sure that the object is correct. We do this with our _validate() function, which essentially remains unchanged:
Listing 20.9 LogObjectInh::_validate() private function
sub _validate
{
my ($self) = @_;
my (@errors);
my ($config, $filename) = ($self->{config}, $self->{filename});
my @keys = keys (%$config); my @legal = keys (%$_defaultConfig);
my $diff = Diff::array(\@keys, \@legal);
push(@errors, "Incorrect keys :@$diff: passed to LogObject!\n")
if (@$diff);
push(@errors, "Unwriteable log file! $filename\n")
if (!(new FileHandle(">> $filename")));
confess ("@errors") if (@errors);
}
With these changes, we are ready to implement the last of the children that we will consider - LogObject::HTML. This will be the true test of how well our inheritance scales up, since the process for making a HTML file is more difficult than simply writing out text to a filehandle.
So what should we do to actually implement a LogObjectInh::HTML class? Notice that writing out an HTML file requires two things that are not necessary part of simply writing out a 'log file'.
First, an HTML file requires a suffix attached to its name in order to be interpreted as HTML. This suffix is either '*.htm' or '*.html'. Hence, any object which outputs HTML files will have to either check to see if the user has input the correct type of file, or actually enforce this standard for the user.
Second, in order for HTML to be interpreted as HTML, it requires tags at the beginning of the file, such as '<HTML><BODY>'. It also requires tags at the end of the file to match the ones at the beginning, such as </BODY></HTML>.
ed - the following seems to be two separate things: restrict to append and keeping a record. am i confused? tks, md
Finally, let's assume that we want to restrict people to only appending to a file; we want to keep a complete record of what has transpired in any single HTML file. (This isn't a necessity; it's just kinda cool to see how it's done.)
Doing all of this will be more difficult with the LogObjectInh::HTML than the other two. Here is the Object Diagram that shows the new relationship that we are striving for:
2011.fig
Figure 20.11
LogObjectInh::HTML object diagram
Therefore, we are going to need to implement three different functions. We will need to implement a new(), again because we are going to need to add the 'beginning tags' to the HTML file. Then comes a write() and a close() function. A different close() function is necessary because we need to add the 'ending tags' to the HTML file.
Finally, there must be a DESTROY() function. Why? Well, when a user says:
my $html = new LogObjectInh::HTML('> filename');
$html->write('here!!!!');
and forgets to say "$html->close()", we need to add the 'ending tags' to the HTML file to make it a complete HTML file. The DESTROY() function does this quite well, executing the close() function for us if we forget.
Let's go ahead and implement this. First are the headers and the new() function:
LogObjectInh/HTML.pm
1 package LogObjectInh::HTML;
2
3 use LogObjectInh;
4 @ISA = qw (LogObjectInh);
5
6 use strict;
7
8 my $_defaultConfig = { 'action' => '>>' };
9 sub new
10 {
11 my ($type, $filename, $config) = @_;
12 my $self = bless {}, $type;
13
14 if ($filename !~ m"\.htm(l)?$") { $filename = "$filename.html"; }
15
16 $config = $config || {};
17 $config = { %$_defaultConfig, %$config };
18
19
20 $self->_new($filename, $config);
21 ($self->{config}->{'action'} eq '>>')
22 || die "Need to use append!\n";
23 $self->open();
24 my $time = localtime();
25 $self->_write
26 (
27 <<"EOL"
28 <HTML>
29 <HEAD>
30 <TITLE>log: $filename. Created by $0 at $time.</TITLE>
31 </HEAD>
32 <BODY>\n
33 EOL
34 );
35 $self;
36 }
Compared to the previous children this is much more elaborate. Needless to say, the other children didn't even have new() functions! We are doing four basic things here to satisfy the requirements of our problem domain:
Override the default configuration so that - by default - we append.(line 8 )
Make sure that the log has the appendix "*.htm" or "*.html" attached. (line 14 )
Make the configuration hash based on what the user has passed to us and the default for the package. (line 16-17)
Call the function $self->_new() which is translated into LogObjectInh::_new(). ( line 20 )
Write the headers to the HTML file ( lines 26-34 )
Actually, the way that we are handling the configuration files is beginning to irritate me; it is just a little unclean since it directly accesses the configuration hash, two levels deep (line 21-22). However, it is fine to make a solution like this, see how it works, and then tidy it up later. So make a mental note that we are going to return to the 'scene of the crime' when we tidy this up later.
All we need to do now is write the four last functions:
Listing 20.10 :LogObjectInh::HTML write() close() DESTROY()
37 sub write
38 {
39 my ($self, @text) = @_;
40 $self->_write("@text\n");
41 }
42
43 sub close
44 {
45 my ($self) = @_;
46 return(1) if ($self->{'closed'};
47 $self->_write(
48 <<"EOL2"
49 </BODY>
50 </HTML>
51 EOL2
52 );
53 $self->SUPER::close();
54 }
55
56 sub DESTROY { my ($self) = @_; $self->close(); }
57 1;
Here, close() is the really interesting function. It is actually doing double duty. First, we need to make sure that the file isn't closed (line 46) returning if it is, and then we need to write out the information necessary to make the HTML file complete.
As you can see from this discussion, inheritance requires a lot of thought, design, discussion, and give-and-take. It is, one might say, '90% thought and 10% action'. And even after all of this thought and action, I'm still not satisfied with the result. For one, what happens if we want to print out a time-stamped entry into a HTML file? Do we say something like:
print LogObjectInh::HTML::Stamped;
making another layer of inherited objects? I would be more inclined to perhaps extend the objects that we have, maybe getting rid of LogObjectInh::Stamped, and concentrate on different types of items (like a 'Regular' item, or a 'Stamped' item). We shall look at these things next chapter. Next, however, let's consider an example where we know that we are going to use inheritance, and simply do it.
The standard distribution contains quite a few modules that are designed to do inheritance. Autoloader, Exporter, DynaLoader, and all of the Tie modules (Tie::Hash, Tie::Scalar) are specifically for the purpose of inheritance. Here we will be using an example of a common way to use one of these modules to lessen the pain of 'tie'ing a hash.
Everybody has gone through a very common pain before. For some reason, the program does not work, they don't know why it doesn't work, and they spend a lot of time figuring out why it doesn't work. After hours of searching, they look at line #257, and find out that when they said:
257 if ($a{'value'} = 1)
258 {
259 doThis();
260 }
that they have only put one $#@% equals sign when it should have been two.*
Although, if we were using '-w' in the first place, this mistake would never happen! |
What would be helpful here (and in other places) would be a way of putting a trace on a variable. When we say the following statement:
$a{'value'} = 2;
we would want to have our program print out:
%a - changed the key value to 2 at script.p line 257
in order to show that script.p has in fact changed the value of the key value to 2.
The easiest way to do this is via 'tie'ing. 'tie'ing lets you redefine what happens when a subroutine is stored, fetched, and so forth. However, as you may recall from chapter 16 (when we implemented a tied hash to count accesses to the hash) that 'tie'ing can be a lot of work. When we 'tie' something, we need to define eight different subroutines:
TIEHASH
STORE
FETCH
FIRSTKEY
NEXTKEY
EXISTS
DELETE
CLEAR
DESTROY
Consider which of these eight subroutines are actually necessary for us to implement the WatchHash as implemented. Do we need a constructor to implement the functionality? No. STORE function? Yes - we need to print out something every single time we store something. FETCH? No. FIRSTKEY? No. NEXTKEY? No. EXISTS? No. DELETE? Yes - we need to print out a message to the effect that something was destroyed. CLEAR? Actually, no - if we program it right. CLEAR can be defined in terms of DELETE, and in terms of FIRSTKEY and NEXTKEY, because all it is a bunch of deletes in a row. DESTROY? DESTROY could be defined in terms of CLEAR, so no.
This limits the functions that we need to define to STORE and DELETE. And although we won't actually need a constructor, we will define one anyway; as you shall see, it will make the package more user friendly. Hence, we come up with a preliminary object diagram, something that looks like Figure 20.12:
2012.fig
Figure 20.12
Preliminary inheritance diagram
All we need to do is come up with something to inherit from. We look in the standard distribution, and come up with Tie::Hash. Opening it up, we find:
1 package Tie::StdHash::Encap;
2
3 @ISA = qw(Tie::Hash);
4 # The Tie::StdHash:Encap package implements standard perl hash behaviour, just
5 # like Tie::StdHash, but with a difference. All the data for the hash is put
6 # inside a '_tied' attribute. This makes it so you don't have key collisions
7 # inside your hash.
8
9
10 sub TIEHASH { bless {}, $_[0] }
11 sub STORE { $_[0]->{'_tied'}{$_[1]} = $_[2] }
12 sub FETCH { $_[0]->{'_tied'}{$_[1]} }
13 sub FIRSTKEY {my $a=scalar keys %{$_[0]->{'_tied'}}; each %{$_[0]->{'_tied'}} }
14 sub NEXTKEY { each %{$_[0]->{'_tied'}} }
15 sub EXISTS { exists $_[0]->{'_tied'}{$_[1]} }
15 sub DELETE { delete $_[0]->{'_tied'}{$_[1]} }
This is exactly what we need.. it fills in almost all the holes. It 'ties' the hash so that if you say:
my $obj = tie (%a, 'Tie::StdHash::Encap');
then $obj will return the object that is 'tied' (ie: connected to) '%a', and such that:
$a{'array'} = 1;
actually calls:
$obj->STORE('array','1');
which in turn calls is translated into:
$obj->{'_tied'}{'array'} = 1;
All of the hash is designed this way, so that you get 'default' hash behavior, yet can override certain methods by inheritance. The only methods left that we don't have defined here are CLEAR and DESTROY. To get these, see that Tie::StdHash::Encap actually inherits off of Tie::Hash. When we look at that part of the file, we see:
1 sub CLEAR {
2 my $self = shift;
3 my $key = $self->FIRSTKEY(@_);
4 my @keys;
5
6 while (defined $key) {
7 push @keys, $key;
8 $key = $self->NEXTKEY(@_, $key);
9 }
10 foreach $key (@keys) {
11 $self->DELETE(@_, $key);
12 }
13 }
Aha! We find a CLEAR function, but we don't find a DESTROY function. So our expanded inheritance hierarchy looks something like Figure 20.13:
2013.fig
Figure 20.13
Improved inheritance diagram.
We therefore just need to define TIEHASH, STORE, and DESTROY. That follows next.
1 package Tie::WarnHash;
2
3 use Tie::Hash;
4 use Carp;
5
6 @ISA = qw (Tie::StdHash::Encap);
7 use strict;
8
9 sub TIEHASH
10 {
11 my ($type, $name) = @_;
12 bless { 'name' => $name, '_tied' => {} }, $type;
13
14 }
15
16 sub STORE
17 {
18 $_[0]->{'_tied'}->{$_[1]} = $_[2];
19 print "\%$_[0]->{'name'} - element '$_[1]' changed to "
20 ,"$_[2]",Carp::longmess();
21 }
22
23 sub DELETE
24 {
25 delete $_[0]->{'_tied'}->{$_[1]};
26 print "\%$_[0]->{'name'} - element '$_[1]' deleted ",Carp::longmess();
27 }
28
29 sub DESTROY
30 {
31 $_[0]->CLEAR();
32 }
33 1;
That's about it.. TIEHASH associates a name with the hash that we tie, so we are going to have to create our hash elements by saying:
tie(%a, 'Tie::WarnHash', 'a');
STORE then does all assignments of hash keys, and at the same time prints out something in the format:
%a - element 'length' changed to 2 at script.p line 257
And finally DELETE and DESTROY print out which element is being deleted, something like:
%a - element 'length' deleted
By using inheritance instead of implementing the methods themselves, this solution has less lines of code associated with it, fewer subroutines to maintain, and fewer idiosyncracies.
In fact, since that was so easy, let's make an additional hash which does just a little bit more than the one above. After this example is done, you are going to want to check out the public domain package Tie::Watch, which does this monitoring for not only hashes, but also arrays and scalars.
Anyway, we have done all the hard work in the example above by thinking about how to do the inheritance, so now it is just a question of plug and play if we want to projects which are similar. First, we think of how we can improve the above. One way becomes clear pretty soon. Sometimes we don't want to monitor all of the hash. Say we wanted to just monitor the hash keys that start with 'h'. Then, we would want to be able to say:
tie ( %hash, 'Tie::WarnHashNew', 'hash',
sub {
my ($key, $value) = @_;
return(1) if ($key =~ m"^h");
return(0);
}
);
Here, we have added a callback to the end of Tie::WarnHash. This (optional) callback filters out which keys we want to monitor, and which keys we do not. In this case, we say we want to monitor it if ($key =~ m"^h") (starts with an h) and we return a 1. If it doesn't start with an h, we fall through and return 0.
These return values are used by Tie::WarnHashNew to do this selective filtering. In general, the logic is going to look like:
my $cb = $self->{cb};
if (&$cb($key, $value)) { print "key changed!\n"; }
where $cb is the callback that we entered into the tie function, and $key and $value are the items we are putting into the hash. The actual code gets a little rougher. It's listed below:
1 package Tie::WarnHashNew;
2
3 use Tie::Hash;
4 use Carp;
5
6 @ISA = qw (Tie::StdHash::Encap);
7 use strict;
8
9 sub TIEHASH
10 {
11 my ($type, $name, $cb) = @_;
12 confess "$cb needs to be a code reference\n"
13 if (defined($cb) && !ref($cb) eq 'CODE');
14
15 bless { 'name' => $name, 'cb' => $cb }, $type;
16 }
First, do the constructor. $cb holds the callback that we discussed previously. We pass that as the last argument, and it in turn gets put into the object (in line 15) for storage.
17 sub STORE
18 {
19 $_[0]->{'_tied'}->{$_[1]} = $_[2];
20 if (($_[0]->{'cb'}) && (&{$_[0]->{'cb'}}($_[1], $_[2])))
21 {
22 print "\%$_[0]->{'name'} - element $_[1] changed to".
23 " $_[2]", Carp::longmess();
24 }
25 }
26
27 sub DELETE
28 {
29 delete $_[0]->{'_tied'}->{$_[1]};
30 if (($_[0]->{'cb'}) && (&{$_[0]->{'cb'}}($_[1],'')))
31 {
32 print "\%$_[0]->{'name'} - element $_[1] deleted ",Carp::longmess();
33 }
34 }
35
36 sub DESTROY
37 {
38 $_[0]->CLEAR();
39 }
40 1;
In turn, STORE and DELETE take the callback, to see if it is defined. If it is defined, then we try it (lines 20 and 30 ). If the true value is returned from the callback, we go on to print out that the element has changed, as we did before. If it is one of those hash keys we can ignore (and 0 or '' or undef) is returned, then we don't print it out.
Perl has quite a few built-in packages you can inherit off of and you are probably going to want to check them out in the standard distribution. Packages such as Autoloader can make your Perl scripts faster by splitting them up into chunks. Dynaloader is pretty much essential for linking C and C++ into Perl. Exporter we have already seen, which lets you move functions between packages so that you don't need to fully qualify them.
The main benefit behind using these modules is that the work has already been done for you; by interfacing with them, you can learn better inheritance practices yourself.
As you can probably see, inheritance is not a technique to be taken lightly. It involves splitting code up into multiple files, which always adds complexity, and sometimes leads to inflexibility.
Hence, inheritance is a tricky thing to do well - and even after you are done with it, it is a hard thing to do in such a way that you aren't 'tweaking' your code for a long time to come. You should always look at an inheritance plan very closely; and judge the solution that you come up against the other major alternative; the one we will talk about next, which is layering.
![]() ![]() |
![]() |
![]() ![]() |
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.