Inheriting from Existing Types
You'll often need to use a slightly different version of an existing class. For example, you might need to add a new method or slightly change an existing one. If you copy and paste the original class and then modify it (certainly a terrible programming practice, unless there is a specific reason to do so), you'll duplicate your code, bugs, and headaches. Instead, in such a circumstance you should use a key feature of OOP: inheritance. To inherit from an existing class in Delphi, you only need to indicate that class at the beginning of the declaration of the new class. For example, this is done each time you create a new form:
type
TForm1 = class(TForm)
end;
This definition indicates that the TForm1 class inherits all the methods, fields, properties, and events of the TForm class. You can call any public method of the TForm class for an object of the TForm1 type. TForm, in turn, inherits some of its methods from another class, and so on, up to the TObject base class.As an example of inheritance, you can derive a new class from TDate and modify its GetText function. You can find this code in the Dates unit of the NewDate example:
type
TNewDate = class (TDate)
public
function GetText: string;
end;
To implement the new version of the GetText function, I used the FormatDateTime function, which uses (among other features) the predefined month names available in Windows; these names depend on the user's regional and language settings. (Many of these regional settings are copied by Delphi into constants defined in the library, such as LongMonthNames, ShortMonthNames, and others you can find under the "Currency and date/time formatting variables" topic in the Delphi Help file.) Here is the GetText method, where 'dddddd' stands for the long date format:
function TNewDate.GetText: string;
begin
GetText := FormatDateTime ('dddddd', fDate);
end;
Tip | Using regional information, the NewDate program automatically adapts itself to different Windows user settings. If you run this program on a computer with regional settings referring to a language other than English, it will automatically show month names in that language. To test this behavior, you just need to change the regional settings. Notice that regional-setting changes immediately affect the running programs. |
Once you have defined the new class, you need to use this new data type in the code of the form of the NewDate example, defining the TheDay object of type TNewDate and creating an object of this new class in the FormCreate method. You don't have to change the code with method calls, because the inherited methods still work exactly in the same way; however, their effect changes, as the new output demonstrates (see Figure 2.6).

Figure 2.6: The output of the NewDate program, with the name of the month and of the day depending on Windows regional settings
Protected Fields and Encapsulation
The code of the GetText method of the TNewDate class compiles only if it is written in the same unit as the TDate class. In fact, it accesses the fDate private field of the ancestor class. If we want to place the descendant class in a new unit, we must either declare the fDate field as protected or add a protected access method in the ancestor class to read the value of the private field.
Many developers believe that the first solution is always the best, because declaring most of the fields as protected will make a class more extensible and will make it easier to write inherited classes. However, this approach violates the idea of encapsulation. In a large hierarchy of classes, changing the definition of some protected fields of the base classes becomes as difficult as changing some global data structures. If 10 derived classes are accessing this data, changing its definition means potentially modifying the code in each of the 10 classes.In other words, flexibility, extension, and encapsulation often become conflicting objectives. When this happens, you should try to favor encapsulation. If you can do so without sacrificing flexibility, that will be even better. Often this intermediate solution can be obtained by using a virtual method, a topic I'll discuss in detail in the section "Late Binding and Polymorphism." If you choose not to use encapsulation in order to obtain faster coding of the inherited classes, then your design might not follow the object-oriented principles.
You've seen that in Delphi, the private and protected data of a class is accessible to any functions or methods that appear in the same unit as the class. For example, consider this class (part of the Protection example):
type
TTest = class
protected
ProtectedData: Integer;
end;
Once you place this class in its own unit, you won't be able to access its protected portion from other units directly. Accordingly, if you write the following code
var
Obj: TTest;
begin
Obj := TTest.Create;
Obj.ProtectedData := 20; // won't compile
the compiler will issue an error message, "Undeclared identifier: 'ProtectedData.'" At this point, you might think there is no way to access the protected data of a class defined in a different unit. However, there is a way. Consider what happens if you create an apparently useless derived class, such as the following:
type
TTestHack = class (TTest);
Now, if you make a direct cast of the object to the new class and access the protected data through it, this is how the code will look:
var
Obj: TTest;
begin
Obj := TTest.Create;
TTestHack (Obj).ProtectedData := 20; // compiles!
This code compiles and works properly, as you can see by running the Protection program. How is it possible for this approach to work? Well, if you think about it, the TTestHack class automatically inherits the protected fields of the TTest base class, and because the TTestHack class is in the same unit as the code that tries to access the data in the inherited fields, the protected data is accessible. As you would expect, if you move the declaration of the TTestHack class to a secondary unit, the program will no longer compile.Now that I've shown you how to do this, I must warn you that violating the class-protection mechanism this way is likely to cause errors in your program (from accessing data that you really shouldn't), and it runs counter to good OOP technique. However, there are times when using this technique is the best solution, as you'll see by looking at the VCL source code and the code of many Delphi components. Two examples that come to mind are accessing the Text property of the TControl class and the Row and Col positions of the DBGrid control. These two ideas are demonstrated by the TextProp and DBGridCol examples, respectively. (These examples are quite advanced, so I suggest that only programmers with a good background in Delphi programming read them at this point in the text—other readers might come back later.) Although the first example shows a reasonable example of using the typecast cracker, the DBGrid example of Row and Col is a counterexample—it illustrates the risks of accessing bits that the class writer chose not to expose. The row and column of a DBGrid do not mean the same thing as they do in a DrawGrid or StringGrid (the base classes). First, DBGrid does not count the fixed cells as actual cells (it distinguishes data cells from decoration), so your row and column indexes will have to be adjusted by whatever decorations are currently in effect on the grid (and those can change on the fly). Second, the DBGrid is a virtual view of the data. When you scroll up in a DBGrid, the data may move underneath it, but the currently selected row might not change.This technique—declaring a local type only so that you can access protected data members of a class—is often described as a hack, and it should be avoided whenever possible. The problem is not accessing protected data of a class in the same unit but declaring a class for the sole purpose of accessing protected data of an existing object of a different class! The danger of this technique is in the hard-coded typecast of an object from a class to a different one.
Inheritance and Type Compatibility
Pascal is a strictly typed language. This means that you cannot, for example, assign an integer value to a Boolean variable, unless you use an explicit typecast. The rule is that two values are type-compatible only if they are of the same data type, or (to be more precise) if their data type refers to a single type definition. To simplify your life, Delphi makes some predefined types assignment compatible: you can assign an Extended to a Double and vice versa, with automatic promotion or demotion (and potential accuracy loss).
There is an important exception to this rule in the case of class types. If you declare a class, such as TAnimal, and derive from it a new class, say TDog, you can then assign an object of type TDog to a variable of type TAnimal. You can do so because a dog is an animal! As a general rule, you can use an object of a descendant class any time an object of an ancestor class is expected. However, the reverse is not legal; you cannot use an object of an ancestor class when an object of a descendant class is expected. To simplify the explanation, here it is again in code terms:
var
MyAnimal: TAnimal;
MyDog: TDog;
begin
MyAnimal := MyDog; // This is OK
MyDog := MyAnimal; // This is an error!!!