Case Study 1: A Shape Hierarchy

The Scenario

You are building a simple vector graphics program. The user can create circles, rectangles, and triangles on a canvas. The program needs to calculate the total area of all shapes, display them in a list, and draw them (as text representations for now). The key requirement: the drawing and reporting code should work with any shape — existing or future — without modification.

This is the quintessential OOP teaching example, and for good reason: it perfectly illustrates why inheritance and polymorphism exist.

The Problem Without Inheritance

Without a class hierarchy, you might store shapes in separate arrays and process each type individually:

// BAD: Without inheritance
var
  Circles: array of record Radius: Double; CX, CY: Integer; end;
  Rectangles: array of record Width, Height: Double; RX, RY: Integer; end;
  Triangles: array of record Base, Height: Double; TX, TY: Integer; end;

// Need separate loops for each type
for i := 0 to High(Circles) do
  totalArea := totalArea + Pi * Circles[i].Radius * Circles[i].Radius;
for i := 0 to High(Rectangles) do
  totalArea := totalArea + Rectangles[i].Width * Rectangles[i].Height;
for i := 0 to High(Triangles) do
  totalArea := totalArea + 0.5 * Triangles[i].Base * Triangles[i].Height;

Every new shape type requires new arrays, new loops, and modifications to every function that processes shapes. This does not scale.

Designing the Hierarchy

Step 1: Identify Common Behavior

All shapes share: - A position (X, Y) - A color (as a string for simplicity) - The ability to calculate their area - The ability to calculate their perimeter - The ability to describe/draw themselves

Step 2: Determine What Varies

What differs between shapes: - The area formula - The perimeter formula - The drawing logic - The shape-specific parameters (radius vs. width/height vs. three sides)

Step 3: Build the Hierarchy

program ShapeHierarchy;

{$mode objfpc}{$H+}

uses
  SysUtils, Math;

type
  { Base class: all shapes share position, color, and abstract behaviors }
  TShape = class
  protected
    FX, FY: Integer;
    FColor: string;
  public
    constructor Create(AX, AY: Integer; AColor: string);
    destructor Destroy; override;

    { Abstract methods — every shape MUST implement these }
    function Area: Double; virtual; abstract;
    function Perimeter: Double; virtual; abstract;
    procedure Draw; virtual; abstract;

    { Concrete method — same for all shapes }
    procedure Describe; virtual;
    procedure MoveTo(NewX, NewY: Integer);

    property X: Integer read FX;
    property Y: Integer read FY;
    property Color: string read FColor write FColor;
  end;

  { Circle: defined by a radius }
  TCircle = class(TShape)
  private
    FRadius: Double;
  public
    constructor Create(AX, AY: Integer; AColor: string; ARadius: Double);
    function Area: Double; override;
    function Perimeter: Double; override;
    procedure Draw; override;
    property Radius: Double read FRadius write FRadius;
  end;

  { Rectangle: defined by width and height }
  TRectangle = class(TShape)
  private
    FWidth, FHeight: Double;
  public
    constructor Create(AX, AY: Integer; AColor: string;
                       AWidth, AHeight: Double);
    function Area: Double; override;
    function Perimeter: Double; override;
    procedure Draw; override;
    property Width: Double read FWidth write FWidth;
    property Height: Double read FHeight write FHeight;
  end;

  { Triangle: defined by three side lengths }
  TTriangle = class(TShape)
  private
    FSideA, FSideB, FSideC: Double;
  public
    constructor Create(AX, AY: Integer; AColor: string;
                       AA, AB, AC: Double);
    function Area: Double; override;
    function Perimeter: Double; override;
    procedure Draw; override;
    function IsValid: Boolean;
    property SideA: Double read FSideA;
    property SideB: Double read FSideB;
    property SideC: Double read FSideC;
  end;

{ ---- TShape Implementation ---- }

constructor TShape.Create(AX, AY: Integer; AColor: string);
begin
  inherited Create;
  FX := AX;
  FY := AY;
  FColor := AColor;
end;

destructor TShape.Destroy;
begin
  inherited Destroy;
end;

procedure TShape.Describe;
begin
  WriteLn(Format('  %s at (%d,%d), Color: %s',
    [ClassName, FX, FY, FColor]));
  WriteLn(Format('  Area: %.2f, Perimeter: %.2f',
    [Area, Perimeter]));
end;

