35 min read

> "If I have seen further it is by standing on the shoulders of giants."

Chapter 17: Inheritance, Polymorphism, and Virtual Methods

"If I have seen further it is by standing on the shoulders of giants." — Isaac Newton, 1675

In Chapter 16, we took a giant leap: we left the world of records and procedures behind and entered the realm of Object Pascal, where data and behavior live together inside classes. You learned to define classes, create objects, write constructors and destructors, and appreciate the principle of encapsulation — that objects should guard their internal state and expose only a carefully designed interface.

That was powerful. But if every class existed in isolation, we would face a familiar problem: duplication. Consider a game like Crypts of Pascalia. We need a TPlayer class, a TMonster class, and a TNPC class. All three share some fundamental attributes — a name, a position on the map, hit points, a method to describe themselves. Without some mechanism for sharing structure, we would copy and paste the same fields and methods into three separate classes, violating one of programming's most important principles: Don't Repeat Yourself.

This chapter introduces three interlocking concepts that solve this problem and, in the process, transform how you think about program design:

  1. Inheritance — creating new classes that automatically acquire the fields and methods of an existing class
  2. Polymorphism — writing code that operates on objects of different classes through a shared interface, with each class responding in its own way
  3. Virtual methods — the mechanism that makes polymorphism work in Object Pascal

Together, these form the second pillar of object-oriented programming (encapsulation was the first; Chapter 18 will introduce the third: interfaces). By the end of this chapter, you will be able to design class hierarchies, override methods with the virtual/override keywords, process heterogeneous collections of objects through base-class references, use the is and as operators for safe type checking and casting, and apply the Liskov Substitution Principle to evaluate whether your hierarchies are well-designed.

We will also make a major upgrade to PennyWise, introducing TRecurringExpense as a subclass of TExpense — and watch polymorphism make our reporting engine automatically handle both one-time and recurring expenses without a single if statement to distinguish them.

Prerequisites: You must be comfortable with classes, objects, constructors, and destructors from Chapter 16. Familiarity with records (Chapter 11) and procedures/functions (Chapter 7) will also help.


17.1 Inheritance: Building on What Exists

Inheritance is one of those ideas that seems obvious once you hear it, yet profoundly changes how you design software.

The core insight is this: many things in the world exist in hierarchical relationships. A golden retriever is a dog. A dog is a mammal. A mammal is an animal. Each level in this hierarchy adds specificity while retaining everything from the levels above. A golden retriever has everything a dog has (four legs, a tail, the ability to bark) plus some attributes of its own (golden fur, a gentle temperament, an irresistible urge to fetch tennis balls).

In programming, we capture this with the "is-a" relationship. When we say TMonster is a TGameEntity, we mean that every monster has everything a game entity has — and then some. The TGameEntity class is called the base class (or parent class, or superclass), and TMonster is called the derived class (or child class, or subclass).

Why Not Just Copy and Paste?

Suppose we have three game entity types, each needing a Name, X and Y position, HitPoints, and a Describe method. Without inheritance:

type
  TPlayer = class
    Name: string;
    X, Y: Integer;
    HitPoints: Integer;
    Level: Integer;           // Player-specific
    procedure Describe;
  end;

  TMonster = class
    Name: string;
    X, Y: Integer;
    HitPoints: Integer;
    Damage: Integer;          // Monster-specific
    procedure Describe;
  end;

  TNPC = class
    Name: string;
    X, Y: Integer;
    HitPoints: Integer;
    Dialogue: string;         // NPC-specific
    procedure Describe;
  end;

Three classes, three copies of Name, X, Y, HitPoints, and Describe. Now suppose you need to add a MoveTo method. You write it three times. Need to add an IsAlive function? Three times. Find a bug in the position logic? Fix it in three places — and pray you do not forget one.

This is not a minor inconvenience. In real games, entity hierarchies can have dozens of shared fields and methods. The duplication becomes a maintenance nightmare.

The Inheritance Solution

With inheritance, we write the shared parts once:

type
  TGameEntity = class
    Name: string;
    X, Y: Integer;
    HitPoints: Integer;
    constructor Create(AName: string; AX, AY, AHP: Integer);
    procedure MoveTo(NewX, NewY: Integer);
    function IsAlive: Boolean;
    procedure Describe; virtual;
  end;

And then each specific type extends it:

type
  TPlayer = class(TGameEntity)
    Level: Integer;
    Experience: Integer;
    procedure Describe; override;
  end;

  TMonster = class(TGameEntity)
    Damage: Integer;
    procedure Describe; override;
  end;

  TNPC = class(TGameEntity)
    Dialogue: string;
    procedure Describe; override;
  end;

The syntax class(TGameEntity) says: "This class inherits everything from TGameEntity." Each derived class automatically has Name, X, Y, HitPoints, MoveTo, IsAlive, and Describe — without writing a single line of code to get them. Then each derived class adds its own unique fields and provides its own version of Describe.

Write the shared logic once. Reuse it everywhere. Fix a bug in one place. This is the promise of inheritance.

💡 Coming From Python/Java: Python uses class TMonster(TGameEntity):, Java uses class TMonster extends TGameEntity. Object Pascal's class(TGameEntity) syntax is comparable. The concept is identical across all three languages — another instance of Theme 2: the discipline transfers.


17.2 Syntax of Inheritance in Object Pascal

Let us formalize the syntax and explore its details.

Declaring a Derived Class

type
  TDerived = class(TBase)
    // additional fields
    // additional methods
    // overridden methods
  end;

The parenthetical (TBase) is the only syntactic addition. If you omit it, your class implicitly inherits from TObject — the root of every class hierarchy in Free Pascal. We will explore TObject in Section 17.7.

What Gets Inherited

A derived class inherits:

  • All fields (instance variables) from the base class
  • All methods from the base class
  • All properties from the base class

However, visibility rules still apply:

Visibility Accessible in derived class? Accessible from outside?
public Yes Yes
protected Yes No
private No* No
published Yes Yes (with RTTI)

The protected visibility level, which we mentioned briefly in Chapter 16, now becomes essential. Fields and methods declared protected are invisible to outside code but fully visible to derived classes. This is the sweet spot for inheritance: you protect the internal state from the outside world while still allowing subclasses to access what they need.

⚠️ Important Detail: In Free Pascal, private members are actually accessible from other classes within the same unit. This is a historical quirk inherited from Turbo Pascal. If you want truly private members that even subclasses in the same unit cannot access, use strict private. Similarly, strict protected prevents access from other classes in the same unit that are not descendants.

A Complete Example

Here is a minimal but complete inheritance example:

program InheritanceBasics;

{$mode objfpc}{$H+}

