| This chapter focuses on object-oriented programming (OOP) as it
applies to the VCL. Specifically, it takes a close look at inheritance, one of
the big-three topics in object-oriented code. The other two key topics are
encapsulation and polymorphism, which you learn about in the next two chapters.
Even if you already know all about OOP, you should still at least skim this
chapter so you can learn about the difference between VCL objects and standard
C++ objects. In particular, this chapter covers the following topics:
- OOP theory and basics
- VCL object construction
- Inheritance
- Virtual methods
- Aggregation
- Form inheritance
The text focuses on several programs designed to show how objects are
constructed. One of the programs is developed in several stages so that you can
see how an object hierarchy emerges out of a set of raw ideas.
After you read the next three chapters on inheritance, encapsulation, and
polymorphism, the next big step is to learn how to build components. In fact,
the real justification for learning this material is that it gives you the
ability to start creating your own components. Building your own components is
one of the most important tasks you can tackle in BCB, so I will lay the
groundwork for it carefully.
It is important to understand that the next three chapters are aimed at
programmers who want to work inside the VCL. I make no attempt to do justice to
all the complex features of the C++ object model. Instead, I try to present you
with a subset of those features as they apply to the VCL. This means that I make
short shrift of interesting topics such as function and operator overloading. My
intent, however, is to show you how to create components. You do not have to be
an expert in C++ OOP theory to achieve that goal.
For all its wonders, I don't think there is anything in C++Builder that even
approaches the significance of components. VCL components are the most amazing
technological achievement I have seen in contemporary programming. If you want
to do something really fantastic with your computer, then pay attention to the
next few chapters so that you can learn how to build great components.
When reading this chapter, you might want to make use of the ClassBrowser
sample program that ships with BCB. It allows you to explore the hierarchy of
the VCL. This program is found in the Examples/ClassBrw directory. It
is far from perfect, but it will serve to give you an overview of the VCL
classes. You should also go to
www.object-domain.com and see whether they have a version of
Snorkle for C++Builder available. The versions of Snorkle for Delphi that I have
seen are very nice indeed, and if they can duplicate their efforts in the world
of C++, then most readers of this book will want to test their technology.
About Objects
It might seem a little strange to start focusing on objects this late in the
book. After all, almost every program I have shown so far uses object-oriented
code. So how could I wait this long to begin talking seriously about objects? To
answer this question, I need to discuss two different issues:
- How does BCB treat objects?
- Why do people write object-oriented code?
The developers wanted BCB to be very easy to use. By its very nature, OOP is
not always a simple topic. As a result, BCB goes to considerable lengths to hide
some of the difficulties of object-oriented programming from the user. The
biggest steps in this direction include the automatic construction of Form1
as an object and the existence of the delegation model. The fact that the
scaffolding for most methods is produced automatically by the IDE is one of the
key ways the product saves time--and one of the key ways it eases the process of
producing applications.
The simple fact is that some people would never be able to approach BCB if
they had to go through the process of writing all this every time they created a
form:
//--------------------------------------------------------------------------
#ifndef Unit1H
#define Unit1H
//--------------------------------------------------------------------------
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
//--------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:
private:
public: // User declarations
virtual __fastcall TForm1(TComponent* Owner);
};
//--------------------------------------------------------------------------
extern TForm1 *Form1;
//--------------------------------------------------------------------------
#endif
I'm leaving out the implementation of the constructor, and a few other
features, but in a stripped-down form, this code is indeed the basis for most
BCB units. It's simple enough to write; nonetheless, it could form a barrier
between the product and certain types of programmers.
The next obvious question is, "Why did the developers choose to write
object-oriented code if the subject itself can at times become somewhat complex?
Why not just use the relatively simpler framework provided by structured
programming?" The answer is that although it is simpler to create small
structured programs than small object-oriented programs, it's easier to write
large object-oriented programs than it is to write large structured programs.
OOP brings discipline and structure to a project. In the long run, this makes
coding easier. The problem is the learning curve associated with understanding
OOP.
Almost everyone agrees that it's easier to finish a group project if you
appoint a leader for the group; it's easier to win at sports if you practice
regularly; and, ultimately, it's easier to become a good musician if you sit
through some boring lessons with a professional. It also might seem at first as
if structured programs are simpler to learn how to write and, therefore, are
simpler to write, but this isn't true. Just as it helps to take lessons,
practice, and learn discipline if you want to become good at playing a sport or
a musical instrument, it helps to learn object- oriented code if you want to
write good programs.
Here's another way of stating the same matter. There is nothing you can do
with object- oriented code that you can't also do with structured programming.
It's just that OOP makes it relatively easy to construct programs that are
fundamentally sound and easily maintained. This doesn't mean you can't write
structured programs that are every bit as architecturally sound as
object-oriented programs. The problem, however, is that it is very difficult to
design a structured program that is truly modularized and truly easy to
maintain. Object-oriented code, on the other hand, has a natural tendency to
move you in the direction of a sound, well-structured design.
The thesis of this chapter is that object-oriented code is basically a
technique for designing robust, well-planned programs. The syntax of OOP emerged
out of the desire to help programmers design applications that work. It is
perhaps arguable as to whether or not OOP by itself succeeded in achieving its
goal, though certainly I personally believe that it is a success. However, I
think it is undeniable that OOP in conjunction with components is the answer to
many core programming problems. If your only experience with components is in
creating ActiveX controls, then you haven't yet seen what this technology can
do. The combination of OOP and components is something that can make programmers
many times more productive than they had ever imagined possible when writing
structured code, or when working with either objects or components alone.
-
NOTE: It's probably worth pointing out
that OOP is not a separate subject from structured programming but its natural
child. OOP emerged out of the same types of thinking that generated structured
code. Much of what is true in structured programs is also true in
object-oriented programs, except OOP takes these theories much further.
Object-based programmers should know nearly everything that structured
programmers know and should then add another layer of information on top of
it.
OOP is certainly not the end-all and be-all of programming. Rather, it is an
intermediate step in an ongoing process that might never have an end. BCB, with
its heavy use of components, already shows part of what the future holds. In
particular, the future is about components and visual manipulation of objects.
The Object Inspector enables you to see inside objects and to start to
manipulate them visually. You can do this without having to write code. It is
quite likely that this trend will continue in the future, and you will start to
see programs not as code but as a series of objects depicted as a hierarchy. If
programmers want to manipulate these objects, they will be able to do so through
tools such as the Object Inspector or through other means currently being used
only in experimental languages.
To take this out of the clouds for a moment, here is my list of what's best
about BCB:
- Visual design tools
- A component architecture replete with a delegation model
- A real object-oriented language
Here are the same ideas looked at again from a slightly more in-depth
perspective:
- Visual Tools: You can easily design a form using visual tools. To create a
useful form, you want to be able to arrange and rearrange the elements of the
visual design quickly and easily. BCB excels at this.
- Components: You want to be able to manipulate objects not only as code,
but as seemingly physical entities you can handle with the mouse. Components
provide an ideal solution to this problem. For example, the plastic Lego sets
you played with as a child were fascinating because they enabled you to build
complex structures out of simple, easy-to-manipulate pieces. In other words,
Legos let you concentrate on the design of structures, making the actual
construction of a robust and easy-to-maintain building relatively trivial.
Components give the same kind of flexibility.
- OOP: Objects, and particularly the ability to view object hierarchies in a
browser, make it easy to see the overall design of a program. It's possible to
see how a program is constructed not only by looking at the code, but also by
looking at an object hierarchy made up of reusable classes. Use the
ClassBrowser example or a copy of Snorkle to view these hierarchies as they
appear in your own programs. These kinds of abstract, visual representations
of a code base aid in the process of designing and maintaining a program. A
key word here is reuse. You can write an object once and then use it over and
over again. Reusability is what OOP is all about.
OOP, then, is part of a theory of design that is moving increasingly in the
direction of reusable, visual components that can be manipulated with the mouse.
Undoubtedly, this means that some types of programs that are difficult to
construct today will become trivial to build in the future. BCB has already
performed this magic with databases. A 10-year-old child could use BCB to
construct a simple database application. However, creating complex programs will
probably always be difficult, simply because it is so hard to design a good
program that performs anything more than trivial tasks. First printing presses,
then typewriters, and finally word processors have made writing much easier than
it used to be, but they have not succeeded in making us all into a race of
Shakespeares.
-
NOTE: One thing that is not
built into BCB that can help you create robust programs is a good
object-modeling tool. There are some tools, such as the products called
WithClass and Snorkle, that are designed to work with the Delphi VCL and
should soon appear in a BCB-based format.
It is worth pointing out that it is easy to draw object hierarchies using some
form of custom or agreed upon notation. Programs such as Visio or Playground
can help with this process. Even if there is no direct code generation
involved, there is still an enormous benefit to be derived from this process.
I have worked on projects I thought had gone hopelessly astray and could not
ever be salvaged. These "lost causes" were saved by simply drawing out my
object hierarchy with a tool that would let me rearrange its elements in
several different patterns. C++ is a great language, but it offers no means
for providing an overview of your object hierarchy. Drawing the object
hierarchy with a simple object notation can help enormously when it is not
clear how to design a particular feature or when you need to try to salvage a
product that has gone astray.
BCB's object-oriented, component-based architecture makes programming easier
than it used to be. That doesn't mean that now everyone will be able to program.
It just means that now the best programmers can make better applications. The
key terms are reuse, visual design tools, components, and objects. If you can
find an object-modeling tool that can aid in program development, you will be
even further ahead.
Creating Simple Objects
To start a discussion of objects, it might be a good idea to cut the VCL out
of the picture as much as possible. This will eliminate the complex object
hierarchy associated with the VCL. In its place, you can construct some very
simple objects with a known hierarchy that is easy to define. As the discussion
progresses, the VCL can be introduced into the programs in a planned and
sensible manner.
- 1. Start a new project.
2. Bring up the Project Manager from the View menu and remove
Form1.cpp and the project resource file.
3. Go to the View menu again and choose Project Source.
4. Go to Options | Project | Linker and choose Console application, as
shown in Figure 19.1.
FIGURE 19.1.
Creating a console application in BCB.
Edit the main source file for the project so it looks like this:
#include <stdio.h>
int main(void)
{
printf("Daughters of Time, the hypocritic Days,\n");
printf("Muffled and dumb like barefoot dervishes\n");
printf("-- Ralph Waldo Emerson");
return 0;
}
Save this file as Object1.mak. It is now a complete application that
circumvents the VCL. If you open up a DOS window and run the program from the
DOS prompt, the output looks like Figure 19.2.
FIGURE 19.2.
The output from the first take of the OBJECT1 program as it appears when run
from the DOS prompt.
It might seem strange to you that I have gone out of my way to eliminate so
much of the object hierarchy in a chapter that is about objects. My goal,
however, is to clear the boards so that you can view objects in a simplified
state, thereby clearly delineating their most salient points.
The program that unfolds through the next few pages is called OBJECT1. This
is a very simple object-oriented program that you will build on the console
application framework established earlier. I'm not going to start by showing you
the code for the whole program, because I want you to build it one step at a
time so that its structure emerges little by little.
To begin, you should create a small object at the top of the program:
class TMyObject
{
};
int main(void)
{
return 0;
}
All I have done here is added a simple class definition and removed the
printf()statements.
Delphi programmers should note that this class is not a descendant of
TObject, even though it would have been in Object Pascal. One of the
fundamental rules of Object Pascal programming is that it is impossible to build
an object that is not a descendant of TObject or one of TObject's
children. The reason for this rule is that TObject contains some
RTTI-based intelligence that is needed by all BCB objects. This same
intelligence is present in the metaclass that is part of the BCB version of
TObject. However, you can create C++ objects that do not descend from
TObject and thus do not include this intelligence.
-
NOTE: You will find that I use
the words class and object almost completely interchangeably. This is
technically correct, although there is some merit in using the word class to
describe the written declarations that appear in a text file and object to
refer to a compiled class that is part of a binary file. In other words,
programs are made up of objects, whereas source files show class definitions.
However, this distinction is not one that I spend a great deal of time
stressing in this book.
To create a true VCL object, you should change TMyObject's
definition so that it reads as follows:
#include <vcl\vcl.h>
class TMyObject : public TObject
{
public:
__fastcall TMyObject(void) : TObject() {};
}
int main(void)
{
return 0;
}
Logically, there is now a considerable difference between this declaration
and the one you created earlier. In particular, this is now a VCL object and
must be created on the heap. It also supports VCL specific syntax such as the
__published directive.
All VCL objects must have a constructor, and it should be declared
__fastcall. Methods or functions declared __fastcall can have some
of their parameters passed in registers, rather than always being pushed on the
stack. This is the calling convention used by VCL constructors, so you should
conform to it.
All VCL objects that are descendants of TComponent must have
destructors declared __fastcall virtual:
__fastcall virtual TComponent(TComponent* Aowner);
The reason for the __fastcall and virtual restrictions has
to do with conformance to the VCL programming model. In particular, the VCL
declares the constructor for TComponent as virtual, so C++
objects that descend from TComponent must follow along. This means that
all components you create must be declared with virtual constructors,
because all components are, at least in practice, descendants of TComponent.
I comment on this fact simply because it is very unusual for C++ constructors to
be declared virtual.
-
NOTE: Actually, it is
theoretically possible for you to create your own class that performs the same
chores that TComponent performs. There is nothing magical about
TComponent; it simply contains standard VCL code that makes it possible
for an object to live on the Component Palette. It is not practical to
duplicate this effort in your own code, and so one could perhaps go so far as
to say that "by definition" all components are descendants of TComponent.
However, this is not strictly true, as you could create your own class that
appears on the Component Palette without descending from TComponent.
I personally cannot imagine any set of circumstances that would justify the
effort involved in duplicating the work done in TComponent.
The declaration and implementation for a C++ constructor can have two forms.
They can appear entirely inside a class declaration, or you can split them up,
with the declaration inside the class and the implementation outside:
#include <vcl\vcl.h>
class TMyObject : public TObject
{
public:
__fastcall TMyObject(void);
};
__fastcall TMyObject::TMyObject(void) : TObject()
{
}
int main(void)
{
return 0;
}
When you implement a constructor, you should follow the header with a colon
and a call to the ancestor's constructor:
_fastcall TMyObject::TMyObject(void) : TObject()
Now that you have an overview of a basic VCL object declaration, the next
step is to declare a variable of type TMyObject and then instantiate it
and dispose of it:
class TMyObject : public TObject
{
public:
__fastcall TMyObject() : TObject() {}
};
int main(void)
{
TMyObject *MyObject = new TMyObject;
delete MyObject;
return 0;
}
The code shown here doesn't do anything functional. Its only purpose is to
teach you how objects work. Specifically, it declares a variable of type
TMyObject:
TMyObject *MyObject
Next, it allocates the memory for the object:
new TMyObject;
Put together on one line, the statement looks like this:
TMyObject *MyObject = new TMyObject;
This statement actually creates a pointer variable of type TMyObject.
In VCL programming, you have to take this step if you want to use MyObject,
and, furthermore, you must dispose of this memory when you are finished with it.
There are two ways to destroy an object. One is to call Free, and
the other is to use the delete operator:
MyObject->Free();
delete MyObject;
Both techniques have the same outcome. I believe the majority of people
prefer delete, and it is what I use most often in the code found in
this book. However, there are reasons you might want to call Free, so I
will discuss it in the next few paragraphs.
When you free an object, what you are really doing is calling the object's
destructor. The following code shows approximately what takes place in the
Free method of TObject:
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
The variable Self always points to the current object. It plays the
same role in Object Pascal that this plays in C++. If you are inside
one of the methods of an object, you can refer to that object by using Self.
(Self is passed as an implicit parameter to all BCB methods.) Here is
how the VCL Free method would look in C++:
void __fastcall TObject::Free()
{
if (this != NULL)
~TObject();
}
NOTE: Programmers use the words
descendant, child object, derived class, and subclass as synonyms. I prefer to
use either descendant or child object, because subclass is also used in
another context and derived class seems unnecessarily obscure. My feeling is
that it's best to stick to one metaphor: parent, child, ancestor, and
descendant, where child and descendant are synonymous, and parent and ancestor
are synonymous.
Standard C++ does not define a Free method. This is something
specific to the VCL. It is added to the VCL to make objects easier to use. It is
very bad to call the destructor of an object that no longer exists. As a result,
the Free method is there to provide a check that gives you some measure
of protection against this error. Despite this, the general consensus is that it
is best to call delete. The great virtue of delete is that it looks like
standard C++ code, and C++ programmers care a lot about standards.
Now that you know how to declare, allocate, and deallocate a simple object,
it's time to narrow the focus and tackle the subject of inheritance. The next
two sections are dedicated to this chore--specifically, to explaining the
relationship between a parent and child object.
Understanding Inheritance
In general, a child object can use any of its parent's methods. A descendant
of an object gets the benefit of its parent's capabilities, plus any new
capabilities it might bring to the table. I say that this is true in general,
because the private directive can limit the capability of a child to
call some of its parent's routines. The private directive is explained
in depth later in this chapter.
Except for its constructor, all of TMyObject's methods and fields
are inherited:
class TMyObject : public TObject
{
public:
__fastcall TMyObject() : TObject() {}
};
This declaration is somewhat deceiving because TObject contains many
methods that are available to instances of TMyObject. In other words,
TMyObject is not quite as simple an object as it appears at first.
So, what are all these methods associated with TObject? Well, you
can see their definitions, as well as their implementations, if you open up the
SysDefs.h file from the \BCB\Include\VCL subdirectory:
class __declspec(delphiclass) TObject
{
public:
__fastcall TObject() {}
__fastcall Free();
TClass __fastcall ClassType();
void __fastcall CleanupInstance();
void * __fastcall FieldAddress(const ShortString &Name);
static TObject * __fastcall InitInstance(TClass cls, void *instance);
static ShortString __fastcall ClassName(TClass cls);
static bool __fastcall ClassNameIs(TClass cls, const AnsiString string);
static TClass __fastcall ClassParent(TClass cls);
static void * __fastcall ClassInfo(TClass cls);
static long __fastcall InstanceSize(TClass cls);
static bool __fastcall InheritsFrom(TClass cls, TClass aClass);
static void * __fastcall MethodAddress(TClass cls, const ShortString &Name);
static ShortString __fastcall MethodName(TClass cls, void *Address);
...// Code omitted here
virtual void __fastcall Dispatch(void *Message);
virtual void __fastcall DefaultHandler(void* Message);
private:
virtual TObject* __fastcall NewInstance(TClass cls);
public:
virtual void __fastcall FreeInstance();
virtual __fastcall ~TObject() {}
};
You can find the entire implementation of TObject in the Object
Pascal System.pas unit that ships with BCB. Much of it is actually in
assembler, but the source is there if you want to study it. You should, of
course, also examine the declaration for TObject in SysDefs.h.
The calls in SysDefs.h, however, ultimately resolve into calls to the
Pascal implementation in System.pas. I should perhaps add that the
TObject declaration in SysDefs.h is very hard to understand, but I
promise you that it does end up resolving into calls that access the
System.pas version of TObject.
-
NOTE: Although I have mentioned
this subject before, it's probably once again time to stress the importance of
viewing the Pascal source code to the VCL. Your version of BCB might or might
not ship with the source, but if you don't have it and can possibly afford to
buy it, you should think seriously about obtaining it. You should peruse the
BCB\Include\VCL subdirectory that contains the header files for the
imported VCL Pascal units. These files provide the interface for key BCB
units. They are not as good as having the source, but they are very valuable.
I refer to both the header files and the Pascal source continuously.
You can see that TObject has a few basic functions declared right at
the top:
__fastcall TObject() {}
__fastcall Free();
virtual __fastcall ~TObject() {}
The point to grasp here is that TMyObject has a destructor and
Free method because it inherits them from TObject.
To understand this point, you can add a line of code to the nascent OBJECT1
program:
#include <conio.h>
#include <stdio.h>
int main(void)
{
TMyObject *MyObject = new TMyObject;
AnsiString S = MyObject->ClassName();
printf(S.c_str());
delete MyObject;
getch();
return 0;
}
This code enables the object to write its name to the screen. The output from
this program is a single string:
TMyObject
When you run the program, this string might flash by too quickly for
leisurely perusal. To remedy the situation, add a getch() at the very
end of the code, right before the return statement. To end this
program, press Enter. (That's the way it used to be done back in the DOS world.)
If you want to, you can even get this object to say its parent's name:
int main(void)
{
TMyObject *MyObject = new TMyObject;
printf(Format("ClassName: %s\nParent's ClassName: %s",
OPENARRAY(TVarRec, (
MyObject->ClassName(),
MyObject->ClassParent()->ClassName()))).c_str());
delete MyObject;
getch();
return 0;
}
The output from this code is the following:
ClassName: TMyObject
Parent's ClassName: TObject
The point, of course, is that TMyObject inherits quite a bit of
functionality from its parent, and, as a result, it has numerous capabilities
that might not be obvious from merely viewing its declaration.
The ability to trace an object's ancestry is relatively appealing, so it
might be nice to add it to TMyObject as a method:
#include <vcl/vcl.h>
#include <stdio.h>
#include <conio.h>
#include "classrefs.h"
USEUNIT("ClassRefs.cpp");
class TMyObject : public TObject
{
public:
TMyObject() : TObject() {}
void PrintString(AnsiString S);
void ShowHierarchy();
};
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
int main(void)
{
ShowClassReferences();
TMyObject *MyObject = new TMyObject;
MyObject->ShowHierarchy();
delete MyObject;
getch();
return 0;
}
This version of the OBJECT1 program includes two methods, listed in the
TMyObject class declaration:
class TMyObject : public TObject
{
public:
TMyObject() : TObject() {}
void PrintString(AnsiString S);
void ShowHierarchy();
};
Take a look at the implementation for ShowHierarchy. Perhaps the
first thing you notice in it is the class reference in the first line, which
uses the TClass type.
The type TClass is an object reference and is declared in
Sysdefs.h as follows:
typedef TMetaClass* TClass;
Because ClassParent returns a variable of type TClass, it
is obviously what needs to be used here.
-
NOTE: An object reference is a
special metaclass that can be assigned to an object. Here is a unit that shows
some legal uses of an object reference:
///////////////////////////////////////
// ClassRefs.cpp
// Object1
// copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <forms.hpp>
#pragma hdrstop
#include "ClassRefs.h"
TClass AClass;
class TDescendant: public TObject
{
public:
TDescendant(): TObject() {}
-
};
void ShowClassReferences()
{
printf("** Start object references **\n");
AClass = __classid(TObject);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
AClass = __classid(TDescendant);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
AClass = __classid(TForm);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
printf("** End object references **\n\n");
}
Notice that you do not have to create an object before you can use it with
an object reference. In general, you can call any of the static
methods of TObject with a class reference.
You cannot use an object reference to refer to a field that belongs only to
the child of the object reference type. For instance, this code does
not compile because Caption is not a property of TObject:
ObjectRef = __classid(TForm)
ObjectRef.Caption := `Sam';
WriteLn(ObjectRef.Caption);
You will find a version of the CLSREF unit in the same
subdirectory as OBJECT1. You can use the Project Manager to add this file to
the project, and you can then call it in the second line of the body of the
OBJECT1 program. However, you should not leave this unit as part of the
project, because it will muddy the view of the object hierarchy that you get
in the Browser.
There are not that many times in which you need to use an object reference in
day-to-day programming. If you are not totally clear on what they do, you can
probably afford to skip the subject. If you really want to know more, you
should examine Sysdefs.h; recognize that the TMetaClass you
see there is the C++ way of creating a feature that exists in the VCL. The
reason the VCL supports this feature is that it needs fairly extensive Run
Time Type Information (RTTI) in order to run, and it gets a good portion of
that information from the methods in TObject that are part of
TMetaClass and are in turn used in an object reference.
When you get past the object reference, the remaining portions of the
ShowHierarchy method are fairly straightforward:
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
This code first writes the ClassName of the current object, which is
TMyObject. Then it gets the ClassParent, which is TObject,
and writes its name to the screen. The code then tries to get TObject's
parent and fails, because TObject has no parent. At this point,
AClass is set to NULL and the code exits the while loop.
The output for the program is shown in Figure 19.3.
FIGURE 19.3.
The output from the OBJECT1 program.
In this section, you have learned about the Create, Destroy,
Free, ClassParent, and ClassName methods of
TObject. The declaration of TObject shows that several other
methods are available to BCB programmers. However, I do not discuss these
methods in depth because they are either self-explanatory (InheritsFrom)
or beyond the scope of this book. I should mention, however, that some of these
routines are used by the compiler itself when dispatching routines or performing
other complex tasks that usually require RTTI support. These are advanced
programming issues that impact only a very small percentage of BCB programmers.
Virtual Methods
Inheritance, in itself, is an interesting feature, but it would not take on
much significance were it not for the presence of virtual methods. Virtual
methods can be overridden in a descendant class. As such, they provide the key
to polymorphism, which is a trait of OOP programs that enables you to give the
same command to two different objects but have them respond in different ways.
This chapter introduces polymorphism, but I will leave the more complex aspects
of this subject for Chapter 21, titled, appropriately enough, "Polymorphism."
Polymorphism is a relatively difficult subject to grasp; therefore, I have
stretched out a full explanation of it over several chapters.
Unlike Object Pascal, BCB has only one type of virtual method. This directive
tells the compiler to store the address of the function in a virtual method
table.
-
NOTE: Delphi programmers should
note that C++ does not support either the dynamic or message directives. In
their place, you can use the MESSAGE_MAP macro.
The OBJECT2 program (shown in Listing 19.2) has one virtual method.
The virtual method is overridden in a child object. When you are
creating the OBJECT2 program, you should start with the source code for the
OBJECT1 program. Modify the code by declaring PrintString as
virtual and by creating a descendant of TMyObject called
THierarchy. Also, don't forget to make sure the program is set to work as a
console application. If you don't have this option checked, you can get an
EInOutError exception. After changing the setting, you should also rebuild
your project so the new option takes effect.
NOTE: When creating one project
based on another, you can often copy the code from the directory where the
first project is stored into a separate directory made for the second project.
After copying the project, it is probably simplest to delete everything but
the actual source files from the new directory. For instance, delete the DSK
file, the MAK file, and any other extraneous files you will not need. Then
create a new project, delete its main form, and add copies of the source files
you want to reuse from the previous project. Otherwise, you might find paths
hard-coded into your DSK or MAK files that address files stored in the first
program's directory. In this particular case, it is probably easiest just to
re-create the project entirely from scratch, but the information in this note
can be used as a general set of guidelines for use when copying projects from
one directory to another.
Listing 19.2. The source code for the main unit in
the OBJECT2 program.
///////////////////////////////////////
// Object2.cpp
// Project: Object2
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl/vcl.h>
#include <stdio.h>
#include <conio.h>
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
virtual void PrintString(AnsiString S);
};
class THierarchy: public TMyObject
{
int FColor;
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
__property int Color={read=FColor,write=FColor};
};
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
textcolor(FColor);
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
int main(void)
{
TMyObject *MyObject = new TMyObject();
MyObject->ShowHierarchy();
MyObject->Free();
THierarchy *Hierarchy = new THierarchy();
Hierarchy->Color = YELLOW;
Hierarchy->ShowHierarchy();
Hierarchy->Free();
getch();
return 0;
}
In OBJECT1, the ShowHierarchy method wrote its output to the screen.
Suppose that you found this object somewhere and liked the way it worked but
wanted to change its behavior so it could also write its output in color. The
OBJECT2 program shows a preliminary version of how you might proceed. After
completing this first take on creating a descendant of TMyObject, I
will revisit the subject and show ways to improve the model shown here. The
output from the program is shown in Figure 19.4.
FIGURE 19.4.
The output from the OBJECT2 program.
In the old world of structured programming, the most likely step would be to
rewrite the original ShowHierarchy method. However, rewriting an
existing method can be a problem for two reasons:
- You might not have the source code to the routine, so you can't rewrite
it. This is a common problem because most programming tools are delivered in
binary libraries.
- You might have the source code but also know that this particular method
is already being called by several different programmers. You, therefore,
might be afraid to rewrite it because you might break the other programmers'
code. Furthermore, making changes like this cuts you off from the upgrade path
provided by the maker of the library. You can't just use the maker's next
version of the product out of the box, because you now have a customized
version of his or her library.
A combination of design and maintenance issues might deter the impulse to
rewrite the original method. Many projects have been delayed or mothballed
because changes in their designs have broken existing code and thrown the entire
project into chaos.
OOP has a simple solution to this whole problem. Instead of declaring
TMyObject as
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
void PrintString(AnsiString S);
};
thoughtful programmers declare it like this:
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
virtual void PrintString(AnsiString S);
};
The difference is that in the second example, the PrintString method
is declared as virtual.
If PrintString is declared as virtual, you can override it
in a descendant object, thereby changing the way the method works without ever
changing the original version of the method. This means that all the other code
that relies on the first version of the program continues to work, and yet you
can rewrite the function for your own purposes. Furthermore, this technique
would work even if you didn't have the source code for TMyObject! I
will show you how this works in just a moment.
Some readers might have asked themselves earlier why I created the
PrintString method in the first place. The answer hinges on the fact that
iterating through a hierarchy of VCL objects can always be accomplished by the
same algorithm. The same technique works for all VCL objects. But the act of
printing information to the screen changes depending on your current
circumstances. Are you in DOS? Are you in Windows? Do you want to use colors?
Each of these circumstances calls for a different way of printing information to
the screen. As a result, I separated the screen IO portion of TMyObject
from the portion of the object that iterates through a hierarchy. Furthermore,
there is no need to declare ShowHierarchy as virtual, but I
must declare PrintString as virtual. The reasoning here is
simply that ShowHierarchy does not need to change in descendants of the
object, but PrintString will need to change. In particular, it will
need to change so that it can write output in color. These types of
considerations are part of a subject known as object design.
At this point, you might think that using virtual methods seems like an
unnecessarily opaque solution to this problem. Wouldn't it have been simpler to
add new methods to the inherited class? Then the user of this second class could
call these new methods rather than the ones from the first instance of the
class. There are three problems with this technique:
- It requires the user to memorize a whole slew of different method names
that perform related but slightly different tasks. This is precisely what you
have to do in structured programming, and it is exactly what you want to
avoid. Instead, you want to have one name that applies to all similar methods
of this family. For instance, if you have an animal object, you are
going to have to make the Walk method virtual because a bird
walks on two legs, and a cat walks on four. The implementation of Walk
is different for each animal, so you need to declare the method virtual.
When you are done, you can use Cat->Walk(), and the cat will walk
properly. Conversely, you can use Bird->Walk(), and the bird will
walk properly. It would be a mess if you had to use Bird->WalkOnTwoLegs()
and Cat->WalkOnFourLegs(). This proliferation of similar but slightly
different method names is exactly the kind of structured programming fiasco
that OOP was designed to avoid. Instead of a bunch of similar names like
WalkOnTwoLegs, CrawlOnYourBelly, WalkOnFourLegs, and
WalkOnOneHundredLegs, you want to have just one word, such as
Walk, that applies to a whole family of objects. In other words, each
object in the family will implement the walk method differently.
- The second problem is that these objects call the PrintString
method internally. If you did not declare the method as virtual, you
would have to figure out some way for the ShowHierarchy method to
know whether it should call the implementation of PrintString that is
part of TMyObject, or whether it should call the implementation that
is part of THierarchy. By declaring a method as virtual,
this chore will be handled for you automatically. If you create an instance of
THierarchy, ShowHierarchy will call
THierarchy->PrintString automatically; and if you create an instance of
TMyObject, it will call TMyObject->PrintString. This is the
way OOP handles virtual methods, and it is one of the key concepts that makes
this system work.
- The final, and best, reason for doing things this way has to do with
polymorphism. As such, it may not be clear to all readers at this time, but I
promise that it will make sense after you have read the polymorphism chapter.
You can declare a pointer of type TMyObject that can be assigned to a
pointer of type THierarchy. When you then call
TMyObject->PrintString(), the PrintString method of
THierarchy will be called even though the object instance is of type
TMyObject. This would not occur if PrintString were not declared
virtual. In fact, this behavior is what polymorphism is all about,
and indeed, it is one of the cornerstones of OOP programming.
The word virtual is inherited from one class to the next. If a base class
declares a method virtual, the descendants need not do so, because they
will inherit the virtual declaration for a particular method from the
base class. Delphi users take note, as this is the exact opposite of what
happens in Object Pascal. I should add that it is generally considered bad form
not to repeat the declaration in child objects, as you want to be sure the
reader of your code can see at a glance how it is structured.
Here is a look at a stripped-down descendant of TMyObject that
overrides the PrintString method:
class THierarchy: public TMyObject
{
int FColor;
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
__property int Color={read=FColor,write=FColor};
};
This declaration states that class THierarchy is a descendant of
class TMyObject and that it overrides PrintString.
-
NOTE: Delphi programmers beware!
The Pascal object model performs the same chore by using the override
directive. That's not the way C++ works. This is a major change between the
new BCB code and the old Object Pascal techniques.
A first take on the new version of the PrintString method looks like
this:
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
textcolor(FColor);
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
This code depends on functionality from Conio.h that allows you to
print strings to the screen in color.
You can see that a field called FColor has been added to this
object: THierarchy now contains not only procedures but also data. One
of the key aspects of class declarations is that they can contain both methods
and data, so that you can bring all the code related to the THierarchy
object together in one place. This is part of a concept called encapsulation,
explained in the next chapter.
-
NOTE: By convention, VCL objects
declare private data as starting with the letter F, which stands for field.
This technique is helpful, because it highlights the difference between an
object's properties and its data. A property is published to the world and
does not begin with the letter F. Private data is inaccessible to the rest of
the world and begins with the letter F.
When you run the OBJECT2 program, the following code is executed in its main
body:
int main(void)
{
TMyObject *MyObject = new TMyObject();
MyObject->ShowHierarchy();
MyObject->Free();
THierarchy *Hierarchy = new THierarchy();
Hierarchy->Color = GREEN;
Hierarchy->ShowHierarchy();
Hierarchy->Free();
getch();
return 0;
}
This code creates an object of type THierarchy and then shows you
how to use the new functionality of the ShowHierarchy method. I also
create an instance of TMyObject so that you can compare the two classes
demonstrated so far in this chapter.
-
NOTE: I should perhaps mention
that it is more expensive to declare or call virtual methods than it is to
call a static method. As a result, you need to weigh the whole issue of
whether you want to declare a method to be virtual.
In my opinion, you should usually create objects that have the best possible
design, re-gardless of the amount of overhead they entail. Of course, it is
possible to take this theory too far, but the mere fact of adding a few
virtual methods is usually not the problem in bloated object hierarchies.
Besides space and performance, a second reason for not declaring an object
virtual is a desire to hide its implementation so you can change it
later. There is usually no point in declaring a private method virtual,
because it can't be seen by other objects, unless they are friends of the
original object. (I will talk about friend objects later in this chapter.) The
great advantage of private methods is that they can always be changed later to
whatever degree you want, because other objects usually cannot see them and
cannot access them directly. As a result, you may want to declare methods
private, and non-virtual, so you can change their implementation
later on. Needless to say, I am referring specifically to the act of changing
the number of parameters these methods take.
Your users will, of course, complain if you take a method they occasionally
want to override and make it private and non-virtual.
However, it is sometimes better to listen to their complaints than to saddle
them with a broken object that cannot be fixed without breaking existing code.
In this section, you have learned about the virtual directive. This
subject's true significance won't be clear until you read about polymorphism.
However, before you tackle that subject, it's best to learn more about
inheritance and encapsulation. In particular, the next section of the chapter
looks at more issues involving object design.
Searching for the Right Design
It is almost impossible to find the right design for an object the first time
you write it. As a rule, the only way you can figure out the design for an
object is by creating it, discovering its limitations, and then making
improvements to its design. In short, object design is an iterative process.
There is no good way to step you through the process of discovering the
correct object design in a book, because the written word is by its nature
static, and the process I'm describing is dynamic. Furthermore, it's confusing
to the reader to show a series of poorly designed objects that are successively
improved in each iteration. The problem with this technique is that the reader
keeps seeing the wrong way to create an object and can easily pick up bad habits
or fundamental misconceptions about object design. Even worse, the reader tends
to get frustrated with having to unlearn the techniques they just acquired in
the previous example that are now revealed as being flawed.
To avoid the problems outlined in the preceding paragraph, I will simply show
you a second version of the THierarchy object and explain why it
contains changes to the original version of the object you saw in the last
section. This process does not tell you much about how I discovered the flaws in
the object, but it will show you what the flaws are and how I got around them.
The main point to grasp is that object design is an iterative process, and that
the correct way to find the flaws in an object is to implement them once as best
you can, and then look for problems.
-
NOTE: There is a second school
of thought that states that you can find the correct design for an object
before implementing it. Proponents of this technique often suggest that a team
be split in two, with part of the members designing objects and the other part
implementing them. I have to confess that I've never actually discussed the
results of this technique with someone who has used it successfully, as all my
experience has been with programmers who use the iterative technique I discuss
here. (Some of these programmers have also tried the second school of
programming, but it did not work for them.)
Of course, you should try to get an object right the first time, and you
should use high-level tools that help with design. However, you should also
expect to have to refine the objects you create through a perhaps lengthy,
iterative process.
Furthermore, you should design your object defensively. That is, you should
carefully hide your implementation inside private methods and data because you
will surely have to change its design at a later time.
The first problem with the original version of the THierarchy object
became clear when I wanted to find a method to clear the screen. When doing so,
I needed to first set the text and background color to which I wanted the screen
to be cleared. As a result, I added new properties and new set methods to the
object. The new property let me add a background color, and the set methods let
me change the text and background colors at the same time I assigned them to the
private data:
class THierarchy: public TMyObject
{
int FTextColor;
int FBackColor;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; textbackground(FBackColor); }
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
virtual void ClrScr();
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
The implementation of PrintString now looks like this:
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
Note that I have removed the code that changed the color of the text. This
code is no longer necessary as the color gets changed in the SetTextColor
method.
The extremely straightforward implementation of ClrScr looks like
this:
void THierarchy::ClrScr()
{
clrscr();
}
NOTE: C++ allows you to declare
methods inline, as shown by the SetTextColor and
SetBackColor method declarations. Conversely, you can also declare a
method outside of the object, as I do in the case of ClrScr. This
latter technique is called an out_of_line declaration, but that has
such a ghastly ring in my ear that I refuse to use it. You can, in fact,
implement a method outside an object and make it inline by using the
inline directive:
inline void SetTextColor(int Color)
{
FTextColor = Color;
textcolor(FTextColor);
}
Inline methods usually execute faster than regular methods because they are
placed directly in your code and do not require the overhead associated with a
function call. Whether you implement them inside or outside of a class
declaration, you should declare only very small methods as inline,
and they should not contain any loops.
Inline methods are great, and I use them regularly. The only drawback I see to
them is that they can make object declarations difficult to read. In
particular, the great thing about an object declaration is that it can provide
a summary of the functionality of an object without asking you to wade through
its implementation. Inline methods implemented inside a class declaration
detract from this feature because they clutter up the landscape.
One possible workaround is to declare inline functions separately from the
object declaration by using the inline keyword. In fact, this is
probably the ideal solution, and it is only laziness that keeps me from using
it at all times. I, for one, would have been glad if the compiler enforced
this rule.
The next change I made to THierarchy involved a desire to increase
the flexibility of the object. In particular, it would be great to be able to
use this object even if you do not descend from it directly. One way to do this
is via multiple inheritance, but that technology is not supported by VCL
objects. I should perhaps add that I am not particularly partial to multiple
inheritance as a technology for use in real-world applications.
A much simpler way to make the object more flexible is to pass the
ShowHierarchy method a copy of the object whose hierarchy you want to
explore:
void TMyObject::ShowHierarchy(TObject *AnObject)
{
TClass AClass;
AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();
PrintString(AClassName);
AClass = AnObject->ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
The interesting thing about this change is that it occurs at the level of the
TMyObject implementation and declaration. In other words, it represents
changes not to THierarchy but to TMyObject. If this object
were of more earth-shaking import and if I had released TMyObject to
the public, I could not have made this sort of change because I would be
breaking the code of those who already called the ShowHierarchy method.
There are, I suppose, three things you can learn from this example:
- 1. Try not to publish any part of an object hierarchy until you are
sure you can live with its current public interface.
2. Hide your implementation. Avoid letting consumers of your objects
directly call one of the methods that involves a significant part of your
implementation.
3. Object design is an art, not a science. There is no such thing as a
perfect object. All objects have flaws. Plan them out carefully ahead of time,
bring them to fruition by a lengthy iterative process, and then send them out
into the world with the understanding that your design is flawed by
definition. Recognize that you are going to have to support the objects you
send into the world, because your users are going to find flaws in them. It is
the mark of an amateur to stonewall the consumer of an object when he or she
offers intelligent criticism of an object implementation. Likewise, it is
naïve for the consumer of an object to expect it to be perfect. Perfection is
too high a goal. Instead, shoot for high quality, and then demand that object
producers provide fixes when flaws are discovered. Fixes need not arrive
immediately, but they should appear in the next version of the product.
After making these changes, I decided that the object was ready to see the
light of day. As a result, I moved it into its own file called MyObject.cpp.
I then made a program called OBJECT3 that tests this new arrangement. The
results of this effort are shown on the CD-ROM that accompanies this book in a
program called OBJECT3. The code for this program is shown in Listings 19.3
through 19.5.
Listing 19.3. TMyObject and THierarchy now reside in
their own file, called MyObject.
///////////////////////////////////////
// MyObject.h
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MyObjectH
#define MyObjectH
#include <conio.h>
#include <vcl\stdctrls.hpp>
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy(TObject *AnObject);
virtual void PrintString(AnsiString S);
};
class THierarchy: public TMyObject
{
int FTextColor;
int FBackColor;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; textbackground(FBackColor); }
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
virtual void ClrScr();
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
#endif
Listing 19.4. The implementation for TMyObject and
THierarchy are shown here in MyObject.cpp.
///////////////////////////////////////
// MyObject.cpp
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "myobject.h"
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy(TObject *AnObject)
{
TClass AClass;
AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();
PrintString(AClassName);
AClass = AnObject->ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
void THierarchy::ClrScr()
{
clrscr();
}
Listing 19.5. The test program for the MyObject
unit.
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "myobject.h"
USEUNIT("MyObject.cpp");
void main()
{
THierarchy *H = new THierarchy();
H->TextColor = YELLOW;
H->BackColor = BLUE;
H->ClrScr();
H->ShowHierarchy(H);
delete H;
getch();
}
Notice that I include code in the test program that "uses" the MyObject
unit. I did not insert this code manually but instead used the program manager
to add the MyObject module to the project. This is the best way to
proceed, as it allows you to avoid editing the make file.
The program itself simply sets a background and text color for the output,
then clears the screen to those colors and shows the hierarchy to the user. The
next-to-last line in the program calls delete rather than Free.
You can use either technique, depending on the dictates of your taste and
background.
Showing the Hierarchy of a VCL Program
To really appreciate the new objects developed in the last section, you need
to add them to a regular BCB application. For instance, if you use them in a
standard Form1 class, here is the output you get:
TForm1
TForm
TScrollingWinControl
TWinControl
TControl
TComponent
TPersistent
TObject
This shows the whole hierarchy of class TForm1, starting with
this, moving to TForm, back to TScrollingWinControl, and
so on, all the way back to TObject.
As you know, VCL objects do not support multiple inheritance. As a result, I
could not add the functionality of THierarchy to TForm by
letting the object inherit it. This is fine with me, because I use multiple
inheritance only very reluctantly.
Instead of multiple inheritance, I prefer to use a technique called
aggregation. In aggregation, you add the object you want to use as a field of
your current object and then expose its methods either by publishing the whole
object as a property, or by wrapping the methods of the object inside methods of
your current object. This technique allows you to easily take precise control of
the functionality from the new object, and it tends to support clean, bug-free
programming.
Before I show you how to use aggregation, I need to point out that it is not
possible to use THierarchy directly in a Windows program because
THierarchy uses Conio.h. The question then becomes, "Is
THierarchy designed in such a way that I can descend from it and change its
functionality so that it works with a Windows program?"
Well, part of the answer should be obvious. The key method that had to be
virtual, the one that had to be overridden to make this work, is PrintString.
And indeed, PrintString is declared virtual, so it is possible
to change THierarchy's stripes. In fact, you will find that I also
override the setters and the ClrScr method.
-
NOTE: You might feel that having
to override so many methods in order to change the output of this program is
an excessive amount of work when compared to the relatively simple task of
rewriting the ShowHierarchy method from scratch. Indeed, when looked
at from this perspective, the whole task of creating an object in this case
seems fruitless. I readily confess that it would indeed have been simpler to
write three versions of a non-OOP method called ShowHierarchyBlackandWhite,
ShowHierarchyColor, and ShowHierarchyWindows. Here is a
ShowHierarchy method tailored for use with a VCL form-based program:
void TForm1::ShowHierarchy()
{
TClass AClass;
Memo1->Clear();
Memo1->Lines->Add(AnsiString(ClassName()));
AClass = ClassParent();
while (True)
{
AnsiString S = AnsiString(AClass->ClassName());
Memo1->Lines->Add(S);
if (AnsiString(AClass->ClassName()) == "TObject")
break;
AClass = AClass->ClassParent();
}
}
Clearly, this object is easier to write than the objects I have produced
here. The key reason I created an object like THierarchy is simply
that it illustrates how OOP works. In real life, objects often contain 20, 30,
or even 50 methods. When seen in that light, having to override three or four
methods does not seem like such a big chore. Furthermore, you can often use
delegation to solve these kinds of problems.
Of course, this isn't real life but a book on programming. I have therefore
intentionally created a small object with few methods so that I can focus your
attention solely on virtual methods that need to be overridden. If I
had cluttered up this chapter with a big object, you never could have seen the
trees for the forest, because at least half the chapter would have involved an
explanation of a huge object.
Furthermore, the ShowHierarchy method is the difficult method in this
program. It's easy to write the methods that need to be overridden, while some
programmers might have trouble creating ShowHierarchy. In this sense,
even the rather simple object I have created here does a good job of hiding
complexity and promoting bug-free reuse.
A final point about this subject is that OOP's main strength is not in
producing small code, nor is it true that objects are necessarily easier to
create than equivalent structured code. Rather, the strength of OOP is in
letting you easily and safely reuse premade objects. Now that these objects
are completed, they can be reused easily in any WinTel-based C++ program,
whether it runs in Windows or from the command line. OOP never makes sense if
you think in terms of writing your whole program from scratch. Instead, the
advantage of OOP comes when you want to create programs by reusing premade
objects. OOP is about code reuse and about writing bug-free programs; it is
not about finding the smallest possible implementation of an algorithm. When
you add components to OOP, you can also get RAD, which is another major boost
in productivity.
In Listings 19.6 through 19.9, you will find the code for the new version of
the Object1.cpp module, as well as the code for a standard VCL
form-based program called HIERARCHY that uses the object. The output from the
program is shown in Figure 19.5.
FIGURE 19.5.
The HIERARCHY form sports a TButton and a TMemo component.
Listing 19.6. The new code for the MyObject header
file.
///////////////////////////////////////
// MyObject.h
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MyObjectH
#define MyObjectH
#include <conio.h>
#include <vcl\stdctrls.hpp>
class TMyObject :public TObject
{
protected:
virtual void PrintString(AnsiString S);
public:
TMyObject() : TObject() {}
void ShowHierarchy(TObject *AnObject);
};
class TListHierarchy: public TMyObject
{
private:
TStringList *FList;
protected:
virtual void PrintString(AnsiString S)
{ FList->Add(S); }
public:
TListHierarchy() { FList = new TStringList(); }
__fastcall virtual ~TListHierarchy() { delete FList; }
TStringList *GetHierarchy(TObject *AnObject)
{ FList->Clear(); ShowHierarchy(AnObject); return FList; }
};
class __declspec(delphiclass) TVCLHierarchy;
class THierarchy: public TMyObject
{
friend TVCLHierarchy;
int FTextColor;
int FBackColor;
protected:
virtual __fastcall void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual __fastcall void SetBackColor(int Color)
{ FBackColor = Color; textbackground(FBackColor); }
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
virtual void ClrScr();
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
class TVCLHierarchy : public THierarchy
{
TMemo *FMemo;
protected:
virtual __fastcall void SetTextColor(int Color)
{ FTextColor = Color; FMemo->Font->Color = TColor(FTextColor); }
virtual __fastcall void SetBackColor(int Color);
public:
TVCLHierarchy(TMemo *AMemo): THierarchy() { FMemo = AMemo; }
virtual void PrintString(AnsiString S)
{ FMemo->Lines->Add(S); }
virtual void ClrScr() { FMemo->Clear(); }
};
#endif
Listing 19.7. The new code for the MyObject
implementation.
///////////////////////////////////////
// MyObject.cpp
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "myobject.h"
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy(TObject *AnObject)
{
TClass AClass;
AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();
PrintString(AClassName);
AClass = AnObject->ClassParent();
while (True)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
if (AClassName == "TObject")
break;
AClass = AClass->ClassParent();
}
}
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
void THierarchy::ClrScr()
{
clrscr();
}
void __fastcall TVCLHierarchy::SetBackColor(int Color)
{
FBackColor = Color;
FMemo->Color = TColor(FBackColor);
}
Listing 19.8. The code for the HIERARCHY header
file.
///////////////////////////////////////
// Main.h
// 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>
#include "MyObject.h"
class TForm1 : public TForm
{
__published:
TButton *ShowHierarchyBtn;
TMemo *Memo1;
TPanel *Panel1;
void __fastcall ShowHierarchyBtnClick(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall TForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern TForm1 *Form1;
#endif
Listing 19.9. The code for the HIERARCHY program.
///////////////////////////////////////
// Main.cpp
// 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)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete Hierarchy;
}
void TForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
When you run this program, it will display the hierarchy of the TForm
object in a memo control. It does so by creating an aggregate of the
THierarchy object and the TForm1 object.
Creating Friend Objects
Here is the declaration for the descendant of THierarchy that works
in Windows:
class TVCLHierarchy : public THierarchy
{
TMemo *FMemo;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; FMemo->Font->Color = TColor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; FMemo->Color = TColor(FBackColor); }
public:
TVCLHierarchy(TMemo *AMemo): THierarchy() { FMemo = AMemo; }
virtual void PrintString(AnsiString S)
{ FMemo->Lines->Add(S); }
virtual void ClrScr() { FMemo->Clear(); }
};
Several changes to this program should jump right out at you. First, notice
that the constructor now takes a parameter. This parameter is the memo control
that will be used to display text to the user. The SetTextColor,
SetBackColor, PrintString, and ClrScr methods have all
been rewritten to take advantage of this new control.
As a result, the only parts of the original object that have come through
unchanged are as shown in the following pseudocode:
class THierarchy: public TMyObject}
{
int FTextColor;
int FBackColor;
public:
void ShowHierarchy(TObject *AnObject); // inherited from TMyObject
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
In this particular case, it is not possible to use the TextColor and
BackColor properties to access the private FTextColor and
FBackColor data stores. Instead, I declare TVCLHierarchy to be a
friend of THierarchy:
class TVCLHierarchy;
class THierarchy: public TMyObject
{
friend TVCLHierarchy;
int FTextColor;
int FBackColor;
... // Code omitted here
}
Notice that TVCLHierarchy is declared as a friend in the first line
of the THierarchy declaration. This means that TVCLHierarchy
has access to the private data of THierarchy. Descendants of
TVCLHierarchy will not inherit this trait.
Using Aggregation
Rather than use multiple inheritance to access TVCLHierarchy,
TForm1 declares a field of this type:
class TForm1 : public TForm
{
... // Code omitted here
TVCLHierarchy *Hierarchy;
void ShowHierarchy();
public:
virtual __fastcall TForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
I have also added a method called ShowHierarchy and a new property
called Hierarchy so that descendants of TForm1 can have access
to the BackColor and TextColor properties of TVCLHierarchy.
A fine point of object design could be debated here. In this particular case,
I have created a property of type TVCLHierarchy that is exposed to
consumers of TForm1. Alternatively, I could have completely hidden
TVCLHierarchy behind a set of properties that provided access to the
FTextColor and FBackColor fields of THierarchy:
class TForm1 : public TForm
{
... // Code omitted here
TVCLHierarchy *Hierarchy;
void ShowHierarchy(TObject *Object);
int GetHierarchyBackColor();
void SetHierarchyBackColor(int Color);
int GetHierarchyTextColor();
void SetHierarchyTextColor(int Color);
public:
virtual __fastcall TForm1(TComponent* Owner);
__property int HierarchyTextColor=
{read=GetHierarchyTextColor, write=SetHierarchyTextColor};
__property int HierarchyBackColor=
{read=GetHierarchyBackColor, write=SetHierarchyBackColor};
};
This technique is very pure from an OOP-based point of view, and it is
perhaps a more classic and complete example of aggregation than the one I use.
However, it is not strictly necessary to create the HierarchyTextColor
and HierarchyBackColor properties. Instead, I can make the
Hierarchy object public by making it a property of type TVCLHierarchy.
The reason this approach is acceptable is because FHierarchy, as well
as the FTextColor and FBackColor fields of THierarchy,
are all still hidden behind properties.
It would be wrong, however, to make FHierarchy public or to allow
users to directly access the FTextColor or FBackColor fields
of THierarchy. The reason this is wrong is because it exposes your data
to the public, thereby limiting your choices when it comes time to maintain or
improve your program.
The technique shown here of exposing TVCLHierarchy via a property
shows up everywhere in the VCL. Most notably, it is present in the Items
or Lines fields of TListBox, TComboBox, and TMemo.
The point here is that you want to use properties to protect your data and your
implementation, but you don't want to take things so far that your code becomes
needlessly bloated.
However, I do not feel that it would be incorrect or wrong to completely hide
FHierarchy from consumers of TForm1 and to instead expose its
properties via properties of TForm1. The great advantage of this
technique would be that it would allow a seamless aggregation of TForm1
and TVCLHierarchy.
-
NOTE: If you sense me waffling a
bit here, that is because I do not believe there is a hard and fast answer as
to what is best in a situation like this. It happens that there are good
arguments on both sides. Component design is an art, not a science. The moment
it becomes a science, most programmers are going to be out of a job.
In the constructor for TForm1, you should create the
TVCLHierarchy object:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
As you can see, I have chosen to hard code default values for the
BackColor and TextColor properties of TVCLHierarchy into
this constructor. This is not necessarily the best thing to do from the point of
view of a descendant of TForm1, but it will not cause any problems in
the simple example I am creating here.
At last, I am able to create a wrapper for the ShowHierarchy method:
void TForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
Once again, you could probably just as easily let consumers of TForm1
access this method via the Hierarchy property. However, I provide this
method in part because it is easier to use than a property, and in part because
I want to illustrate explicitly how aggregation works.
Finally, I have included a method of TForm1 that calls the
TForm1::ShowHierarchy method:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
This method does nothing to forward my example of aggregation, and I include
it solely for expediency's sake. A real-world example of aggregation would leave
TForm1 without features of this type and would instead expect you to
inherit from it in order to access the functionality exposed via Hierarchy
and ShowHierarchy. For an example of this type of program, see the
HIERARCHY2 program in the Chap19 directory.
A reader could object that rather than using aggregation, I could have
declared a single method that looked like this:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
THierarchy *Hierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
Hierarchy->ShowHierarchy();
delete Hierarchy;
}
Once again, I have to confess that this probably is a simpler technique in
this particular case. However, if you created descendants of TForm1,
aggregation would allow them to call the ShowHierarchy method with one
line of code. It would, in effect, add the functionality of ShowHierarchy
to TForm, so that TForm1 appeared to be an "aggregate" of two
objects. Admittedly, the simplicity of the example I show here makes these
advantages difficult to discern, but when you are working with larger, more
complex objects, this technology really starts to shine. OOP is about managing
complexity, but it is easier to teach OOP if you use simple examples.
By this time, you should have a fairly good grasp of inheritance and
hierarchies. The key point to understand is that, in general, a child object
inherits the capability to use any of its parent's methods. In other words, it
comes into an inheritance, where the inheritance is the methods, fields, and
properties of its parent. Except for TObject itself, all VCL objects
have parents that can trace their roots back to TObject.
Form Inheritance and Aggregation
Form inheritance is a powerful technique used by VCL programs. Here is a
description of how to use it.
- 1. In the HIERARCHY2 program, I remove the ShowHierarchy
button from the Form1 file used in the HIERARCHY program.
2. I also rename Form1 to HierarchyForm1. I then save
the old Form1 file as a new file called HierarchyForm.
3. Next I start a new project and remove its main form.
4. I add the HierarchyForm to this new project and use the
Object Repository to create a new form that descends from HierarchyForm1.
To do this, choose File | New, click on the HIERARCHY2 page in the Object
Repository, and choose to inherit a new form from HierarchyForm1.
5. Save the new form you have created as Main.cpp.
6. Open the Options | Project menu and use the Forms page to set
Form1 as the main form for your application. In other words, make the
descendant of the HierarchyForm the main form for your application.
When you are done, the declaration for your main form should look like this:
///////////////////////////////////////
// HierarchyForm.h
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef HierarchyFormH
#define HierarchyFormH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "Myobject.h"
class THierarchyForm1 : public TForm
{
published:
TMemo *Memo1;
TPanel *Panel1;
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
protected:
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall THierarchyForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern THierarchyForm1 *HierarchyForm1;
#endif
Go into the declaration for HierarchyForm1 and make sure that
ShowHierarchy is declared in the public or protected section.
Now add a button to the main form called ShowHierarchy and add the
following event handler to its OnClick event:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
Now run the program, and when you click on ShowHierarchy, you should
get the following result:
TForm1
THierarchyForm1
TForm
TScrollingWinControl
TWinControl
TControl
TComponent
TPersistent
TObject
As you can see, the hierarchy now goes from TForm1 to
HierarchyForm1 to TForm, and so on. TForm1 is descended
from THierarchyForm1, and it therefore inherits the ability to all the
things a regular form can do, plus it can show its own hierarchy.
Once again, there are a number of quibbles you could make here. For instance,
it might be better to remove the visual controls from the HierarchyForm,
and so on. However, the main point of this exercise is merely to illustrate how
form inheritance works, and the code shown here does that.
For the sake of clarity, I have included the complete code to the program in
Listings 19.10 through 19.14. Remember that you need to use the visual tools to
inherit from the HierarchyForm, or Form1 will not have the
correct appearance.
Listing 19.10. The header for the HierarchyForm from
the HIERARCHY2 program.
///////////////////////////////////////
// HierarchyForm.h
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef HierarchyFormH
#define HierarchyFormH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "Myobject.h"
class THierarchyForm1 : public TForm
{
__published:
TMemo *Memo1;
TPanel *Panel1;
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
protected:
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall THierarchyForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern THierarchyForm1 *HierarchyForm1;
#endif
Listing 19.11. The main form for the HierarchyForm.
///////////////////////////////////////
// HierarchyForm.cpp
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "HierarchyForm.h"
#pragma resource "*.dfm"
THierarchyForm1 *HierarchyForm1;
__fastcall THierarchyForm1::THierarchyForm1(TComponent* Owner)
: TForm(Owner)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
void __fastcall THierarchyForm1::FormDestroy(TObject *Sender)
{
delete Hierarchy;
}
void THierarchyForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
Listing 19.12. The header for the programs main
form.
///////////////////////////////////////
// Main.h
// Hierarchy2
// 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>
#include "Myobject.h"
#include "HierarchyForm.h"
class TForm1 : public THierarchyForm1
{
__published:
TButton *ShowHierarchyBtn;
void __fastcall ShowHierarchyBtnClick(TObject *Sender);
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 19.13. The main form for the program.
///////////////////////////////////////
// Main.cpp
// Hierarchy2
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "HierarchyForm.h"
#pragma link "HierarchyForm"
#pragma link "HierarchyForm"
#pragma resource "*.dfm"
TForm1 *Form1;
//--------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: THierarchyForm1(Owner)
{
}
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
//--------------------------------------------------------------------------
Listing 19.14. The project source for the HIERARCHY2
program.
#include <vcl\vcl.h>
#pragma hdrstop
USERES("Hierarchy2.res");
USEFORM("Main.cpp", Form1);
USEFORM("HierarchyForm.cpp", HierarchyForm1);
USEUNIT("..\..\Utils\MyObject.cpp");
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->CreateForm(__classid(THierarchyForm1), &HierarchyForm1);
Application->Run();
}
catch (Exception &exception)
{
MessageBox(NULL, exception.Message.c_str(), Application->Title.c_str(),
MB_OK);
}
return 0;
}
I have included the complete source for this program so you can double-check
your work against it in case anything goes wrong. The key parts you need to
concentrate on are the class declarations in the headers, the declarations for
the global variable for the HierarchyForm, and the headers for the
various methods used in the forms. The actual implementation of the methods was
already cleared up in the HIERARCHY program, so you don't need to check them.
The HIERARCHY2 program is shown in Figure 19.6.
FIGURE 19.6.
The HIERARCHY2 program at runtime.
You should spend some time adding controls to the HierarchyForm and
watching how they then automatically appear on the main form for the program.
Notice that if you move a control on the HierarchyForm, the
corresponding control on the main form will also move. If you move a control on
the main form, however, that breaks the connection between that control and the
corresponding control on the HierarchyForm. To restore the connection,
right-click on the control in the main form and choose Revert To Inherited from
the menu.
You can disconnect and revert properties one at a time if you wish. For
instance, if you move the left side of a component on the main form, that will
break the connection for that one property, but it will leave the remaining
properties still connected to the HierarchyForm. To restore the
Left property, choose that property in the Object Inspector with the right
mouse button and select Revert To Inherited from the menu. To test this, you
might want to drop a single button in the middle of the HierarchyForm,
then switch back to the main form and work on changing just one of the inherited
button's properties, such as its Left property or its Caption.
Summary
In this chapter, you have had a good chance to start working with
object-oriented programming. However, there are still several big topics to
tackle, including encapsulation and polymorphism. Rather than trying to cover
such big topics inside this already lengthy chapter, I have decided to break
things up and give them their own chapters where they can have plenty of room to
unfold naturally.
Plenty of material was covered in this chapter, including inheritance,
virtual methods, aggregation, and form inheritance. If you are new to this
material, it would only be natural that some of it did not sink in the first
time around. If so, you can either re-read the chapter or go on to the next
chapter and see if some of it doesn't start to become clear when you work with
new examples that approach the material from a slightly different angle.
Inheritance and virtual methods are everywhere in the BCB, and the more you
work with them, the easier it will be to understand the principles involved.
However, this is material that you must master if you want to be good at using
BCB--and particularly if you want to start creating your own components.
|