procedure TShape.MoveTo(NewX, NewY: Integer);
begin
  FX := NewX;
  FY := NewY;
end;

{ ---- TCircle Implementation ---- }

constructor TCircle.Create(AX, AY: Integer; AColor: string; ARadius: Double);
begin
  inherited Create(AX, AY, AColor);
  FRadius := ARadius;
end;

function TCircle.Area: Double;
begin
  Result := Pi * FRadius * FRadius;
end;

function TCircle.Perimeter: Double;
begin
  Result := 2 * Pi * FRadius;
end;

procedure TCircle.Draw;
begin
  WriteLn(Format('  Drawing %s circle (r=%.1f) at (%d,%d)',
    [FColor, FRadius, FX, FY]));
  WriteLn('      ****');
  WriteLn('    *      *');
  WriteLn('   *        *');
  WriteLn('    *      *');
  WriteLn('      ****');
end;

{ ---- TRectangle Implementation ---- }

constructor TRectangle.Create(AX, AY: Integer; AColor: string;
                              AWidth, AHeight: Double);
begin
  inherited Create(AX, AY, AColor);
  FWidth := AWidth;
  FHeight := AHeight;
end;

function TRectangle.Area: Double;
begin
  Result := FWidth * FHeight;
end;

function TRectangle.Perimeter: Double;
begin
  Result := 2 * (FWidth + FHeight);
end;

procedure TRectangle.Draw;
begin
  WriteLn(Format('  Drawing %s rectangle (%.1fx%.1f) at (%d,%d)',
    [FColor, FWidth, FHeight, FX, FY]));
  WriteLn('   +----------+');
  WriteLn('   |          |');
  WriteLn('   |          |');
  WriteLn('   +----------+');
end;

{ ---- TTriangle Implementation ---- }

constructor TTriangle.Create(AX, AY: Integer; AColor: string;
                             AA, AB, AC: Double);
begin
  inherited Create(AX, AY, AColor);
  FSideA := AA;
  FSideB := AB;
  FSideC := AC;
end;

function TTriangle.Area: Double;
var
  S: Double;