type
  TAnimal = class
  protected
    FName: string;
    FSound: string;
  public
    constructor Create(AName, ASound: string);
    procedure Speak;
    function GetName: string;
  end;

  TDog = class(TAnimal)
  private
    FBreed: string;
  public
    constructor Create(AName, ABreed: string);
    function GetBreed: string;
  end;

constructor TAnimal.Create(AName, ASound: string);
begin
  FName := AName;
  FSound := ASound;
end;

procedure TAnimal.Speak;
begin
  WriteLn(FName, ' says: ', FSound);
end;

function TAnimal.GetName: string;
begin
  Result := FName;
end;

constructor TDog.Create(AName, ABreed: string);
begin
  inherited Create(AName, 'Woof!');   // Call parent constructor
  FBreed := ABreed;
end;

function TDog.GetBreed: string;
begin
  Result := FBreed;
end;

var
  Dog: TDog;
begin
  Dog := TDog.Create('Rex', 'German Shepherd');
  Dog.Speak;              // Inherited from TAnimal!
  WriteLn('Breed: ', Dog.GetBreed);  // TDog's own method
  WriteLn('Name: ', Dog.GetName);    // Inherited from TAnimal
  Dog.Free;
end.

Output:

Rex says: Woof!
Breed: German Shepherd
Name: Rex

Notice the inherited keyword in TDog.Create. This calls the parent class's constructor. Without it, FName and FSound would remain uninitialized. We will explore inherited in detail in Section 17.8.

Single Inheritance

Object Pascal supports single inheritance only: a class can have exactly one parent class. You cannot write TDog = class(TAnimal, TPet). This might seem limiting, but it avoids the notorious "diamond problem" that plagues languages with multiple inheritance (like C++). For situations where you need a class to implement multiple contracts, Chapter 18 introduces interfaces, which serve this role elegantly.

Memory Layout: How Inheritance Works Internally

Understanding what happens in memory helps solidify the concept. When you create a TDog object, the runtime allocates a single block of memory that contains:

  1. A pointer to TDog's Virtual Method Table (VMT) — this is how the runtime knows what type the object actually is
  2. All fields from TObject (the ultimate ancestor)
  3. All fields from TAnimal (FName, FSound)
  4. All fields from TDog (FBreed)

The fields are laid out in inheritance order — parent fields first, then child fields. This is why a TAnimal pointer can safely point to a TDog object: the TAnimal fields are in the same positions they would be in a plain TAnimal object. The TDog-specific fields come after, and TAnimal code simply never accesses them.

Memory layout of a TDog instance:
+-------------------+
| VMT pointer       |  --> Points to TDog's virtual method table
+-------------------+
| FName  (string)   |  \
| FSound (string)   |  / TAnimal fields
+-------------------+
| FBreed (string)   |  -- TDog field (added by derived class)
+-------------------+

This layout is why inheritance works without any runtime overhead for field access — the compiler knows exactly where each field is located, just as it would for a non-inherited class. The only "cost" of inheritance is the VMT pointer (one pointer per object) and the VMT itself (one table per class, shared by all instances).

What You Cannot Do in a Derived Class

While a derived class can add fields and methods and override virtual methods, there are things it cannot do:

  • Remove inherited fields or methods — if TAnimal has FName, TDog has FName too, period
  • Change the type of an inherited field — FName is a string in TAnimal, and it stays a string in TDog
  • Reduce visibility — you cannot make a public parent method private in the child
  • Override a non-virtual method — the override keyword requires the parent method to be virtual or dynamic

These restrictions are not arbitrary — they preserve the guarantee that a derived object can always be used where a base object is expected.

🔗 Connection to Theme 2 (Discipline Transfers): These same restrictions apply in Java, C#, Swift, and Kotlin. The mental model you build here — base fields first in memory, visibility can only widen, types cannot change — transfers directly to every mainstream OOP language.


17.3 Method Overriding: virtual, override, and reintroduce

Inheritance gives us shared structure. But shared structure alone is not enough. We need derived classes to be able to replace inherited behavior with their own implementations. A TDog should speak differently from a TCat, even though both inherit Speak from TAnimal.

This is called method overriding, and it requires two keywords working together: virtual and override.

The virtual Keyword

Marking a method as virtual in the base class signals the compiler: "Derived classes may replace this method's implementation."

type
  TAnimal = class
    procedure Speak; virtual;   // This method can be overridden
  end;

Behind the scenes, virtual causes the compiler to add an entry for Speak in the class's Virtual Method Table (VMT) — an internal data structure that maps method names to actual code addresses. When you call a virtual method, the program does not jump directly to the base class implementation. Instead, it looks up the correct implementation in the VMT at runtime. This is called dynamic dispatch or late binding, and it is the engine that drives polymorphism.

The override Keyword

In the derived class, you replace the implementation using override:

type
  TDog = class(TAnimal)
    procedure Speak; override;   // Replace the parent's Speak
  end;

  TCat = class(TAnimal)
    procedure Speak; override;   // Replace with a different version
  end;

procedure TDog.Speak;
begin
  WriteLn('Woof! Woof!');
end;

procedure TCat.Speak;
begin
  WriteLn('Meow...');
end;

The override keyword tells the compiler: "I am intentionally replacing the inherited virtual method." This is not optional — if you want polymorphic behavior, you must use override. The compiler will reject override if the base method is not virtual (or dynamic, which we discuss below).

What Happens Without virtual/override?

If you simply redeclare a method with the same name in a derived class without these keywords, you get method hiding (also called static binding):

type
  TBase = class
    procedure Greet;   // NOT virtual
  end;

  TDerived = class(TBase)
    procedure Greet;   // This hides TBase.Greet — no polymorphism
  end;

With hiding, the method that gets called depends on the variable's declared type, not the object's actual type. This is almost never what you want, and the compiler will warn you about it.

The reintroduce Keyword

If you intentionally want to hide a parent's method (rare, but occasionally useful), use reintroduce to suppress the compiler warning:

type
  TDerived = class(TBase)
    procedure Greet; reintroduce;  // "Yes, I know I'm hiding it"
  end;

Think of reintroduce as a note to the compiler (and to future readers of your code): "This hiding is deliberate, not a mistake."

virtual vs. dynamic

Free Pascal supports two keywords for virtual methods: virtual and dynamic. Both enable method overriding and polymorphism. The difference is purely about internal implementation:

  • virtual methods use a VMT (fast lookup, more memory per class)
  • dynamic methods use a DMT (slower lookup, less memory per class)

In practice, always use virtual unless you have a class hierarchy with hundreds of descendants and many rarely-overridden methods (a situation that almost never arises in typical applications). The performance difference in method lookup is negligible on modern hardware.

Calling the Inherited Implementation

Often, an overridden method wants to extend rather than completely replace the parent's behavior. Use inherited to call the parent version:

