High‑level motivation
In Module 1, programs were small enough for one person to keep an understanding of the code in their mental model. Invariants were maintained by careful factory function design and by personal discipline about how objects were constructed and modified.
In Module 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. Three forces contribute to making real software systems different from smaller programs: contributor count exceeds what an individual can manage, duration exceeds what an individual can remember, and code volume exceeds what an individual can audit. These result in unmanageable complexity, lost understandability, and brittle evolvability.
In response to these challenges, we move from programmer discipline to encoding invariants in the language itself. By encoding invariants into types, we shift the burden of consistency from individual care to language enforcement. In this module we develop class-based abstractions as the mechanism for that shift. 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.
Lecture Sequence Overview
L1: The class as a unit of abstraction. To move invariant enforcement out of programmer discipline, we need a language mechanism that bundles state with the operations that maintain it. Classes provide that unit through fields, methods, and constructors with an enforced construction path. A class bounds reasoning to one kind of thing at a time, reducing complexity at the system level and providing named types that can be depended upon.
L2: Cohesive decomposition. Decomposing a system into classes only pays off if each class makes sense on its own; a class that enforces several invariants stops being a useful abstraction, since its users must understand all of them at once. We anchor what belongs inside a class to the invariant it enforces, applying the Single Responsibility Principle at both the class and method level to decompose the system into cohesive classes. A cohesive class is understandable from its invariant alone and can be modified without impacting the rest of the system.
L3: Verifying the invariant. "This class enforces its invariant" is a claim until we verify it. Tests validate invariants directly, covering expected and unexpected behaviours. Tests document invariants as checkable contracts, supporting trust now and evolvability later, when the implementation changes but the contract should not. Testability serves as a feedback loop into prior design choices.
L4: Error handling. A class's contract covers not just success but also failure: what counts as an invalid input, what happens when an invariant cannot be maintained, and how these conditions are communicated to callers. Error-handling paradigms differ in where failure lives: exceptions keep failure as a dynamic concern that callers must remember to handle, while types like Result or Option lift failure into the type system itself.
L5: Encapsulation. A verified invariant is only durable if external code cannot reach in and break it after construction, which convention cannot prevent at scale. TypeScript's access modifiers turn information hiding from a discipline-based convention into a language-enforced boundary, hiding the parts of the design most likely to change.
L6: Interfaces. Components should depend on abstractions rather than on concretions; this allows the specific implementation to be hidden from its caller. Interfaces provide the ultimate abstraction boundary as a type, declaring what a class commits to without saying anything about how it is implemented.
L7: Polymorphism through class extension. Interfaces let many classes commit to the same contract, but they do not express the case where one class is a kind of another. Class extension lets a subclass inherit and refine its parent's contract rather than reimplementing it, with instances of the subclass usable wherever the parent is expected.
L8: Polymorphism and the Open/Closed Principle. As a codebase evolves, we need code that continues to apply to types that did not exist when it was written, without requiring modification. Polymorphism unifies interface implementation and class extension under the Open/Closed Principle, keeping existing code open to extension by new types and closed to modification.
High‑level motivation
In Module 1, programs were small enough for one person to keep an understanding of the code in their mental model. Invariants were maintained by careful factory function design and by personal discipline about how objects were constructed and modified.
In Module 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. Three forces contribute to making real software systems different from smaller programs: contributor count exceeds what an individual can manage, duration exceeds what an individual can remember, and code volume exceeds what an individual can audit. These result in unmanageable complexity, lost understandability, and brittle evolvability.
In response to these challenges, we move from programmer discipline to encoding invariants in the language itself. By encoding invariants into types, we shift the burden of consistency from individual care to language enforcement. In this module we develop class-based abstractions as the mechanism for that shift. 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.
Lecture Sequence Overview
L1: The class as a unit of abstraction. To move invariant enforcement out of programmer discipline, we need a language mechanism that bundles state with the operations that maintain it. Classes provide that unit through fields, methods, and constructors with an enforced construction path. A class bounds reasoning to one kind of thing at a time, reducing complexity at the system level and providing named types that can be depended upon.
L2: Cohesive decomposition. Decomposing a system into classes only pays off if each class makes sense on its own; a class that enforces several invariants stops being a useful abstraction, since its users must understand all of them at once. We anchor what belongs inside a class to the invariant it enforces, applying the Single Responsibility Principle at both the class and method level to decompose the system into cohesive classes. A cohesive class is understandable from its invariant alone and can be modified without impacting the rest of the system.
L3: Verifying the invariant. "This class enforces its invariant" is a claim until we verify it. Tests validate invariants directly, covering expected and unexpected behaviours. Tests document invariants as checkable contracts, supporting trust now and evolvability later, when the implementation changes but the contract should not. Testability serves as a feedback loop into prior design choices.
L4: Error handling. A class's contract covers not just success but also failure: what counts as an invalid input, what happens when an invariant cannot be maintained, and how these conditions are communicated to callers. Error-handling paradigms differ in where failure lives: exceptions keep failure as a dynamic concern that callers must remember to handle, while types like Result or Option lift failure into the type system itself.
L5: Encapsulation. A verified invariant is only durable if external code cannot reach in and break it after construction, which convention cannot prevent at scale. TypeScript's access modifiers turn information hiding from a discipline-based convention into a language-enforced boundary, hiding the parts of the design most likely to change.
L6: Interfaces. Components should depend on abstractions rather than on concretions; this allows the specific implementation to be hidden from its caller. Interfaces provide the ultimate abstraction boundary as a type, declaring what a class commits to without saying anything about how it is implemented.
L7: Polymorphism through class extension. Interfaces let many classes commit to the same contract, but they do not express the case where one class is a kind of another. Class extension lets a subclass inherit and refine its parent's contract rather than reimplementing it, with instances of the subclass usable wherever the parent is expected.
L8: Polymorphism and the Open/Closed Principle. As a codebase evolves, we need code that continues to apply to types that did not exist when it was written, without requiring modification. Polymorphism unifies interface implementation and class extension under the Open/Closed Principle, keeping existing code open to extension by new types and closed to modification.