Part 2: Defining Abstractions
Once a system grows beyond what one person can keep track of, a design has to keep working as the system changes.
In Part 1, programs were small enough for one person to keep the whole design in mind. We maintained invariants with carefully written constructor functions and with programmer discipline about how objects were created and changed.
Part 2 widens the scope. Real software is built by teams, maintained for years, and solves problems too large for one person. No single person can keep track of every contributor, remember the whole history of the code, or review all of it. We cannot assume that other programmers will use our code correctly, or that every invariant will survive through discipline alone.
So we move from programmer discipline to encoding invariants in the language itself. When classes and the abstractions built around them encode the invariants, the language enforces them instead of each programmer's care, and the way programmers coordinate becomes an explicit part of the design.
Programmer Discipline vs Enforcement
Recall that in CPSC 110, the signature recorded type information, but the teaching languages did not enforce it. Part 1 showed the shift from the unenforced signature in CPSC 110:
(@signature Number -> Number)
(define (double n) (* n 2))
; no issues statically, causes a runtime error: '*: expects a number, given "Clearly not a number"'
(double "Clearly not a number")to the typed signature in TypeScript, which the type checker enforces:
function double(n: number): number {
return n * 2;
}
// static error: "Argument of type 'string' is not assignable to parameter of type 'number'"
double("Clearly not a number");This is a shift from programmer discipline (in CPSC 110, assuming callers would respect the signature) to enforcement by the language. The type checker reports the error before the program runs, and it also reports it in the right place. The mistake is the call that passes a string as n, not the * inside double, which is where the CPSC 110 error appeared.
In Part 2, we'll see the same shift, but with more complex constraints than type signatures.
This part develops class-based abstractions as the mechanism for enforcing invariants. Across seven chapters, we define classes, decompose systems into cohesive units, hide what is free to change, separate what a class means from how it stores it, depend on abstractions through interfaces, organise classes into hierarchies, and write code that keeps working as new types are added.
Intended Learning Objectives
By the end of Part 2, you will be able to:
- Design classes that own and protect state, using constructors, access modifiers, and methods to maintain invariants inside the object.
- Decompose a problem into cohesive classes, so each unit has a clear responsibility and the relationships among units are explicit.
- Use encapsulation and interfaces to hide change, exposing only the operations clients need while keeping representations free to evolve.
- Keep an implementation free to change, distinguishing what an abstraction means from how it is represented, and judging what each commitment a class avoids costs it in return.
- Apply polymorphism and extension deliberately, so new behaviour can be added by introducing new classes rather than reopening code that already works.
Chapter Overview
Part 2 covers three connected themes across seven chapters.
Building abstractions:
- Building Abstractions with Classes introduces classes as the direct language support for bundling state with the operations that maintain it.
- Decomposing Systems into Cohesive Classes shows how to split a system into classes and responsibilities that belong together.
Hiding what can change:
- Encapsulating What Varies uses access control to keep a representation private, so the invariant that makes an object meaningful cannot be broken from outside.
- Preserving Implementation Freedom with Abstract Values treats the freedom to change an implementation as something a design can lose, showing how a careless comparison gives it away and how far it extends beyond the representation.
- Defining Boundaries with Interfaces establishes narrow contracts that let clients depend on a stable shape rather than on a concrete implementation.
Designing for growth:
- Extending Behaviour Through Polymorphism uses inheritance and overriding to let related classes share behaviour while varying the parts that differ.
- Growing Systems with the Open/Closed Principle brings the design ideas together and shows how polymorphism lets software grow by adding new code instead of rewriting code that already works.
Toward Part 3: Evolution
Part 2 ends with a design goal that matters most for large systems: fixing problems and adding features without changing the existing code around them. Part 3 builds on this. It examines how systems are composed from interchangeable pieces, how dependencies are managed so that concrete implementations can be supplied from outside, and how a codebase can stay open to new extensions while remaining manageable across modules and teams.