procedure TMonster.Describe;
begin
  inherited Describe;   // Call TGameEntity.Describe first
  WriteLn('  Damage: ', FDamage);
  WriteLn('  Hostile: ', FIsHostile);
end;

You can also use inherited without a method name to call the parent's version of the same method with the same parameters:

procedure TMonster.Describe;
begin
  inherited;   // Same as inherited Describe;
  WriteLn('  Damage: ', FDamage);
end;

📊 Mental Model: The Matryoshka Doll. Think of inheritance like nested Russian dolls. The outermost doll is the derived class, and inside it sits the parent, and inside that sits the grandparent, all the way down to TObject. When you call inherited, you are reaching one layer inward to invoke the parent's version. Each layer can do its own work and then delegate to the layer inside.

A Common Mistake: Forgetting virtual in the Base Class

One of the most frequent errors students make is declaring override in the derived class when the base method is not virtual:

type
  TBase = class
    procedure DoSomething;   // NOT virtual — just a regular method
  end;

  TDerived = class(TBase)
    procedure DoSomething; override;   // COMPILE ERROR!
  end;

The compiler will tell you: "No matching virtual method found for override." The fix is to add virtual to the base class declaration. This error message is actually one of Pascal's safety features — it prevents you from accidentally thinking you have polymorphic behavior when you do not.

The reverse mistake — writing virtual in the derived class instead of override — is also wrong:

type
  TBase = class
    procedure DoSomething; virtual;
  end;

  TDerived = class(TBase)
    procedure DoSomething; virtual;   // WRONG: creates a new VMT entry
  end;

This compiles (with a warning), but creates a new virtual method that hides the parent's version instead of replacing it. The derived class now has two VMT entries with the same method name, and the parent's version will still be called through base-class references. Always use override when your intent is to replace.

When NOT to Override

Not every method should be virtual. Consider carefully:

  • Methods that implement invariant behavior — logic that must be the same across all descendants — should be non-virtual. This prevents subclasses from accidentally breaking guarantees.
  • Methods that are implementation details should remain non-virtual. The base class is saying: "This is my business; subclasses should not interfere."
  • Methods called from constructors should be used with caution if virtual. During construction, the VMT may point to the base class rather than the derived class, leading to unexpected behavior. (Free Pascal handles this more safely than C++, but the principle of caution still applies.)

As a rule of thumb: make a method virtual only if you can articulate a reason why subclasses should be able to change its behavior.


17.4 Polymorphism: One Interface, Many Implementations

Now we arrive at the concept that makes all this machinery worthwhile. Polymorphism — from the Greek poly (many) and morph (form) — is the ability to treat objects of different classes through a single, shared interface.

The Key Insight: Base-Class Variables Can Hold Derived Objects

In Object Pascal, a variable of type TGameEntity can hold a reference to any object whose class descends from TGameEntity:

var
  Entity: TGameEntity;
begin
  Entity := TPlayer.Create('Hero', 5, 5, 100);   // A TPlayer in a TGameEntity variable
  Entity.Describe;   // Calls TPlayer.Describe (not TGameEntity.Describe)!
  Entity.Free;

  Entity := TMonster.Create('Goblin', 10, 3, 30);  // Now it holds a TMonster
  Entity.Describe;   // Calls TMonster.Describe!
  Entity.Free;
end;

Even though Entity is declared as TGameEntity, the method call Entity.Describe executes the correct version based on the actual object type at runtime. This is polymorphism in action: one variable, one method call, many possible behaviors.

Polymorphic Arrays: The Real Power

The true power of polymorphism emerges when you process collections of mixed objects:

var
  Entities: array[0..2] of TGameEntity;
  i: Integer;
begin
  Entities[0] := TPlayer.Create('Hero', 5, 5, 100);
  Entities[1] := TMonster.Create('Goblin', 10, 3, 30);
  Entities[2] := TNPC.Create('Merchant', 7, 7, 50);

  for i := 0 to 2 do
  begin
    Entities[i].Describe;   // Each one calls its OWN Describe
    WriteLn('---');
  end;

  for i := 0 to 2 do
    Entities[i].Free;
end;

This loop does not know or care whether it is dealing with a player, a monster, or an NPC. It just calls Describe, and each object responds appropriately. If you later add a TTrap class that descends from TGameEntity, this loop requires zero modifications to handle it. That is the Open/Closed Principle in action: the code is open for extension (add new entity types) but closed for modification (existing code does not change).

What Do You Think Will Happen?

Before reading on, predict the output of this program:

var
  Entities: array[0..1] of TGameEntity;
begin
  Entities[0] := TPlayer.Create('Hero', 5, 5);
  Entities[1] := TMonster.Create('Goblin', 10, 3, 30);
  Entities[0].Describe;
  Entities[1].Describe;
end.

If you said "It calls TPlayer.Describe for the first one and TMonster.Describe for the second one, even though both are declared as TGameEntity" — you have internalized polymorphism. The key insight bears repeating: the variable type determines what methods you can call; the object type determines which implementation runs.

How the VMT Makes It Work