begin
  { Heron's formula }
  S := (FSideA + FSideB + FSideC) / 2;
  Result := Sqrt(S * (S - FSideA) * (S - FSideB) * (S - FSideC));
end;

function TTriangle.Perimeter: Double;
begin
  Result := FSideA + FSideB + FSideC;
end;

procedure TTriangle.Draw;
begin
  WriteLn(Format('  Drawing %s triangle (%.1f,%.1f,%.1f) at (%d,%d)',
    [FColor, FSideA, FSideB, FSideC, FX, FY]));
  WriteLn('       /\');
  WriteLn('      /  \');
  WriteLn('     /    \');
  WriteLn('    /______\');
end;

function TTriangle.IsValid: Boolean;
begin
  Result := (FSideA + FSideB > FSideC) and
            (FSideA + FSideC > FSideB) and
            (FSideB + FSideC > FSideA);
end;

{ ---- Polymorphic Functions ---- }

function TotalArea(Shapes: array of TShape): Double;
var
  i: Integer;
begin
  Result := 0;
  for i := 0 to High(Shapes) do
    Result := Result + Shapes[i].Area;    { Polymorphic call }
end;

procedure DrawAll(Shapes: array of TShape);
var
  i: Integer;
begin
  for i := 0 to High(Shapes) do
  begin
    Shapes[i].Draw;      { Each shape draws itself }
    WriteLn;
  end;
end;

procedure DescribeAll(Shapes: array of TShape);
var
  i: Integer;
begin
  for i := 0 to High(Shapes) do
  begin
    Shapes[i].Describe;  { Polymorphic: calls virtual Area and Perimeter }
    WriteLn;
  end;
end;

function LargestShape(Shapes: array of TShape): TShape;
var
  i: Integer;
begin
  Result := Shapes[0];
  for i := 1 to High(Shapes) do
    if Shapes[i].Area > Result.Area then
      Result := Shapes[i];
end;

{ ---- Main Program ---- }

var
  Shapes: array[0..4] of TShape;
  Largest: TShape;
  i: Integer;
begin
  { Create a mixed collection of shapes }
  Shapes[0] := TCircle.Create(10, 20, 'Red', 5.0);
  Shapes[1] := TRectangle.Create(30, 40, 'Blue', 8.0, 4.0);
  Shapes[2] := TTriangle.Create(50, 10, 'Green', 3.0, 4.0, 5.0);
  Shapes[3] := TCircle.Create(70, 30, 'Yellow', 3.0);
  Shapes[4] := TRectangle.Create(20, 60, 'Purple', 10.0, 10.0);

  WriteLn('=== Shape Descriptions ===');
  WriteLn;
  DescribeAll(Shapes);

  WriteLn('=== Drawing All Shapes ===');
  WriteLn;
  DrawAll(Shapes);

  WriteLn('=== Summary ===');
  WriteLn(Format('Total area of all shapes: %.2f', [TotalArea(Shapes)]));

  Largest := LargestShape(Shapes);
  WriteLn(Format('Largest shape: %s (area = %.2f)',
    [Largest.ClassName, Largest.Area]));

  { Count shapes by type using 'is' }
  WriteLn;
  WriteLn('=== Shape Counts ===');
  WriteLn('Circles: ', 0);     { We'll count properly below }
  var CircleCount: Integer = 0;
  var RectCount: Integer = 0;
  var TriCount: Integer = 0;
  for i := 0 to High(Shapes) do
  begin
    if Shapes[i] is TCircle then
      Inc(CircleCount)
    else if Shapes[i] is TRectangle then
      Inc(RectCount)
    else if Shapes[i] is TTriangle then
      Inc(TriCount);
  end;
  WriteLn('Circles: ', CircleCount);
  WriteLn('Rectangles: ', RectCount);
  WriteLn('Triangles: ', TriCount);

  { Clean up }
  for i := 0 to High(Shapes) do
    Shapes[i].Free;
end.

Expected Output

=== Shape Descriptions ===

  TCircle at (10,20), Color: Red
  Area: 78.54, Perimeter: 31.42

  TRectangle at (30,40), Color: Blue
  Area: 32.00, Perimeter: 24.00

  TTriangle at (50,10), Color: Green
  Area: 6.00, Perimeter: 12.00

  TCircle at (70,30), Color: Yellow
  Area: 28.27, Perimeter: 18.85

  TRectangle at (20,60), Color: Purple
  Area: 100.00, Perimeter: 40.00

=== Drawing All Shapes ===

  Drawing Red circle (r=5.0) at (10,20)
      ****
    *      *
   *        *
    *      *
      ****

  Drawing Blue rectangle (8.0x4.0) at (30,40)
   +----------+
   |          |
   |          |
   +----------+

  Drawing Green triangle (3.0,4.0,5.0) at (50,10)
       /\
      /  \
     /    \
    /______\

  ...

=== Summary ===
Total area of all shapes: 244.81
Largest shape: TRectangle (area = 100.00)

Analysis Questions

  1. Extensibility: Suppose you need to add a TEllipse class. Which existing functions (TotalArea, DrawAll, DescribeAll, LargestShape) would need modification? Why?

  2. Abstract vs. Virtual: Area, Perimeter, and Draw are abstract, but Describe is virtual with an implementation. Why the difference? When would you make Describe abstract instead?

  3. The Describe Method: TShape.Describe calls Area and Perimeter, which are abstract. How does this work when Describe is called on a TCircle? Trace the virtual dispatch chain.

  4. Template Method Pattern: Describe is an example of the Template Method pattern — a concrete method that delegates to abstract methods. Identify another situation in this case study where this pattern could be useful.

  5. LSP Check: Does TTriangle satisfy LSP with respect to TShape? What if a triangle's sides are invalid (e.g., 1, 1, 100)? How would you handle this — in the constructor, in Area, or both?

  6. Composition Alternative: Instead of inheritance, you could use a TShapeKind enumeration and a single TShape class with a Kind field. Compare this approach to the inheritance approach. What are the trade-offs in terms of extensibility, type safety, and code complexity?

Key Takeaways from This Case Study

  • Abstract methods (virtual; abstract;) define the contract that all shapes must fulfill
  • Polymorphic functions like TotalArea operate on TShape arrays without knowing the concrete types
  • The ClassName method (inherited from TObject) provides runtime type information for free
  • The is operator enables type-specific counting while polymorphism handles the common operations
  • Heron's formula for triangle area demonstrates that derived classes can encapsulate arbitrarily complex domain logic behind the same simple interface
  • The Template Method pattern (seen in Describe) is a natural consequence of mixing virtual and non-virtual methods