Polymorphism
Overview
In this chapter, you
will learn about an esoteric but important subject called polymorphism. If
you use object-oriented programming (OOP) but skip polymorphism, you miss
out on a key tool that yields robust, flexible architectures.
You don't need to
understand polymorphism or much about objects to program in BCB. However,
if you want to be an expert BCB programmer and want to create components,
you should master this material.
In this chapter, I take
you through several fairly simple programs designed to illustrate key
aspects of polymorphism. By the time you're done, you should understand why
the simple ability to assign a child object to a parent object is one of
the most important features of the entire C++ language.
Polymorphism
from
20,000 Feet
Polymorphism can be
confusing even to experienced OOP programmers. My explanation of this
subject starts with a high-level overview of the theoretical issues that
lie at the core of this field of study. The text then moves on to show some
real-world examples and finally comes back to a second take on the
high-level overview. Don't panic if you don't understand the information in
the next few paragraphs right away. I cover this material several times in
several different ways, and by the time you finish this chapter, all should
be clear to you.
Polymorphism is a
technique that allows you to set a parent object equal to one or more of
its child objects:
Parent = Child;
The interesting thing
about this technique is that, after the assignment, the parent acts in
different ways, depending on the traits of the child that is currently
assigned to it. One object, the parent, therefore acts in many different
ways--hence the name "polymorphism," which translates literally
from its Greek roots to an English phrase similar to "many
shapes."
NOTE: I find the words "assign" and
"equal" extremely tricky. If I'm speaking off the cuff and use
the word "assign" in three consecutive sentences, I will trip
over myself for sure! Because this seemingly simple subject is so tricky,
I'm going to take a moment to lay out some rules that you can reference
while you're reading this chapter.
Consider the following code fragment:
Parent = Child;
In this simple
statement, the child is assigned to its parent. The parent is not assigned
to its child. You could easily argue that this definition could be
reversed, but I do not take that approach in this chapter.
When referencing the preceding code fragment, I also say that the parent is
set equal to its child. The child is not set equal to its parent. Once
again, you can argue that this definition could be reversed, or that the
action is reflexive. Throughout this chapter, however, I consistently state
that the preceding code sets a parent equal to its child, and not a child equal to its parent.
Because these seemingly simple English phrases are so confusing, at least
to me, I try to illustrate exactly what I mean as often as possible. That
is, I say the statement in English, insert a colon, and then follow the
English phrase with code that provides an example of the meaning of my
statement. I find the English phrases ambiguous and confusing, but the code
is starkly clear. Don't waste too much time parsing the English;
concentrate on the code!
The classic example of
polymorphism is a series of objects, all of which do the following:
- Descend from one base class
- Respond to a virtual command called Draw
- Produce different outcomes
For instance, you might
have four objects called TRectangle, TEllipse, TCircle, and TSquare. Suppose that each of these
objects is a descendant of a base class called TShape, and that TShape has a virtual method called Draw. (This hypothetical TShape object is not necessarily the one that appears on BCB's component palette.) All the children of TShape also have Draw methods, but one draws a circle; one, a square; the next, a
rectangle; and the last, an ellipse. You could then assign any of these
objects to a variable of type TShape, and that TShape variable would act differently after each assignment. That is, the
object of type TShape would draw a square if set
equal to a TSquare object:
TShape *Shape = Square;
Shape->Draw(); // Draws a square.
It would draw an
ellipse if set equal to a TEllipse object:
TShape *Shape = Ellipse;
Shape->Draw(); // Draws an ellipse;
and so on.
Notice that in both
these cases the object that does the drawing is of type TShape. In both cases, the same command of TShape is called. You would therefore
expect that a call to Shape->Draw() would always produce the same
results. Polymorphism puts the lie to this logic. It allows a method of an
object to act in many different ways. One object, called Shape, "morphs" from one
set of functionality to another, depending on the context of the call. That's
polymorphism.
From a conceptual point
of view, this description does much to explain what polymorphism is all
about. However, I still need to explain one key aspect.
According to the rules
of OOP, you can pass all these objects to a single function that takes an
object of type TShape as a parameter. That single
function can call the Draw method of each of these objects, and each one will behave
differently:
void DrawIt(TShape *Shape)
{
Shape->Draw(); // TShape draws different shapes depending on "assignment"
}
void DoSomething()
{
TRectangle *Rectangle = new TRectangle();
TSquare *Square = new TSquare();
TEllipse *Ellipse = new TEllipse();
DrawIt(Rectangle); // Draws a rectangle
DrawIt(Square); // Draws a square
DrawIt(Ellipse); // Draws an ellipse
delete Rectangle;
delete Square;
delete Ellipse;
}
When you pass an object
of type TRectangle to a function that takes a TShape as a parameter, you are accessing the TRectangle object through an object of
type TShape. Or, if you look at the act of passing a
parameter from a slightly different angle, you're actually assigning a
variable of type TRectangle to a variable of type TShape:
Shape = Rectangle;
This assignment is the
actual hub around which polymorphism revolves. Because this assignment is
legal, you can use an object of a single type yet have it behave in many
different ways: Once again, that's polymorphism.
NOTE: Grasping the idea behind polymorphism is a
bit like grasping the idea behind pointers. Many programmers have a hard
time understanding pointers when they first see them. Then, after a time,
manipulating pointers becomes as natural as tying their shoes. It no longer
requires thought. The same is true of polymorphism. The concept can seem
quite opaque at first to many programmers, and then they have a little
epiphany. Then wham! Suddenly their coding ability makes the same kind of
huge jump forward that occurred when they learned about pointers.
Polymorphism has the same relationship to OOP that pointers have to C or
C++. People might say they are C++ programmers, but if they don't
understand pointers, they are missing out on at least half of what the
language has to offer. The same is true of OOP and polymorphism. Many
programmers claim to understand OOP, but if they don't yet see what
polymorphism is all about, they are missing at least half the power of
technology.
To fully understand the
preceding few paragraphs, you have to grasp that children of an object are
assignment-compatible with their parents. Consider the following
declaration:
class TParent
{
}
class TChild: public TParent
{
}
Given these declarations, the following is legal:
{
TParent *Parent;
TChild *Child;
Parent = Child;
}
But this syntax is
flagged as a type mismatch; that is, the compiler will complain that it
cannot convert a variable of type TParent* to TChild*:
{
TParent *Parent;
TChild *Child;
Child = Parent;
}
You can't set a child
equal to a parent because the child is larger than its parent--that is, it
has more methods or fields--and therefore all its fields and methods will
not be filled out by the assignment. All other things being equal, you can
build a two-story building out of the pieces meant for a three-story
building; but you can't build a three-story building out of the pieces
meant for a two-story building.
Consider the following
hierarchy:
class TParent: public TObject
{
virtual void Draw();
};
class TChild: public TParent
{
virtual void Draw();
virtual void ShowHierarchy();
};
The issue here is that
setting a child equal to a parent is not safe:
Child = Parent; // Don't do this!
If it were allowed,
writing the following would be a disaster:
Child->ShowHierarchy();
In this hypothetical
world, the call might compile, but it would fail at runtime because Parent has no ShowHierarchy method; therefore, it could not provide a valid address for the
function at the time of the assignment operation. I will return to this
subject in the next section of this chapter.
If you set a parent
equal to a child, all the features of the parent will be filled out
properly:
Parent = Child;
That is, all the
functions of TParent are part of TChild, so you can assign one to the other without fear of something going
wrong. The methods that are not part of TParent are ignored.
NOTE: When you're thinking about this material,
you need to be sure you are reading statements about assigning parents to
children correctly. Even if I manage to straighten out my grammar, nothing
in the English language makes totally clear which item in an assignment
statement is on the left and which is on the right. I could use the terms lvalue and rvalue in this case, except that they
don't quite fit. However, if you take this description as an analogy, you
can consider a child to be an rvalue and a parent to be an lvalue. You can set a parent equal to a child:
Parent = Child;
but you can't set a
child equal to a parent:
Child = Parent;
You literally can't do
this. You get a type mismatch. The compiler replies "Cannot convert TParent* to TChild." In this sense, Child becomes like an rvalue in this one case. Even though assigning values to Child is ultimately possible, you
can't assign a Parent to it. In this one case, it might as well
be an rvalue.
To see this process in action, start a new project in the IDE, drop a
button on the form, and write the following code:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
TForm *Form;
TComponent *Component;
Component = Form;
Form = Component;
}
The compiler will allow
the first assignment but object to the second. This objection occurs
because TForm is a descendant of TComponent.
Another
View of Polymorphism
Here's another way of
looking at polymorphism. A base class defines a certain number of functions
that are inherited by all its descendants. If you assign a variable of the
child type to one of its parents, all the parent's methods are guaranteed
to be filled out with valid addresses. The issue here is that the child, by
the very fact of its being a descendant object, must have valid addresses
for all the methods used in its parent's virtual method table (VMT). As a
result, you can call one of these methods and watch as the child's
functions get called. However, you cannot call one of the child's methods
that does not also belong to the parent. The
parent doesn't know about those methods, so the compiler won't let you call
them. In other words, the parent may be able to call some of the child's
functions, but it is still a variable of the parent type.
NOTE: A virtual method table, or VMT, is a table
maintained in memory by the compiler; it contains a list of all the
pointers to the virtual methods hosted by an object. If you have an object
that is descended from TObject, the VMT for that object will
contain all the virtual methods of that object, plus the virtual methods of TObject.
To help you understand
this arrangement, picture the VMT for TParent. It has a pointer to a Draw method in it, but no pointer to
a ShowHierarchy method. Therefore, an attempt to call its ShowHierarchy method would fail, as would an attempt to fill out a TChild's ShowHierarchy through an assignment with a TParent object.
Consider this schema:
Simplified VMT for Parent Simplified VMT for Child
StartTable StartTable
Draw ------------------------------ Draw
EndTable ShowHierarchy
EndTable
This schema shows a
parent being set equal to a child. As you can see, the address of the Draw method for the parent is
assigned to the address for the Draw method for the child. No ShowHierarchy method exists in the parent, so it is ignored.
Here's what happens if
you try to set the child equal to the parent:
Simplified VMT for Child Simplified VMT for Parent
StartTable StartTable
Draw ------------------------------ Draw
ShowHierarchy -------------------- ????
EndTable EndTable
As you can clearly see,
no method pointer in the parent table can be assigned to the ShowHierarchy method of the child table. Therefore, it is left blank, which means
a call to the ShowHierarchy method of the child almost
certainly fails.
Because the ShowHierarchy method cannot be filled out properly, assigning TParent to TChild is illegal. In other words, it's not just
a legal technicality, it's a literal impossibility. You literally can't
successfully assign a parent to a child. You can, however, assign a child
to a parent, and it's this assignment that lies at the heart of
polymorphism. For the sake of clarity, let me spell it out. Here is the
legal assignment:
Parent = Child;
Here is the illegal
assignment:
Child = Parent;
Needless to say, I
wouldn't be placing so much emphasis on this subject if it were not vitally
important. You simply won't really understand OOP unless you grasp what has
been said in the last few pages.
Virtual
Methods and Polymorphism
If some of the methods
in a base class are defined as virtual, each of the descendants can redefine
the implementation of these methods. The key elements that define a typical
case of polymorphism are a base class and the descendants that inherit a
base class's methods. In particular, the fanciest type of polymorphism
involves virtual methods that are inherited from a base class.
A classic example of
polymorphism is evident if you examine the BCB VCL. All these objects are
descendants of a single base class called TObject; therefore, they all inherit a
virtual destructor. As a result, you can pass all the many hundreds of BCB
classes to a routine that takes a parameter of the same type as their base
class:
void FreeAllClasses(TObject O)
{
delete O;
}
You can pass any VCL
object to the FreeAllClasses function, and the object will
be properly destroyed. The VirtualMethodTest program, shown in Listings 21.1 and 21.2, shows how this process works. You
can also find this program on the CD that comes with this book.
Listing 21.1. The header for the VirtualMethodTest program.
///////////////////////////////////////
// Main.h
// VirtualMethodTest
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
class TParent
{
public:
TParent() {}
virtual __fastcall ~TParent()
{ ShowMessage("TParent Destructor"); }
virtual void Draw() {}
};
class TChild: public TParent
{
public:
TChild(): TParent() {}
virtual __fastcall ~TChild()
{ ShowMessage("TChild Destructor"); }
virtual void Draw() {}
virtual void ShowHierarchy() {}
};
class TForm1 : public TForm
{
__published:
TButton *RunTestBtn;
void __fastcall RunTestBtnClick(TObject *Sender);
private:
void FreeObjects(TParent *Parent);
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing
21.2. The main body of the main form for the VirtualMethodTest program.
///////////////////////////////////////
// Main.cpp
// VirtualMethodTest
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void TForm1::FreeObjects(TParent *Parent)
{
delete Parent;
}
void __fastcall TForm1::RunTestBtnClick(TObject *Sender)
{
TParent *Parent = new TParent();
TChild *Child = new TChild();
FreeObjects(Parent);
FreeObjects(Child);
}
From a visual point of
view, this program is very unexciting, as you can see from a glance at
Figure 21.1.
FIGURE
21.1. The main form of the VirtualMethodTest program.
To put this program to
work, you need to change the destructors for the TParent and TChild objects so that they are
sometimes declared as virtual and sometimes declared as standard,
non-virtual methods:
class TParent
{
public:
TParent() {}
virtual __fastcall ~TParent() { ShowMessage("TParent Destructor"); }
virtual void Draw() {}
};
class TParent
{
public:
TParent() {}
__fastcall ~TParent() { ShowMessage("TParent Destructor"); }
virtual void Draw() {}
};
In the first case, the
destructor is declared as virtual, and in the second, it is not declared as
virtual. Notice that I do not make TParent descend from TObject, because all VCL classes must have virtual destructors since TObject itself declares its destructor as virtual.
When you do not declare
the destructors as virtual, and you pass the objects to the FreeObjects method, the destructor for TParent is called:
void TForm1::FreeObjects(TParent *Parent)
{
delete Parent;
}
If you declare both
destructors as virtual, when you pass them to FreeObjects, the destructor for TChild will be called, even though it is being called off an instance of TParent.
If what I'm saying is
not clear, start C++Builder and load the VirtualMethodTest program so that you can watch this process
in action. Run the program with the destructor declared as virtual and then
run it again without the virtual destructor declaration. This material is
extremely important. You simply cannot understand what OOP is all about if
you do not understand polymorphism.
Remember that passing a
parameter to FreeAllClasses is analogous to an assignment
statement. In effect, you are writing
Parent = Parent;
Parent = Child;
You could not, however,
write this:
void TForm1::FreeObjects(TChild *Child)
{
delete Child;
}
void __fastcall TForm1::Button1Click(TObject *Sender)
{
TParent *Parent = new TParent();
TChild *Child = new TChild();
FreeObjects(Parent);
FreeObjects(Child);
}
The issue, of course,
is that by passing Parent to FreeAllClasses, you are, in effect, asking BCB
to make the following assignment:
Child = Parent;
BCB is kind enough to
tell you that making this assignment is bad practice.
Every object in the VCL
can use polymorphism to some degree because they all inherit methods from TObject. In particular, they all inherit a virtual destructor. Without this
virtual destructor, the whole system would collapse. In particular, the
component array hosted by each form is iterated at closing time, and the
destructor of each object is called. Doing so would not be possible if the
destructors of these objects were not declared virtual. Or, rather, doing
so would be possible, but the objects themselves would not be properly
destroyed. The reason for the failure is illustrated clearly in the VirtualMethodTest program.
To help illustrate this
point, I will implement the classic shape demo example described earlier in
this chapter. This program creates a parent object called TMyShape and two child objects called TRectangle and TCircle. All three of these objects
have virtual methods called DrawShape. You can pass a single instance
of this object to a method declared as follows:
void TForm1::CallDrawShape(TMyShape *Shape)
{
Shape->DrawShape();
}
If you pass in an
instance of TMyShape, then the TMyShape::DrawShape method will be called, which is what you
would expect. However, if you pass in either a TRectangle or TCircle, then the respective DrawShape methods of each object will be called, as shown in Figure 21.2.
FIGURE
21.2. The ClassicShapeDemo shows how one object can behave in three different ways: on the left it
draws a star, in the center a circle, and on the right a square.
The TForm1::CallDrawShape method works with a variable of
type TMyShape. It has only one kind of object in it. It
works only with variables of type TMyShape. And yet if you pass both a TRectangle object and a TMyShape object into it, one instance
will call the DrawShape method of TRectangle and the other will call the DrawShape method of TMyShape. This example illustrates polymorphism in action. The concept here
is so important that I have placed this example on disk in a directory
called ClassicShapeDemo and show it in Listings 21.3
and 21.4.
Listing 21.3. The header for ClassicShapeDemo shows how polymorphism looks in
action.
///////////////////////////////////////
// Main.h
// Classic Shape Demo
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
class TMyShape: public TCustomControl
{
public:
virtual __fastcall TMyShape(TComponent *AOwner): TCustomControl(AOwner) {}
virtual __fastcall TMyShape(TComponent *AOwner, int X, int Y)
: TCustomControl(AOwner) { Width = 50; Height = 50; Left = X; Top = Y; }
virtual void DrawShape()
{ Canvas->Brush->Style = bsClear; Canvas->TextOut(0, 0, "*"); }
};
class TCircle: public TMyShape
{
public:
virtual __fastcall TCircle(TComponent *AOwner): TMyShape(AOwner) {}
virtual __fastcall TCircle(TComponent *AOwner, int X, int Y)
: TMyShape(AOwner) { Width = 50; Height = 50; Left = X; Top = Y; }
virtual void DrawShape()
{ Canvas->Brush->Color = clBlue; Canvas->Ellipse(0, 0, Width, Height); }
};
class TRectangle: public TMyShape
{
public:
virtual __fastcall TRectangle(TComponent *AOwner): TMyShape(AOwner) {}
virtual __fastcall TRectangle(TComponent *AOwner, int X, int Y)
: TMyShape(AOwner) { Width = 50; Height = 50; Left = X; Top = Y; }
virtual void DrawShape()
{ Canvas->Brush->Color = clLime; Canvas->Rectangle(0, 0, Width, Height); }
};
class TForm1 : public TForm
{
__published:
TButton *DrawShapesBtn;
TPanel *Panel1;
void __fastcall DrawShapesBtnClick(TObject *Sender);
private:
void TForm1::DrawShape(TMyShape *Shape);
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing
21.4. The main form for the ClassicShapeDemo program.
///////////////////////////////////////
// Main.cpp
// Classic Shape Demo
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void TForm1::DrawShape(TMyShape *Shape)
{
Shape->DrawShape();
}
void __fastcall TForm1::DrawShapesBtnClick(TObject *Sender)
{
TMyShape *Shape = new TMyShape(Panel1, 10, 10);
Shape->Parent = Panel1;
TCircle *Circle = new TCircle(Panel1, 50, 10);
Circle->Parent = Panel1;
TRectangle *Rectangle = new TRectangle(Panel1, 140, 10);
Rectangle->Parent = Panel1;
DrawShape(Shape);
DrawShape(Circle);
DrawShape(Rectangle);
}
Most of the complexity
of this example is a result of the default behavior of VCL objects. Notice
that TMyShape descends from TCustomControl. I chose this lineage because TCustomControl has a Canvas object ready for use by its consumers. I
pay a price for this functionality, because I must create a default
constructor with the following signature if I want to suppress valid
compiler warnings:
virtual __fastcall TMyShape(TComponent *AOwner): TCustomControl(AOwner) {}
After I have declared
this constructor, I can create a second constructor that suits my own
needs:
virtual __fastcall TMyShape(TComponent *AOwner, int X, int Y)
: TCustomControl(AOwner) { Width = 50; Height = 50; Left = X; Top = Y; }
Notice that this
constructor gives the object a default height and width, and also assigns
values to its Left and Top fields. All these fields are inherited from TCustomControl and must be filled out properly
if you want to draw the object on the screen.
NOTE: TCustomControl is one of the objects from
which you can make descendants if you want to create a component. In fact,
both of the examples shown here are valid components that could be placed
on the Component Palette with only a little more work. However, I will save
a complete explanation of TCustomControl for the next chapter of this
book. For now, all you need to know is that these fairly sophisticated
objects come replete with Left, Top, Width, and Height properties, as well as an easy-to-use Canvas object.
When you create an
instance of a VCL control, you need to give it both an owner and a parent:
TCircle *Circle = new TCircle(Panel1, 50, 10);
Circle->Parent = Panel1;
This code assigns Panel1 as both the parent and owner of TCircle. Panel1 is just a standard panel to
serve as the drawing surface on which the program paints its output.
After you declare
constructor objects of type TMyShape, TRectangle, and TCircle, the final step is to pass each
of these objects to the CallDrawShape method:
CallDrawShape(Shape);
CallDrawShape(Circle);
CallDrawShape(Rectangle);
When you look at it
from this end, the fact that CallDrawShape would produce different results
depending on the type of object you pass into it makes sense. However, if you
look at the CallDrawShape method itself, the fact that
you can pass objects of different types to it is not at all obvious:
void TForm1::CallDrawShape(TMyShape *Shape)
{
Shape->DrawShape();
}
The odd thing about
polymorphism is that you can make the assignment of an object of type TRectangle to an object of type TMyShape. Once you understand that you
can do that, everything else falls out fairly naturally.
If you have any
questions about what's going on in this code, run this program. Step
through it with the debugger. Do whatever is necessary to make sense of
this very important code. The key point to grasp is that one variable,
called Shape,
which is of type TMyShape, behaves in three different
ways. It's polymorphic--one variable acting in different ways.
Polymorphism
in the VCL
One place where BCB
uses polymorphism heavily is with the TField objects assigned to the Fields array in a TTable object. The objects in the Fields array are of type TField, but they behave as if they are of type TStringField, TIntegerField, TFloatField, and so on. The issue, of
course, is that variables of type TStringField and TIntegerField can all be assigned to the Fields array because you can assign a
child object to a variable of its parent type:
Parent = Child;
Here is the declaration
of the Fields property from the TDataSet declaration in DB.hpp:
__property TField* Fields[int Index] = {read=GetField, write=SetField};
Clearly, this array of
objects is of type TField. Suppose, however, that you
execute the following method in a simple program containing a TTable object that's pointed to the Biolife table:
void __fastcall TForm1::bViewFieldTypesClick(TObject *Sender)
{
int i;
for (i = 0; i < Table1->FieldCount; i++)
{
ListBox1->Items->Add(Table1->Fields[i]->ClassName());
}
}
According to the
declaration of the Fields object, the type of each of the members of
the Fields array should be TField. However, the following is what
actually gets printed in the program's list box, as you can see in Figure
21.3.
TFloatField
TStringField
TStringField
TStringField
TFloatField
TFloatField
TMemoField
TGraphicField
Clearly, this isn't
really an array of TField objects at all. So what's going
on anyway?
FIGURE
21.3. The PolyFields program shows how polymorphism underlies key parts of the VCL.
Well, by now the answer
should be clear. Somewhere internally, BCB is assigning TStringField, TFloatField, TMemoField, and TGraphicField objects to the members of the TField array. Why is this legal? Because setting a parent object equal to
a child object is always legal! In essence, the following happens:
{
TField *Field;
TStringField *StringField;
Field = StringField; // This is legal!
}
Here is the hierarchy
of some of the key TField descendants:
-TField
-TStringField
-TNumericField
-TSmallIntField
-TWordField
-TAutoIncField
-TFloatField
-TCurrencyField
-TBlobField
-TMemoField
-TGraphicField
Given this hierarchy,
assigning a TStringField or TFloatField object to a variable of type TField is always legal:
{
TField *Field[3];
TStringField *StringField;
TFloatField *FloatField;
TGraphicField *GraphicField;
Fields[0] = StringField; // legal!
Fields[1] = FloatField; // legal!
Fields[2] = GraphicField; // legal!
};
This point is so
important that I'm going to include the source code for a program called PolyFlds.mak that demonstrates the issue discussed in this section. Listings
21.5 and 21.6 show this source code.
Listing 21.5. The PolyFields program shows how BCB makes practical use of polymorphism.
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\DBGrids.hpp>
#include "Grids.hpp"
#include <vcl\DBTables.hpp>
#include <vcl\DB.hpp>
class TForm1 : public TForm
{
__published:
TDBGrid *DBGrid1;
TButton *bViewFieldTypes;
TListBox *ListBox1;
TTable *Table1;
TDataSource *DataSource1;
void __fastcall bViewFieldTypesClick(TObject *Sender);
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing
21.6. The main form for the PolyFields program.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma link "Grids"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::bViewFieldTypesClick(TObject *Sender)
{
int i;
for (i = 0; i < Table1->FieldCount; i++)
{
ListBox1->Items->Add(Table1->Fields[i]->ClassName());
}
}
This sample program
makes clear how important polymorphism is to BCB. The Fields array of TTable is one of the key elements in all of BCB. And what lies at the
heart of the whole thing? Polymorphism!
Another important point
to grasp here is that very little about BCB is mysterious. If you work with
Visual Basic or PowerBuilder, the only explanation for much of the syntax
is simply "because." Why does it work that way? Because! With
BCB, the explanation is never like that. You can find an explanation for
everything in BCB, and you can build any part of the VCL or the environment
yourself by using BCB.
Remember: BCB is built
into the VCL. You'll find no mysteries here. If you know the language well
enough, even the most esoteric parts of the VCL will make sense to you. How
can the Fields array be so powerful? How does it do those things? Well, now you know the
answer: It's polymorphic!
Polymorphism
Encapsulated (Review of Main Points)
I have, at long last,
said all I want to say about polymorphism. The key points to remember are these:
- You can set a parent equal to a child
object, but you can't set a child equal to a parent object:
Parent = Child; // Little set equal to big: OK
Child = Parent; // Big set equal to little: Doesn't work
Setting a parent equal to a child object is
what makes polymorphism tick.
- The defining elements in polymorphism
are the methods of the parent object, particularly those methods that
are declared virtual. Even if you assign a child to
a parent object
Parent = Child;
the parent can't call methods of
the child that are not also visible in the parent's class declaration. For
example, if you assign a TForm instance to a TObject instance, you can't access the form's Caption property from the TObject instance without a typecast.
The point is that you
can take a whole slew of hierarchically arranged objects, assign each of
them to their parent, call a virtual method belonging to the parent, and
watch them all behave in different ways. Polymorphism!
For some readers, I'm
sure this information is old hat. Other readers might be new to the subject
but have grasped it completely from the descriptions given already. However,
most readers probably still have some questions lingering in the backs of
their minds.
If you want, just
reread the preceding sections as often as necessary. I know from personal
experience that as many as three out of four object-oriented programmers
don't really understand polymorphism. The subject, however, is not that
complicated. Concentrate on these two sentences:
Polymorphism allows you
to set one variable of type TParent equal to a
series of child objects. When you call certain virtual methods of that
parent, it will behave in different ways, depending on the traits of the
currently selected child.
That's a mouthful, but
the concepts set forth are not impossible to comprehend.
So far, all the
examples of polymorphism are very stripped-down programs that contain only
code directly relevant to the subject at hand. However, the real power of
polymorphism won't become clear until you work with a larger program that
really taps into the power of OOP. Unfortunately, the example I have for
this kind of programming really cries out to be implemented as a set of
polymorphic components. Because I have not yet described how to create
components, I will have to delay showing you this program until Chapter 23,
"Creating Components from Scratch." In that chapter, you will
find a sample program called WareHouse that
clearly illustrates what polymorphism looks like in a large, and fairly
practical, program.
Summary
In this chapter, you
have tackled the complex subject of polymorphism. You saw that polymorphism
is built on the fact that OOP languages enable you to assign a variable of
type child to a variable declared to be of type parent. When you then call
a method of the parent type, it will go to the address of the child
object's methods. As a result, an object of type TParent, when assigned to four
different descendant objects, might react in four different ways. One object, many different faces: polymorphism.
VMS Desenvolvimentos
Diversas Dicas, Apostilas, Arquivos Fontes,
Tutoriais, Vídeo Aula, Download de Arquivos Relacionado a Programação em C++ Builder.
|