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
-
Extensibility: Suppose you need to add a
TEllipseclass. Which existing functions (TotalArea,DrawAll,DescribeAll,LargestShape) would need modification? Why? -
Abstract vs. Virtual:
Area,Perimeter, andDraware abstract, butDescribeis virtual with an implementation. Why the difference? When would you makeDescribeabstract instead? -
The Describe Method:
TShape.DescribecallsAreaandPerimeter, which are abstract. How does this work whenDescribeis called on aTCircle? Trace the virtual dispatch chain. -
Template Method Pattern:
Describeis 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. -
LSP Check: Does
TTrianglesatisfy LSP with respect toTShape? What if a triangle's sides are invalid (e.g., 1, 1, 100)? How would you handle this — in the constructor, inArea, or both? -
Composition Alternative: Instead of inheritance, you could use a
TShapeKindenumeration and a singleTShapeclass with aKindfield. 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
TotalAreaoperate onTShapearrays without knowing the concrete types - The
ClassNamemethod (inherited fromTObject) provides runtime type information for free - The
isoperator 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