Skip to content

Part 2: Defining Abstractions

When a system grows beyond one person's attention, the question is no longer whether a design works, but whether it can keep working as it changes.

In Part 1, we saw programs small enough for one person to keep the whole design in their mental model. We maintained invariants by careful factory function design and by personal programmer discipline about how objects were constructed and modified.

In Part 2, we expand our scope. Real software is built by teams, is maintained for years, and solves problems too large for any one person to tackle alone. In real software systems, the contributor count exceeds what an individual can manage, the longevity of the codebase exceeds what an individual can remember, and the code volume exceeds what an individual can audit. All of this means we cannot trust that other programmers will use the code we write correctly, or that every invariant will survive by discipline alone.

In response, we move from programmer discipline to encoding invariants in the language itself. By encoding invariants into classes and their associated abstractions, we shift the burden of consistency from individual care to language enforcement and from ad hoc coordination to explicit design.

In this module we develop class-based abstractions as the mechanism for invariant enforcement. Across eight lectures, we define classes, decompose systems into cohesive units, verify their invariants, design how failures are communicated, hide what is free to change, depend on abstractions through interfaces, organize classes into hierarchies, and write code that continues to apply as new types arrive.

Intended Learning Objectives

By the end of Part 2, you will be able to:

  1. Design classes that own and protect state, using constructors, access modifiers, and methods to maintain invariants inside the object.
  2. Decompose a problem into cohesive classes, so each unit has a clear responsibility and the relationships among units are explicit.
  3. Verify class behaviour with tests and errors, deciding when a failure should be prevented, asserted, or communicated as part of the API.
  4. Use encapsulation and interfaces to hide change, exposing only the operations clients need while keeping representations free to evolve.
  5. 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 four connected themes across eight chapters.

Building abstractions:

  1. The Class as a Unit of Abstraction introduces classes as the direct language support for bundling state with the operations that maintain it.
  2. Cohesive Decomposition shows how to split a system into classes and responsibilities that belong together.

Making class-based code reliable:

  1. Verifying Behaviour develops tests for class behaviour and discusses how to check that an object continues to respect its promises.
  2. Error Handling asks how class-based APIs should communicate failure when a precondition is violated or a request cannot be completed.

Hiding what can change:

  1. Encapsulation uses access control to keep representations private and preserve class invariants.
  2. Interfaces as Explicit Boundaries defines narrow contracts that let clients depend on a stable shape rather than on a concrete implementation.

Designing for growth:

  1. Polymorphism Through Class Extension uses inheritance and overriding to let related classes share behaviour while varying the parts that differ.
  2. 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: Design for Evolution

Part 2 ends with a design goal that is crucial for large systems: we need the ability to fix problems and add new features without impacting all of the existing code within the rest of the system. Part 3 extends this further: We examine at how systems are composed from interchangeable pieces, how dependencies are managed so that concrete implementations can be supplied from the outside, and how a codebase can remain open to new extensions while staying manageable across modules and teams.