Inheriting from Existing Types

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.

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).

Warning 

If you redefine the same data type in two different units, the types won't be compatible, even if their names are identical. A program using two equally named types from two different units will be a nightmare to compile and debug.

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!!!


Part I: Foundations