Let us trace what happens at the machine level when you call Entities[0].Describe:

  1. The program reads the VMT pointer from the object (the first field in every object's memory)
  2. This pointer leads to TPlayer's VMT, not TGameEntity's
  3. The program looks up the entry for Describe in TPlayer's VMT
  4. That entry points to TPlayer.Describe's compiled code
  5. The program jumps to that code and executes it

Step 2 is the critical one — because Entities[0] actually holds a TPlayer object, the VMT pointer points to TPlayer's table. If Entities[0] held a TMonster, the same code would find TMonster's VMT and call TMonster.Describe instead.

This lookup adds a tiny amount of overhead compared to a direct (non-virtual) method call — one pointer dereference and one table lookup. On modern hardware, this is on the order of nanoseconds. The flexibility it provides is worth far more than this microscopic cost.

Without Polymorphism: The Alternative Is Ugly

Consider what you would write without polymorphism:

// UGLY: Without polymorphism
for i := 0 to High(Entities) do
begin
  if Entities[i] is TPlayer then
    TPlayer(Entities[i]).DescribePlayer
  else if Entities[i] is TMonster then
    TMonster(Entities[i]).DescribeMonster
  else if Entities[i] is TNPC then
    TNPC(Entities[i]).DescribeNPC;
end;

Every time you add a new entity type, you must find and update every if-else chain in your entire codebase. Miss one, and you have a bug. Polymorphism eliminates this fragility entirely.

💡 Theme 5 (A+DS=P) Connection: Wirth's insight shines here. An array of base-class references is a data structure, and the polymorphic method calls are the algorithm. The combination — iterating over a heterogeneous collection and letting virtual dispatch select the right behavior — is a pattern you will use in nearly every non-trivial program.

A Polymorphic Game Loop

Here is a sketch of what the Crypts of Pascalia main loop might look like:

procedure RunGameLoop(Entities: array of TGameEntity);
var
  i: Integer;
begin
  for i := 0 to High(Entities) do
  begin
    if Entities[i].IsAlive then
    begin
      Entities[i].Update;       // Virtual: player reads input, monster uses AI
      Entities[i].Describe;     // Virtual: each describes itself differently
    end;
  end;
end;

One loop. Any number of entity types. Every entity handles its own update logic. This is elegant, extensible, and maintainable.


17.5 Abstract Methods and Classes

Sometimes a base class wants to declare a method but cannot provide a meaningful default implementation. What does TGameEntity.Describe do in the general case? There is no sensible answer — the description depends entirely on whether the entity is a player, monster, or NPC.

For these situations, Object Pascal provides abstract methods:

type
  TGameEntity = class
    // ...
    procedure Describe; virtual; abstract;
  end;

An abstract method has no implementation in the base class. It is a promise: "Every concrete descendant will provide this method." If a derived class fails to override an abstract method, the compiler issues a warning, and calling the method at runtime will cause an "Abstract method called" error — a crash that is deliberately dramatic to help you find the mistake.

When to Use Abstract

Use abstract methods when:

  1. The base class defines a contract (what to do) but not an implementation (how to do it)
  2. Every subclass must provide its own version — there is no reasonable default
  3. You want the compiler to help ensure completeness

Common abstract patterns:

type
  TShape = class
    function Area: Double; virtual; abstract;
    function Perimeter: Double; virtual; abstract;
    procedure Draw; virtual; abstract;
  end;

Every shape has an area, perimeter, and drawing method — but the formulas are entirely different for circles, rectangles, and triangles. Making these abstract forces each TShape descendant to implement all three.

Abstract Classes

A class that contains one or more abstract methods is informally called an abstract class. You can create variables of abstract class types (for polymorphism), but you should not create instances of the abstract class itself — the abstract methods would have no implementation.

var
  Shape: TShape;              // Fine — variable declaration
begin
  Shape := TCircle.Create(5.0);    // Fine — TCircle overrides everything
  // Shape := TShape.Create;       // Compiles, but DANGEROUS — abstract methods
end;

⚠️ Free Pascal Quirk: Free Pascal allows you to instantiate a class with abstract methods — it compiles with only a warning, not an error. This is different from languages like Java and C#, which enforce abstract classes at compile time. Be vigilant: treat the compiler warning as an error. If you call an abstract method on such an instance, your program will crash with an "Abstract error" at runtime.

Partially Abstract Classes

A class can mix abstract and concrete methods. This is common and useful:

type
  TGameEntity = class
    FName: string;
    FHitPoints: Integer;
    constructor Create(AName: string; AHP: Integer);
    function IsAlive: Boolean;           // Concrete: same for all entities
    procedure MoveTo(X, Y: Integer);     // Concrete: same for all entities
    procedure Describe; virtual; abstract;  // Abstract: each entity is different
    procedure Update; virtual; abstract;    // Abstract: each entity is different
    procedure TakeDamage(Amount: Integer); virtual;  // Virtual with default
  end;

Here, IsAlive and MoveTo have sensible, universal implementations. Describe and Update have no reasonable default. TakeDamage has a default (reduce HP) but can be overridden (for example, a TPlayer might reduce damage based on armor).

This mix of abstract and concrete methods creates what is known as a partially abstract class — it provides some behavior while requiring subclasses to fill in the rest. This is one of the most practical and common patterns in OOP.

Abstract Methods and the Open/Closed Principle

Abstract methods embody the Open/Closed Principle: the base class defines a framework that is open for extension (add new subclasses with different implementations) but closed for modification (the base class code never changes when you add new entity types).

When you write:

procedure RunGame(Entities: array of TGameEntity);
var i: Integer;
begin
  for i := 0 to High(Entities) do
    Entities[i].Update;   // Calls abstract Update — resolved at runtime
end;

This code is closed for modification — it will never need to change. But it is open for extension — you can add TTrap, TProjectile, TBoss, or any other TGameEntity descendant, and RunGame will handle it automatically. This is the kind of code architecture that scales from a small student project to a production game engine.


17.6 Type Checking and Casting (is, as)

Polymorphism lets us treat different object types uniformly. But sometimes we need to go the other direction — to determine an object's actual type and access type-specific members. Object Pascal provides two operators for this.

The is Operator (Type Checking)

The is operator tests whether an object is of a particular class (or a descendant of that class):

var
  Entity: TGameEntity;
begin
  Entity := TPlayer.Create('Hero', 5, 5, 100);

  if Entity is TPlayer then
    WriteLn('This is a player');

  if Entity is TGameEntity then
    WriteLn('This is a game entity');    // Also true!

  if Entity is TMonster then
    WriteLn('This is a monster');        // False — not printed
end;

Notice that Entity is TGameEntity returns True — because a TPlayer is a TGameEntity (by inheritance). The is operator respects the entire inheritance chain.

The as Operator (Type Casting)

The as operator performs a safe downcast — converting a base-class reference to a derived-class reference, with a runtime check:

var
  Entity: TGameEntity;
  Player: TPlayer;
begin
  Entity := TPlayer.Create('Hero', 5, 5, 100);

  Player := Entity as TPlayer;   // Safe cast
  WriteLn('Level: ', Player.Level);   // Access TPlayer-specific field

  // This would raise an EInvalidCast exception:
  // Monster := Entity as TMonster;   // CRASH! Entity is not a TMonster
end;

If the cast fails (the object is not of the target type), as raises an EInvalidCast exception. This is safer than a hard cast like TPlayer(Entity), which blindly reinterprets the pointer and can cause memory corruption if the types do not match.

The Safe Pattern: is + as

The idiomatic pattern combines both operators:

procedure HandleEntity(Entity: TGameEntity);
begin
  if Entity is TPlayer then
  begin
    WriteLn('Player level: ', (Entity as TPlayer).Level);
    WriteLn('Experience: ', (Entity as TPlayer).Experience);
  end
  else if Entity is TMonster then
    WriteLn('Monster damage: ', (Entity as TMonster).Damage)
  else if Entity is TNPC then
    WriteLn('NPC says: ', (Entity as TNPC).Dialogue);
end;

⚠️ Design Smell Alert: If you find yourself writing long is/as chains, that is usually a sign that you should be using polymorphism instead. The whole point of virtual methods is to avoid type-checking cascades. Use is/as sparingly — for exceptional situations where genuinely type-specific behavior is needed (like saving different entity types to different file formats, or a GUI inspector panel that shows type-specific properties).

A Practical Example: Entity Interaction System

Let us see is and as used in a realistic scenario — an interaction system for Crypts of Pascalia where the player interacts with whatever entity is in front of them:

procedure PlayerInteract(Player: TPlayer; Target: TGameEntity);
begin
  WriteLn(Player.Name, ' approaches ', Target.Name, '...');

  if Target is TNPC then
  begin
    WriteLn((Target as TNPC).Talk);
    if (Target as TNPC).HasQuest then
      WriteLn('  [A quest marker glows above their head]');
  end
  else if Target is TMonster then
  begin
    WriteLn('  ', Target.Name, ' snarls aggressively!');
    WriteLn('  Combat begins!');
    { Combat logic would go here }
  end
  else
    WriteLn('  Nothing interesting happens.');
end;

This is one of those cases where is/as is genuinely appropriate. The interaction behavior depends fundamentally on the target type in a way that virtual methods alone cannot capture — because PlayerInteract is about the relationship between two entities, not the behavior of a single entity. The player talks to NPCs but fights monsters. That distinction belongs in the interaction system, not in the entity classes themselves.

Hard Casts vs. as

You may see code that uses a hard cast: TPlayer(Entity). This performs no runtime check — it simply tells the compiler "trust me, this is a TPlayer." If you are wrong, the result is undefined behavior: corrupted data, mysterious crashes, or (worst of all) code that seems to work until it catastrophically fails in production.

Rule of thumb: Use as when you need a safe, checked downcast. Use hard casts only when you have already verified the type with is and performance is critical:

if Entity is TPlayer then
  TPlayer(Entity).GainExperience(50);   // Safe: we just checked

Upcasting: Always Safe, Always Implicit

While downcasting (base to derived) requires is/as, upcasting (derived to base) is always safe and happens automatically:

var
  Player: TPlayer;
  Entity: TGameEntity;
begin
  Player := TPlayer.Create('Hero', 5, 5);
  Entity := Player;   // Upcast: TPlayer -> TGameEntity. Always safe.
  Entity.Describe;     // Calls TPlayer.Describe (virtual dispatch)
end;

No cast operator is needed. This is because every TPlayer is a TGameEntity — there is nothing that could go wrong. Upcasting is so natural in Object Pascal that you barely notice it happening. Every time you pass a TPlayer to a function expecting a TGameEntity, you are upcasting.


17.7 The TObject Root Class

Every class in Free Pascal ultimately descends from TObject, even if you do not explicitly write it. These two declarations are identical:

type
  TMyClass = class          // Implicitly inherits from TObject
  end;

  TMyClass = class(TObject) // Explicitly — same thing
  end;

TObject provides a set of fundamental methods that every class inherits. The most important ones:

Method Purpose
Create Default constructor (does nothing; override to initialize fields)
Free Safe destructor call (checks for nil before calling Destroy)
Destroy The actual destructor (override this, not Free)
ClassName Returns the class name as a string (e.g., 'TPlayer')
ClassType Returns the class reference
InheritsFrom(AClass) Returns True if the object's class descends from AClass
ClassParent Returns the parent class reference

ClassName in Practice

The ClassName method is incredibly useful for debugging and logging:

procedure LogEntity(Entity: TGameEntity);
begin
  WriteLn('[', Entity.ClassName, '] ', Entity.Name,
          ' at (', Entity.X, ',', Entity.Y, ')');
end;

Output might be:

[TPlayer] Hero at (5,5)
[TMonster] Goblin at (10,3)
[TNPC] Merchant at (7,7)

No is checks needed — ClassName returns the actual runtime type as a string.

Free vs. Destroy

Always call Free, never Destroy directly. The difference: Free checks whether Self is nil before calling Destroy. If you have a variable that might not have been assigned, Free handles it safely:

var
  Entity: TGameEntity;
begin
  Entity := nil;
  Entity.Free;     // Safe: does nothing
  // Entity.Destroy; // CRASH: nil dereference!
end;

InheritsFrom

The InheritsFrom method is a method-based equivalent of the is operator, but it works on class references rather than instances:

if TPlayer.InheritsFrom(TGameEntity) then
  WriteLn('TPlayer descends from TGameEntity');

This can be useful in factory patterns and plugin systems where you work with class references rather than instances.

Why TObject Matters

You might wonder: why have a universal base class? Several reasons:

  1. Universal type compatibility. Any class can be stored in a TObject variable. This enables generic containers (before generics were added to the language) and frameworks that operate on any object.

  2. Guaranteed interface. Every object supports ClassName, Free, and InheritsFrom. You never need to check whether an object supports these — it always does.

  3. Infrastructure hooks. TObject provides the foundation that the runtime system needs: memory allocation, RTTI (Runtime Type Information), and the VMT structure.

  4. Debugging. ClassName is invaluable for logging, error messages, and debugging. When something goes wrong, knowing the actual type of an object often immediately reveals the problem.

Not every language takes this approach. C++ has no universal base class (though many frameworks define their own, like Qt's QObject). Python's object base class is similar to Pascal's TObject. Java's java.lang.Object is the closest parallel — it provides toString(), equals(), and getClass() just as TObject provides ClassName and InheritsFrom.

Exploring an Object's Class at Runtime

Free Pascal provides rich runtime type information through TObject. Here is a debugging helper that leverages this:

procedure DebugPrint(Obj: TObject);
begin
  if Obj = nil then
    WriteLn('  (nil)')
  else
  begin
    WriteLn('  Class: ', Obj.ClassName);
    WriteLn('  Parent: ', Obj.ClassParent.ClassName);
    WriteLn('  Instance size: ', Obj.InstanceSize, ' bytes');
  end;
end;

This procedure works with any object of any class — because every class descends from TObject. You could pass it a TPlayer, a TExpense, a TStringList, or any other object, and it would print useful debugging information.


17.8 Constructors and Destructors in Hierarchies

When you have a chain of inheritance — TObject -> TGameEntity -> TMonster — object creation and destruction require careful coordination. Each level in the hierarchy may have its own initialization and cleanup needs.

Constructor Chains

A derived class constructor should call the parent constructor using inherited:

type
  TGameEntity = class
  protected
    FName: string;
    FX, FY: Integer;
    FHitPoints: Integer;
  public
    constructor Create(AName: string; AX, AY, AHP: Integer);
    destructor Destroy; override;
  end;

  TMonster = class(TGameEntity)
  private
    FDamage: Integer;
    FLootTable: TStringList;
  public
    constructor Create(AName: string; AX, AY, AHP, ADamage: Integer);
    destructor Destroy; override;
  end;

constructor TGameEntity.Create(AName: string; AX, AY, AHP: Integer);
begin
  inherited Create;   // Call TObject.Create
  FName := AName;
  FX := AX;
  FY := AY;
  FHitPoints := AHP;
end;

constructor TMonster.Create(AName: string; AX, AY, AHP, ADamage: Integer);
begin
  inherited Create(AName, AX, AY, AHP);   // Call TGameEntity.Create
  FDamage := ADamage;
  FLootTable := TStringList.Create;
end;

The call chain flows from most derived to most base: TMonster.Create calls TGameEntity.Create, which calls TObject.Create. Each level initializes its own fields.

💡 Best Practice: Always call inherited Create at the beginning of your constructor (so that parent fields are initialized before you use them) and call inherited Destroy at the end of your destructor (so that you clean up your own resources before the parent cleans up its resources).

Destructor Chains

Destructors run in reverse order — derived class first, then parent:

destructor TGameEntity.Destroy;
begin
  // Clean up TGameEntity-specific resources (if any)
  inherited Destroy;   // Call TObject.Destroy
end;

destructor TMonster.Destroy;
begin
  FLootTable.Free;       // Clean up TMonster-specific resources FIRST
  inherited Destroy;     // Then let TGameEntity clean up
end;

Virtual Destructors: Why Destroy Must Be override

Notice that TGameEntity.Destroy is declared with override — because TObject.Destroy is virtual. This is critically important. When you call Free on a TGameEntity variable that actually holds a TMonster, the virtual dispatch mechanism ensures that TMonster.Destroy gets called (which then chains to TGameEntity.Destroy). Without virtual destructors, calling Free on a base-class reference would only run the base destructor, leaking any resources allocated by the derived class.

var
  Entity: TGameEntity;
begin
  Entity := TMonster.Create('Goblin', 10, 3, 30, 8);
  // ... use entity ...
  Entity.Free;  // Calls TMonster.Destroy (virtual dispatch), then TGameEntity.Destroy
end;

This is one of those places where Pascal's design protects you. Because TObject.Destroy is already virtual, you just need to remember to write override (which the compiler will remind you about if you forget).

Constructors Can Be Virtual Too

Free Pascal allows virtual constructors. This is useful in factory patterns:

type
  TGameEntityClass = class of TGameEntity;   // A class reference type

  TGameEntity = class
    constructor Create(AName: string); virtual;
  end;

  TPlayer = class(TGameEntity)
    constructor Create(AName: string); override;
  end;

function SpawnEntity(EntityClass: TGameEntityClass; AName: string): TGameEntity;
begin
  Result := EntityClass.Create(AName);   // Calls the correct constructor!
end;

var
  E: TGameEntity;
begin
  E := SpawnEntity(TPlayer, 'Hero');   // Creates a TPlayer
end;

The class of syntax declares a class reference type — a variable that holds a class rather than an instance. Combined with virtual constructors, this enables flexible factory patterns that can create different object types without if-else chains.


17.9 Designing Good Class Hierarchies

Knowing the syntax of inheritance is easy. Designing good class hierarchies is an art that takes practice. Let us discuss the principles that separate elegant hierarchies from tangled nightmares.

The Liskov Substitution Principle (LSP)

Named after Barbara Liskov, who formally defined it in 1987, the LSP states:

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.

In plain language: anywhere you use a base class, you should be able to substitute a derived class and have everything still work correctly. A TMonster should work everywhere a TGameEntity is expected. A TRecurringExpense should work everywhere a TExpense is expected.

This sounds obvious, but violations are surprisingly common. The classic counterexample:

type
  TRectangle = class
    FWidth, FHeight: Double;
    procedure SetWidth(W: Double); virtual;
    procedure SetHeight(H: Double); virtual;
    function Area: Double;
  end;

  TSquare = class(TRectangle)
    procedure SetWidth(W: Double); override;   // Sets both width AND height
    procedure SetHeight(H: Double); override;  // Sets both width AND height
  end;

Mathematically, a square "is-a" rectangle. But in code, TSquare violates LSP. If you have code that expects to independently set width and height of a TRectangle, substituting a TSquare breaks that assumption. The overridden SetWidth changes the height too — surprising behavior that violates the caller's expectations.

Is-A vs. Has-A

Not every relationship should be modeled with inheritance. Consider:

  • A TPlayer is-a TGameEntity — inheritance is appropriate
  • A TPlayer has-an TInventory — this should be composition (a field), not inheritance

The "has-a" relationship should use composition: the class contains a field of the other type.

type
  TPlayer = class(TGameEntity)
  private
    FInventory: TInventory;   // Has-a: composition
    FLevel: Integer;
  end;

A useful test: can you say "X is a kind of Y" and have it sound natural? "A player is a kind of game entity" — yes. "A player is a kind of inventory" — obviously not.

📊 Common Design Mistake: Beginners often overuse inheritance. If you find yourself creating deep hierarchies (four or more levels), step back and ask whether composition might be cleaner. Most production code uses shallow hierarchies (two or three levels) with composition for everything else.

Keep Hierarchies Shallow

Every level of inheritance adds complexity. Deep hierarchies (five, six, seven levels deep) become nearly impossible to reason about — a method call might chain through half a dozen inherited calls before reaching the actual implementation. Prefer wide, shallow hierarchies over narrow, deep ones.

Favor virtual Methods for Extension Points

When designing a base class, think carefully about which methods derived classes will need to customize. Mark those virtual. Leave non-virtual any method that represents invariant behavior — logic that should not change across the hierarchy.

type
  TGameEntity = class
    procedure MoveTo(NewX, NewY: Integer);   // Non-virtual: movement physics are universal
    procedure Describe; virtual; abstract;    // Virtual: every entity describes itself differently
    procedure Update; virtual; abstract;      // Virtual: players and monsters update differently
    function IsAlive: Boolean;               // Non-virtual: alive means HP > 0, period.
  end;

This communicates intent clearly: MoveTo and IsAlive are fixed parts of the framework; Describe and Update are customization points.

Practical Heuristics for Hierarchy Design

Here are concrete rules of thumb that professional Pascal developers follow:

1. The "One Sentence" Test. You should be able to describe the inheritance relationship in one natural sentence: "A TRecurringExpense is a kind of expense that repeats at a given frequency." If the sentence sounds awkward or requires qualifications, inheritance may be wrong.

2. The "All Members" Test. Does every public method of the base class make sense for the derived class? If TBird has Fly, does TPenguin.Fly make sense? If not, either the hierarchy is wrong or the method should not be in the base class.

3. The "Future Developer" Test. Imagine a developer who has never seen your derived class. They write code using the base class interface. Would your derived class surprise them? If yes, you may be violating LSP.

4. The "Constructor" Test. Does the derived class constructor feel natural — or are you passing dummy values for base class fields that do not apply? If TSquare.Create takes width and height but ignores height, that is a sign the hierarchy is wrong.

When to Prefer Composition

Inheritance is not always the answer, even when it seems like it might be. Consider these cases where composition is typically better:

{ INHERITANCE (questionable): }
TLoggingExpense = class(TExpense)
  // Adds logging when amount changes
end;

{ COMPOSITION (better): }
TExpenseLogger = class
  FExpense: TExpense;     // Wraps any expense
  FLogFile: TextFile;
  procedure LogChange(OldAmount, NewAmount: Double);
end;

The logging version with inheritance locks you into logging for that specific expense subtype. The composition version can log any expense. In general, if the "extension" is a cross-cutting concern (logging, caching, validation, serialization) rather than a genuine specialization, composition is cleaner.

Another red flag for inheritance: if you find yourself creating a class just to override one method and everything else passes through unchanged. That is a sign you might want a callback, a delegate, or a strategy pattern instead of a subclass.

The Template Method Pattern

A powerful pattern emerges from combining virtual and non-virtual methods. The base class defines a non-virtual method that calls virtual methods:

procedure TGameEntity.TakeTurn;
begin
  if IsAlive then
  begin
    Update;           // Virtual — subclass customizes
    CheckCollisions;  // Non-virtual — same for everyone
    Describe;         // Virtual — subclass customizes
  end;
end;

The overall structure (TakeTurn) is fixed, but the specific steps (Update, Describe) vary by entity type. This is the Template Method design pattern — one of the most frequently used patterns in OOP.


17.10 Project Checkpoint: PennyWise Inheritance

Time to apply everything we have learned to our progressive project. In Chapter 16, we defined TExpense and TBudget classes. Now we will extend the system with inheritance.

The Problem

Rosa Martinelli has a feature request: recurring expenses. Her monthly subscription to Adobe Creative Cloud, her office rent, her phone bill — these expenses happen on a predictable schedule. She wants PennyWise to automatically calculate monthly totals for recurring expenses based on their frequency (weekly, biweekly, monthly, quarterly, yearly).

Tomás has a similar need: his Spotify subscription, his gym membership, his share of the rent — all recurring. He wants to see how much of his monthly budget goes to recurring expenses versus one-time purchases.

The Design

A recurring expense "is-a" expense — it has all the same fields (description, amount, category, date) plus additional information about frequency. This is a textbook inheritance scenario:

TExpense
  |
  +-- TRecurringExpense (adds Frequency, IsActive, CalcMonthlyAmount override)

We will also make CalcMonthlyAmount virtual in TExpense so that our reporting code can call it polymorphically on any expense — one-time or recurring — and get the correct monthly figure.

The Code

type
  TExpenseFrequency = (efWeekly, efBiweekly, efMonthly,
                        efQuarterly, efYearly);

  TExpense = class
  protected
    FDescription: string;
    FAmount: Double;
    FCategory: string;
    FDate: TDateTime;
  public
    constructor Create(ADesc: string; AAmount: Double;
                       ACat: string; ADate: TDateTime);
    destructor Destroy; override;
    function CalcMonthlyAmount: Double; virtual;
    procedure Display; virtual;
    property Description: string read FDescription;
    property Amount: Double read FAmount;
    property Category: string read FCategory;
    property Date: TDateTime read FDate;
  end;

  TRecurringExpense = class(TExpense)
  private
    FFrequency: TExpenseFrequency;
    FIsActive: Boolean;
  public
    constructor Create(ADesc: string; AAmount: Double;
                       ACat: string; ADate: TDateTime;
                       AFreq: TExpenseFrequency);
    function CalcMonthlyAmount: Double; override;
    procedure Display; override;
    function FrequencyToStr: string;
    property Frequency: TExpenseFrequency read FFrequency;
    property IsActive: Boolean read FIsActive write FIsActive;
  end;

The key implementation:

function TExpense.CalcMonthlyAmount: Double;
begin
  Result := FAmount;   // One-time expense: monthly amount is just the amount
end;

function TRecurringExpense.CalcMonthlyAmount: Double;
begin
  case FFrequency of
    efWeekly:     Result := FAmount * 52 / 12;    // ~4.33 weeks/month
    efBiweekly:   Result := FAmount * 26 / 12;    // ~2.17 per month
    efMonthly:    Result := FAmount;
    efQuarterly:  Result := FAmount / 3;
    efYearly:     Result := FAmount / 12;
  end;
end;

Polymorphic Reporting

Now the reporting code becomes beautifully simple:

function CalcTotalMonthly(Expenses: array of TExpense): Double;
var
  i: Integer;
begin
  Result := 0;
  for i := 0 to High(Expenses) do
    Result := Result + Expenses[i].CalcMonthlyAmount;  // Polymorphic!
end;

This function does not know or care whether each expense is one-time or recurring. The virtual method dispatch handles it. When Rosa adds her $54.99/month Adobe subscription as a `TRecurringExpense` and her $12.50 lunch as a plain TExpense, the reporting code processes both correctly without modification.

Notice several design decisions here:

  • FDescription, FAmount, FCategory, and FDate are protected in TExpense, so TRecurringExpense can access them directly if needed
  • CalcMonthlyAmount is virtual in TExpense (returns the raw amount for one-time expenses) and overridden in TRecurringExpense (converts based on frequency)
  • Display is also virtual, so recurring expenses can show their frequency and active status
  • TRecurringExpense.Create calls inherited Create to initialize the base fields, then adds its own
  • The frequency conversion uses precise arithmetic: a weekly $25 payment is $25 * 52 / 12 = $108.33/month, not $25 * 4 = $100/month (which would undercount by over $8)

Testing the Design Against LSP

Does TRecurringExpense satisfy the Liskov Substitution Principle? Let us check:

  • Code that calls CalcMonthlyAmount on a TExpense expects a Double representing the monthly cost. TRecurringExpense returns exactly that — just computed differently. No surprise.
  • Code that calls Display on a TExpense expects information printed to the console. TRecurringExpense prints more information, but that is adding detail, not breaking the contract.
  • Code that reads Amount, Description, Category, or Date gets sensible values. A recurring expense has all of these.

The substitution works perfectly. Any function that processes TExpense objects will handle TRecurringExpense objects correctly and naturally. This is good design.

Running It

var
  Expenses: array[0..3] of TExpense;
  Total: Double;
  i: Integer;
begin
  Expenses[0] := TExpense.Create('Lunch', 12.50, 'Food',  Now);
  Expenses[1] := TRecurringExpense.Create('Adobe CC', 54.99, 'Software',
                   Now, efMonthly);
  Expenses[2] := TRecurringExpense.Create('Gym', 25.00, 'Health',
                   Now, efBiweekly);
  Expenses[3] := TExpense.Create('New Mouse', 45.00, 'Equipment', Now);

  WriteLn('=== Expense Report ===');
  for i := 0 to High(Expenses) do
  begin
    Expenses[i].Display;   // Polymorphic
    WriteLn('  Monthly: $', Expenses[i].CalcMonthlyAmount:0:2);
    WriteLn;
  end;

  Total := CalcTotalMonthly(Expenses);
  WriteLn('Total Monthly Cost: $', Total:0:2);

  for i := 0 to High(Expenses) do
    Expenses[i].Free;
end.

Output:

=== Expense Report ===
[Expense] Lunch - $12.50 (Food)
  Monthly: $12.50

[Recurring] Adobe CC - $54.99/month (Software)
  Monthly: $54.99

[Recurring] Gym - $25.00/biweekly (Health)
  Monthly: $54.17

[Expense] New Mouse - $45.00 (Equipment)
  Monthly: $45.00

Total Monthly Cost: $166.66

Notice how the gym membership ($25 biweekly) automatically converts to $54.17/month. Rosa can now see that her recurring expenses add up to $109.16/month (Adobe + gym), which is a significant portion of her monthly outflow. This is exactly the insight she needed.

🔗 Looking Ahead: In Chapter 18, we will introduce the IExportable interface. Both TExpense and TRecurringExpense will implement it, enabling polymorphic export to CSV, JSON, and plain text — without modifying the class hierarchy at all.


17.11 Chapter Summary

This chapter has been a watershed moment in your journey as a Pascal programmer. You have moved from thinking about isolated classes to thinking about families of related classes that share structure and behavior.

Spaced Review: Revisiting Earlier Concepts

Before we summarize, let us connect back to two earlier topics:

From Chapter 15 — When would you use a stack vs. a queue? A stack is last-in-first-out (LIFO) — use it for undo operations, expression evaluation, or backtracking (think: browser back button). A queue is first-in-first-out (FIFO) — use it for task scheduling, print queues, or breadth-first search. In the context of this chapter, consider: both TStack and TQueue could descend from a common TAbstractContainer base class, sharing Count, IsEmpty, and Clear while providing different implementations of Push/Pop vs. Enqueue/Dequeue. That is inheritance and polymorphism applied to data structures.

From Chapter 12 — How do you test if an element is in a Pascal set? Use the in operator: if 'A' in CharSet then .... Sets provide a compile-time-checked membership test. Compare this to the runtime type checking we learned today: if Entity is TPlayer then .... Both test membership — one in a set of values, the other in a hierarchy of types.

Key Concepts Summarized

Inheritance lets you create new classes that automatically acquire the fields and methods of existing classes. The derived class adds specialization while reusing everything from the base class. Declare it with class(TParent).

Virtual methods are declared with the virtual keyword in the base class and override in derived classes. They enable dynamic dispatch — the runtime selects the correct method implementation based on the actual object type, not the variable type.

Polymorphism is the ability to treat objects of different types through a common interface. A TGameEntity variable can hold a TPlayer, TMonster, or TNPC, and virtual method calls execute the correct version for each. This eliminates cascading if-else type checks and makes code extensible without modification.

Abstract methods (declared virtual; abstract;) have no base class implementation and force every concrete derived class to provide one. Use them when there is no reasonable default behavior.

The is operator tests whether an object is of a particular type (or a descendant). The as operator performs a safe downcast with a runtime check. Use them sparingly — prefer polymorphism.

TObject is the root class of every hierarchy. It provides Create, Free, Destroy, ClassName, and InheritsFrom.

Constructors should call inherited Create first. Destructors should call inherited Destroy last. The Destroy destructor must always be declared override because TObject.Destroy is virtual.

The Liskov Substitution Principle states that derived objects must be substitutable for base objects without breaking program correctness. Test this by asking: "Would code written for the base class be surprised by this derived class's behavior?"

A Vocabulary for Conversations About Code

This chapter has given you a technical vocabulary that professional developers use daily. When a colleague says "make that method virtual," you know they mean "allow subclasses to override it with dynamic dispatch." When someone warns "that violates LSP," you know they mean "the derived class would surprise code written for the base class." When an architect recommends "favor composition over inheritance," you know they mean "use has-a instead of is-a for this relationship." This vocabulary is not jargon for its own sake — it is the shared language of software design, and it enables precise communication about program structure.

Theme Connections

Theme 2 (Discipline Transfers): Every concept in this chapter — inheritance, polymorphism, virtual methods, abstract classes, LSP — exists in essentially every modern object-oriented language. Java, C#, Python, Swift, Kotlin — they all have these mechanisms, often with nearly identical syntax. Learn them well in Pascal, and you can apply them anywhere. The keywords differ slightly (virtual/override in Pascal and C#, @Override annotation in Java, implicit in Python) but the concepts are identical.

Theme 5 (A+DS=P): Polymorphic arrays — collections of base-class references that hold objects of various derived types — are one of the most powerful combinations of algorithms and data structures. The data structure (array of TGameEntity) combined with the algorithm (iterate and call virtual methods) produces programs of remarkable flexibility. Wirth would appreciate the elegance: the algorithm does not change when the data structures (the specific entity types) multiply.

What We Built

In this chapter, we:

  1. Created the TGameEntity hierarchy for Crypts of Pascalia
  2. Learned virtual, override, reintroduce, and abstract
  3. Explored polymorphic arrays and the game loop pattern
  4. Used is and as for type checking and casting
  5. Examined TObject and its utility methods
  6. Mastered constructor/destructor chains in hierarchies
  7. Applied the Liskov Substitution Principle and is-a vs. has-a analysis
  8. Extended PennyWise with TRecurringExpense and polymorphic reporting

Looking Ahead

In Chapter 18, we will extend these ideas further with interfaces and abstract classes as formal contracts. Interfaces let a class promise to implement a set of methods without inheriting implementation — enabling a form of "multiple inheritance" that avoids the diamond problem. We will add IExportable and IPrintable to PennyWise, and IDamageable and IInteractable to Crypts of Pascalia.

But for now, take a moment to appreciate what you have accomplished. You can design class hierarchies, write polymorphic code, and think about software in terms of families of related types. This is a fundamental shift in how you approach programming — and it will serve you well no matter what language you work in next.


"Object-oriented programming is an exceptionally bad idea which could only have originated in California." — Edsger Dijkstra (probably apocryphal, but too good not to share)

Even Dijkstra, had he lived to see modern Object Pascal, might have admitted that inheritance and polymorphism — used judiciously — solve real problems with real elegance. The key word is "judiciously." Inherit when the relationship is genuinely is-a. Compose when it is has-a. And always, always ask: does my derived class honor the contract of its parent?