Chapter 33 Exercises: Units, Packages, and Modular Design
Part A: Conceptual Understanding
A.1. Explain the difference between a unit's interface section and its implementation section. Why does Pascal enforce this separation instead of allowing all code to be public?
Guidance
The interface section declares what the unit offers to the outside world — types, constants, procedure/function signatures — without revealing how they work. The implementation section provides the actual code and can also contain private helpers, types, and variables invisible to other units. Pascal enforces this separation because it provides information hiding: external code depends only on the public contract, not on internal details. This means you can change how a function works internally without breaking any code that calls it, as long as the signature stays the same.A.2. What is a circular reference between units? Why does Pascal prohibit circular references in interface uses clauses? Describe two strategies for resolving a circular dependency.
Guidance
A circular reference occurs when Unit A's interface uses Unit B and Unit B's interface uses Unit A. Pascal prohibits this because the compiler must process interface sections in dependency order — if both depend on each other, there is no valid order. Two strategies: (1) Move one dependency to the implementation uses clause, since implementation sections are processed after all interfaces. (2) Extract shared types into a third unit that both depend on, eliminating the cycle entirely.A.3. Define cohesion and coupling in the context of modular design. Why do we want high cohesion and low coupling? Give a concrete example of a unit with poor cohesion.
Guidance
Cohesion measures how closely related the elements within a module are — high cohesion means everything in the module serves a single, focused purpose. Coupling measures how much modules depend on each other — low coupling means modules interact through narrow interfaces. We want high cohesion because it makes modules easier to understand and test. We want low coupling because it means changing one module does not ripple through others. A unit with poor cohesion example: a "Utilities" unit containing date formatting, database connections, string parsing, and email validation — these are unrelated responsibilities that should be in separate units.A.4. Explain the difference between static and dynamic libraries. When would you choose one over the other?
Guidance
A static library is linked into the executable at compile time — the library code becomes part of the executable file. A dynamic library (DLL/SO) is loaded at runtime — it exists as a separate file. Choose static when you want a standalone executable with no external dependencies. Choose dynamic when multiple programs share the same library, when you need to update the library without recompiling all programs, or when you want to load libraries conditionally at runtime.A.5. What is the purpose of the initialization and finalization sections of a unit? In what order do they execute when a program uses multiple units?
Guidance
The initialization section runs setup code automatically when the program starts — opening files, registering handlers, setting defaults. The finalization section runs cleanup code when the program exits — closing files, releasing resources. Initialization runs in dependency order: if Unit A uses Unit B, then B initializes first. Finalization runs in reverse order: A finalizes before B. This ensures resources are cleaned up in the opposite order from how they were set up.Part B: Applied Analysis
B.1. You are building a student grading system with 3,000 lines of code. Propose a unit decomposition with at least four units. For each unit, list its name, purpose, and the key types or functions it would export. Draw the dependency graph.
Guidance
Example decomposition: (1) GradeModels — TStudent, TAssignment, TGrade records and related constants. (2) GradeStorage — TGradeRepository class for loading/saving grades to file. (3) GradeCalculator — functions like CalculateAverage, CalculateGPA, CurveGrades. (4) GradeReports — TReportCard, GenerateTranscript, ExportCSV. Dependency graph: GradeModels has no dependencies. GradeStorage depends on GradeModels. GradeCalculator depends on GradeModels. GradeReports depends on GradeModels and GradeCalculator. The main program depends on all four.B.2. A colleague shows you the following unit interface. Critique it — identify at least three design problems and suggest improvements.
unit AppUtils;
interface
uses Classes, SysUtils, SQLite3, fpjson;
var
DatabaseConnection: TSQLite3Connection;
CurrentUser: string;
TempBuffer: array[0..1023] of Byte;
procedure InitDB;
procedure CloseDB;
function FormatCurrency(Value: Double): string;
function ParseJSON(const S: string): TJSONData;
procedure SendEmail(const Recipient, Subject, Body: string);
procedure SortStringList(List: TStringList);
function CompressData(const Data: array of Byte): TBytes;
Guidance
Problems: (1) Low cohesion — the unit mixes database operations, string formatting, JSON parsing, email, sorting, and compression. These should be in separate units. (2) Global mutable variables — DatabaseConnection, CurrentUser, and TempBuffer are public global state, which creates tight coupling. (3) Unnecessary interface dependencies — SQLite3 and fpjson are imported in the interface, making every unit that uses AppUtils transitively dependent on them. Improvements: Split into DatabaseManager (InitDB, CloseDB, connection), StringHelpers (FormatCurrency), JsonHelpers (ParseJSON), EmailService (SendEmail), etc. Move variables to private implementation sections or encapsulate in classes.B.3. Consider this dependency scenario: Unit A uses Unit B in its interface. Unit B uses Unit C in its interface. Unit C uses Unit A in its implementation. Is this valid? Draw the dependency graph and explain why or why not.
Guidance
This is valid. The interface dependency chain is A → B → C, which is acyclic. C's dependency on A is in the implementation section, which does not create a compilation cycle because all interface sections (C, then B, then A) can be processed first, and then implementation sections are processed. The graph has an implementation-level back-edge from C to A, but this is permitted and compiles correctly.Part C: Code Exercises
C.1. Create a unit called Temperature that provides functions for converting between Fahrenheit, Celsius, and Kelvin. The interface should export six functions (one for each direction of conversion). Write a test program that exercises all six functions.
Guidance
The interface should declare: CelsiusToFahrenheit, FahrenheitToCelsius, CelsiusToKelvin, KelvinToCelsius, FahrenheitToKelvin, KelvinToFahrenheit. Each takes and returns a Double. Implementation uses the standard formulas: F = C * 9/5 + 32, K = C + 273.15, etc. The test program should verify round-trip conversions (e.g., converting to Fahrenheit and back should return the original value within floating-point precision).C.2. Create two units: StackUnit (implements a stack of integers using an array) and QueueUnit (implements a queue of integers using an array). Each should export Push/Enqueue, Pop/Dequeue, Peek, IsEmpty, and Size operations. Write a program that uses both units together to demonstrate the difference between LIFO and FIFO ordering.
Guidance
Both units should encapsulate their data in private implementation variables or (better) export a record/class type. The stack pushes and pops from the top (same end); the queue enqueues at the back and dequeues from the front. The test program should push/enqueue the same sequence of values into both and then pop/dequeue them, showing that the stack returns them in reverse order while the queue returns them in the original order.C.3. Refactor the following monolithic program into at least three units, preserving all functionality. Explain your unit boundaries.
program InventoryTracker;
uses SysUtils;
type
TItem = record
Name: string[50];
Price: Currency;
Quantity: Integer;
end;
var
Items: array[1..100] of TItem;
Count: Integer;
procedure AddItem(const N: string; P: Currency; Q: Integer);
begin
Inc(Count);
Items[Count].Name := N;
Items[Count].Price := P;
Items[Count].Quantity := Q;
end;
function TotalValue: Currency;
var I: Integer;
begin
Result := 0;
for I := 1 to Count do
Result := Result + Items[I].Price * Items[I].Quantity;
end;
procedure PrintInventory;
var I: Integer;
begin
WriteLn('INVENTORY REPORT':40);
WriteLn('':40);
for I := 1 to Count do
WriteLn(Items[I].Name:20, Items[I].Price:10:2,
Items[I].Quantity:8);
WriteLn('':40);
WriteLn('Total Value: ', TotalValue:0:2);
end;
procedure SaveToFile(const Filename: string);
var F: file of TItem; I: Integer;
begin
AssignFile(F, Filename);
Rewrite(F);
for I := 1 to Count do Write(F, Items[I]);
CloseFile(F);
end;
begin
Count := 0;
AddItem('Widget', 9.99, 50);
AddItem('Gadget', 24.95, 20);
AddItem('Gizmo', 14.50, 35);
PrintInventory;
SaveToFile('inventory.dat');
end.
Guidance
Suggested decomposition: (1) InventoryModels — TItem type, AddItem, TotalValue (domain logic). (2) InventoryStorage — SaveToFile, LoadFromFile (persistence). (3) InventoryReports — PrintInventory (display). The main program uses all three. InventoryModels has no dependencies beyond SysUtils. InventoryStorage depends on InventoryModels. InventoryReports depends on InventoryModels. The Items array and Count variable should be encapsulated — either as parameters or inside a TInventory class/record in InventoryModels.C.4. Create a unit with an initialization section that records the program start time, and a finalization section that prints the total program runtime. The unit should export a function ElapsedSeconds that returns the time elapsed since the program started. Test it with a program that does some work (e.g., sorting a large array) and calls ElapsedSeconds at various points.
Guidance
Use a private TDateTime variable set in initialization (via Now). ElapsedSeconds calculates the difference between Now and the start time, converting to seconds. The finalization section prints the total runtime. The test program should call ElapsedSeconds before and after some computational work to show elapsed time.Part D: Challenge Problems
D.1. Design a plugin system using dynamic libraries. Create a main program that loads .dll/.so files at runtime from a plugins/ directory, calls a standard GetPluginName function from each, and displays the list of available plugins. Create at least two sample plugin libraries. Handle errors gracefully (missing function, failed load).
Guidance
Use the DynLibs unit with LoadLibrary, GetProcAddress, and UnloadLibrary. Define a standard plugin interface: every plugin DLL must export GetPluginName: PChar and Execute: Integer. The main program uses FindFirst/FindNext to enumerate .dll files in the plugins directory, loads each, checks for the required exports, and displays plugin names. Each sample plugin is a separate library project. Error handling should catch nil function pointers and failed loads.D.2. Build a unit dependency analyzer. Write a program that reads a Pascal unit file, extracts all unit names from the uses clauses (both interface and implementation), and builds a dependency graph. Run it on the PennyWise units from Section 33.8 and display the dependency tree.