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, with programmer discipline about how objects were created and changed, and in Chapter 4 by hiding state inside closures, a pattern the language gave us no direct support for.
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. Chapter 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.
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 immutable values and generic classes extend it 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, and weighs inheritance against composition.
- Growing Systems with the Open/Closed Principle brings the design ideas together, adds the Dependency Inversion Principle, 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 large systems depend on: adding new kinds of behaviour without reopening code that already works. Part 3 builds on this. It examines how the parts of a system depend on one another, how concrete implementations are supplied from outside, how a program depends on and publishes contracts across its boundaries, and how working systems are refactored, debugged, and extended.