Last active
March 13, 2026 20:46
-
-
Save tadas-subonis/60f6f5de71f8b3d142e85e2488ec2c91 to your computer and use it in GitHub Desktop.
GOOS Cheatsheet — Growing Object-Oriented Software, Guided by Tests (Typst source)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #set document(title: "GOOS Cheatsheet", author: "Freeman & Pryce") | |
| #set page( | |
| paper: "a4", | |
| margin: (top: 1.8cm, bottom: 1.8cm, left: 1.5cm, right: 1.5cm), | |
| ) | |
| #set text(font: "Noto Sans", size: 8.5pt, lang: "en") | |
| #set par(leading: 0.6em, justify: false, spacing: 0.5em) | |
| #set list(indent: 8pt, body-indent: 4pt, spacing: 0.3em) | |
| #set enum(indent: 8pt, body-indent: 4pt, spacing: 0.3em) | |
| #set raw(block: true) | |
| #show raw.where(block: true): it => block( | |
| fill: rgb("#f5f5f5"), | |
| inset: (x: 8pt, y: 6pt), | |
| radius: 3pt, | |
| width: 100%, | |
| text(size: 7.5pt, font: "Noto Sans Mono", it) | |
| ) | |
| #show heading.where(level: 1): it => { | |
| v(1.4em, weak: true) | |
| text(size: 9pt, weight: "bold")[★ #upper(it.body)] | |
| v(0.1em, weak: true) | |
| line(length: 100%, stroke: 0.5pt + black) | |
| v(0.4em, weak: true) | |
| } | |
| #show heading.where(level: 2): it => { | |
| v(0.6em, weak: true) | |
| text(size: 8.5pt, weight: "bold")[▸ #it.body] | |
| v(0.2em, weak: true) | |
| } | |
| #let note(body) = block( | |
| width: 100%, | |
| spacing: 4pt, | |
| inset: (x: 7pt, y: 5pt), | |
| stroke: (left: 2pt + rgb("#555")), | |
| fill: rgb("#f8f8f8"), | |
| body | |
| ) | |
| #let side(left, right) = grid( | |
| columns: (1fr, 1fr), | |
| gutter: 8pt, | |
| block(stroke: 0.5pt + rgb("#ccc"), inset: (x: 7pt, y: 5pt), radius: 2pt, width: 100%, left), | |
| block(stroke: 0.5pt + rgb("#ccc"), inset: (x: 7pt, y: 5pt), radius: 2pt, width: 100%, right), | |
| ) | |
| // ── Title ───────────────────────────────────────────────────────────────────── | |
| #align(center)[ | |
| #text(size: 14pt, weight: "bold")[Growing Object-Oriented Software, Guided by Tests] | |
| #linebreak() | |
| #text(size: 9pt, style: "italic")[Freeman & Pryce — London School TDD Reference] | |
| ] | |
| #v(0.5em) | |
| #line(length: 100%, stroke: 1pt) | |
| #v(0.6em) | |
| #columns(2, gutter: 1.5em)[ | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| = Core Idea | |
| Software dev is a *learning process* — you can't know the right design upfront. TDD gives nested feedback loops (seconds → weeks) to discover design incrementally. Tests aren't just verification. They're a *design tool* that shapes your code. This is the "London school" of TDD: use tests to discover design through object collaboration. | |
| #note[*Golden Rule:* Never write new functionality without a failing test.] | |
| = The Two Loops | |
| *Outer loop* — acceptance tests, drives features end-to-end, measures real progress. | |
| *Inner loop* — unit tests, drives object design, runs in seconds. | |
| ``` | |
| Acceptance Test (feature goal) | |
| ↓ | |
| Unit Test (object behavior) | |
| ↓ write minimal code | |
| ↓ refactor | |
| ↑ repeat until AT passes | |
| Next feature | |
| ``` | |
| = Walking Skeleton | |
| Before any features, build the thinnest end-to-end slice that actually runs: | |
| `automated build → deploy → test via external interface` | |
| Show one value on screen, send one message. Then grow features through it. | |
| #side( | |
| [*Instead of:*\ — build DB layer\ — build service\ — build UI\ — integrate later], | |
| [*Build first:*\ `UI → Service → DB`\ Works, does almost nothing. Grow features through it.] | |
| ) | |
| - Avoids big-bang integration · proves architecture early | |
| - Forces real system boundaries · front-loads risk (correct!) | |
| - *Not* Big Design Up Front — reserve the right to change | |
| Projects with late integration start calm and end in chaos. Teams that skip this often can't deploy when it matters. | |
| = Five Key Practices | |
| == Practice 1 — Write Acceptance Tests First | |
| Write a failing acceptance test describing user behavior → make it fail → implement using smaller unit tests. | |
| ``` | |
| auction.placeBid(100) | |
| expect auction.lastBid == 100 | |
| ``` | |
| Write in *domain language*, not technology. If the protocol changes FTP→HTTP, acceptance tests shouldn't break. Test through external interfaces only — never call internal methods. | |
| Separate *progress tests* (failing = work to do) from *regression tests* (must stay green). Start with simplest success case, not error handling. | |
| == Practice 2 — Tell, Don't Ask | |
| Tell objects what to do — don't pull their state and decide for them. | |
| #side( | |
| [*Bad — data pulling:*\ ``` | |
| if (order.getCustomer() | |
| .getBalance() > price) | |
| order.getCustomer() | |
| .withdraw(price) | |
| ```], | |
| [*Good — tell:*\ ``` | |
| customer.purchase(price) | |
| ```\ \ Hides structure.\ Reduces coupling.] | |
| ) | |
| Replace `obj.getA().getB().doC()` with a single intention-revealing call. Name the interaction. Exception: asking for values and collections is fine. | |
| == Practice 3 — Test Behavior, Not Methods | |
| Tests should verify *observable behavior*, not internal implementation. | |
| #side( | |
| [*Brittle — method:*\ `test calculateTax()`], | |
| [*Resilient — behavior:*\ `test order.totalPrice()`] | |
| ) | |
| Name tests to describe scenarios: `rejectsOutsideBusinessHours()`, not `testBidAccepted()`. Behavior-level tests survive refactoring. | |
| == Practice 4 — Listen to the Tests | |
| When tests become hard to write, your design is wrong. Don't fight the test — listen to it. | |
| #table( | |
| columns: (1fr, 1fr), | |
| stroke: 0.5pt + rgb("#ccc"), | |
| inset: 5pt, | |
| fill: (_, y) => if y == 0 { rgb("#eee") } else { white }, | |
| table.header([*Symptom*], [*Design problem*]), | |
| [Hard to create object], [too many dependencies], | |
| [Need many mocks], [object doing too much], | |
| [Complex setup], [poor boundaries], | |
| [Hard assertions], [unclear behavior], | |
| ) | |
| == Practice 5 — Start Every Feature With a Failing Test | |
| Full TDD cycle: | |
| + Write a failing *acceptance test* in domain language | |
| + Write a failing *unit test* for the next small behavior | |
| + Write *simplest code* to pass — no gold plating | |
| + *Refactor* — improve structure, keep tests green. Not optional. | |
| + Repeat 2–4 until the acceptance test passes | |
| = Three Deeper Ideas | |
| == 1. Collaboration over Data Structures | |
| Most devs design around data models (`User`, `Order`, `Product`) → anemic models and procedural services. OO systems are *networks of communicating objects*. Behavior emerges from interactions. Design roles and messages — not entities. | |
| #side( | |
| [*Procedural — exposes data:*\ ``` | |
| OrderService | |
| getUser() | |
| checkInventory() | |
| chargePayment() | |
| ```], | |
| [*OO — sends messages:*\ ``` | |
| order → PaymentGateway | |
| order → Inventory | |
| order → Shipment | |
| ```] | |
| ) | |
| Conceptual ancestor of: microservices, actor systems, event-driven arch, DDD aggregates. | |
| == 2. Interfaces Emerge From Tests | |
| Don't design APIs first. Write tests describing collaboration — interfaces emerge naturally. | |
| ``` | |
| // write test: | |
| order.submit() | |
| // → sends payment request | |
| // → reserves inventory | |
| // discover while writing: | |
| PaymentGateway, InventoryService, OrderRepository | |
| // → now define the interfaces | |
| ``` | |
| Avoids speculative abstractions. Produces smaller, usage-driven APIs. Foundation of hexagonal architecture and DDD. | |
| == 3. Value World vs. Object World | |
| *Values* — immutable, represent facts, no identity: `Money`, `Price`, `Date`, `Quantity` | |
| *Objects* — have identity, behavior, change over time: `Order`, `Auction`, `Session` | |
| Confusing these leads to bad design. Modern patterns trace directly to this distinction: DDD Value Objects, immutable data in FP, event sourcing event semantics. Benefits: safer concurrency, easier testing, fewer bugs. | |
| = External vs. Internal Quality | |
| #side( | |
| [*External quality*\ Does the system work for users? Functional, reliable, responsive. Measured by acceptance tests.], | |
| [*Internal quality*\ Can devs understand and change the code safely? Loosely coupled, cohesive. Driven by unit tests.] | |
| ) | |
| If a class is hard to unit-test, it's probably tightly coupled, has hidden dependencies, or unclear responsibilities. Don't work around it with bytecode tricks — *fix the design*. | |
| = OO Design Rules | |
| *Separation of concerns* — gather code that changes for the same reason. Isolate domain from infrastructure (ports & adapters / anticorruption layer). | |
| *No And's, Or's, or But's* — describe an object without conjunctions. If you can't — split it into collaborators. | |
| *Composite simpler than its parts* — the composed object's API must be simpler than all its internals combined. (2 clock hands, dozens of gears inside.) | |
| *Context independence* — objects shouldn't know where they live. Pass in everything they need. A class using terms from multiple domains may be violating this. | |
| *Values vs. Objects* — values are immutable, no identity (`Money`, `Price`, `Date`). Objects have identity, mutable state, communicate via messages (`Order`, `Session`). Define domain value types even if small — they find bugs and attract behavior. | |
| *Higher abstraction* — program by combining meaningful components, not by manipulating variables and control flow directly. | |
| = Object Peer Stereotypes | |
| *Dependencies* — required services. Must be injected via constructor. No valid object without them. | |
| *Notifications* — fire-and-forget peers. Object doesn't know or care who listens. Default to null object or empty collection. | |
| *Adjustments* — policy/strategy objects that tune behavior. Can default to common values, changed later. | |
| = How Objects Are Born | |
| *Breaking out* — object too big or test too hard to read → split into smaller collaborators, unit-test separately. Sometimes roll back and reimplement clean. | |
| *Budding off* — need a new service → define the interface from the client's perspective ("on-demand design"), mock it in tests, implement later. Ask: "If this worked, who would know?" | |
| *Bundling up* — cluster always used together → wrap in a new composite. Name it (naming forces understanding). Test it directly. Mock it in callers' tests. | |
| = Only Mock Types You Own | |
| Don't mock third-party APIs. You don't understand them well enough to simulate correctly, and you can't act on design feedback about code you can't change. | |
| + Write a thin *adapter layer* in your domain terms | |
| + *Mock your adapters* in unit tests | |
| + Write *integration tests* against the real library to verify your understanding | |
| = Design Smell Catalog | |
| *Bloated constructor* — args always used together? Bundle into new object. Separate dependencies (ctor) from adjustments/notifications (setters). | |
| *Confused object* — can't describe without "and"? Mixes responsibilities. Split it. Test suite slices with no overlap = time to split the class too. | |
| *Too many dependencies* — too many responsibilities, or some args are adjustments that can default. | |
| *Too many expectations* — mock setup massive? Object orchestrates too much. Distinguish stubs (`allowing`) from expectations (`oneOf`). Tell the nearest object to do the work. | |
| *Can't replace an object* — singletons, `new Date()`, statics are hidden dependencies. Inject them explicitly. (Inject a `Clock`, not `new Date()`.) | |
| *Logging everywhere* — separate support logging (user-facing, test-driven) from diagnostic scaffolding. Extract a notification interface. Test it. | |
| *Mocking concrete classes* — missing a role abstraction. Extract an interface — naming it discovers domain concepts. (`CdPlayer` → `ScheduledDevice`) | |
| *Mocking values* — just construct instances. Use test data builders for complex objects. | |
| = Modern Architecture Mapping | |
| #table( | |
| columns: (1fr, 1fr), | |
| stroke: 0.5pt + rgb("#ccc"), | |
| inset: 5pt, | |
| fill: (_, y) => if y == 0 { rgb("#eee") } else { white }, | |
| table.header([*Modern concept*], [*Origin in GOOS*]), | |
| [Microservices], [Object collaboration], | |
| [Hexagonal / ports & adapters], [Role-based interfaces from tests], | |
| [Event-driven systems], [Message-based object design], | |
| [DDD Value Objects], [Value vs. object separation], | |
| [Clean architecture], [Dependency direction from tests], | |
| ) | |
| = Writing Good Tests | |
| - Names describe features, not methods: `holdsItemsInOrder`, not `testAdd`. Include scenario + expected result. | |
| - Write *backwards*: name → target call → expectations → setup | |
| - *Test data builders*: safe defaults + chainable `with*()` methods\ | |
| `anOrder().from(aCustomer().withNoPostcode()).build()` | |
| - Test for *information, not representation* — assert behavior, not exact string format | |
| - *One coherent feature per test* — if it asserts unrelated things, split it | |
| - Let exceptions propagate — no try/catch unless asserting on the exception | |
| - Name literals: `UNUSED_CHAT = null`, `INVALID_ID = 666` | |
| - *Precise assertions*: don't over-specify (brittle) or under-specify (false confidence) | |
| = Applying to Legacy Code | |
| - Start by automating *build and deploy*, then add end-to-end tests for areas you need to change | |
| - Safest first test = simplest path through the system; build infrastructure before tackling hard functionality | |
| - Introduce unit tests as you add features; refactor and inject dependencies as you go | |
| - Risky to rework without tests — but also risky to leave poor structure baking in. The longer you wait, the harder cleanup becomes. | |
| = Coding Style Notes | |
| *Between objects* — message-passing style. Tell, don't ask. Communicate through roles and interfaces. | |
| *Within an object* — functional style. Build up behavior from methods and values with no side effects. Immutability within a class leads to safer code. | |
| *Small methods to express intent* — even tiny methods are worth it if they make the code read naturally. Helper methods in tests especially. | |
| *Interface ≠ protocol* — an interface describes whether components will fit together. A protocol describes whether they will work together. Tests with mocks make protocols visible. | |
| *Code isn't sacred* — if a spike taught you what to do, roll it back and reimplement cleanly. The second time is faster. | |
| = Practices Checklist | |
| *Before a feature* — failing acceptance test in domain language.\ | |
| *Before a class* — failing unit test for this scenario.\ | |
| *After green* — refactor: names, extract methods, simplify.\ | |
| *New dependency* — inject via constructor; if bloated, extract a hidden concept.\ | |
| *Third-party lib* — adapter → mock adapter → integration test real lib.\ | |
| *Hard to test* — stop. Fix the design. Don't fight the test.\ | |
| *Getter chain* — replace with one intention-revealing method. Name the interaction.\ | |
| *Building a composite* — API must be simpler than sum of its parts.\ | |
| *Naming a test* — describe scenario + expected outcome, not the method called. | |
| #note[*The real takeaway:* The book isn't about testing. It's about using tests as a tool for designing clean, decoupled object systems. When a test is hard to write, don't fight it — listen to it. The pain points at a design problem.] | |
| ] // end columns | |
| #v(0.4em) | |
| #line(length: 100%, stroke: 0.5pt + rgb("#aaa")) | |
| #align(center)[#text(size: 7pt, fill: rgb("#aaa"), style: "italic")["Growing Object-Oriented Software, Guided by Tests" — Freeman & Pryce (Addison-Wesley, 2009). Read the book for the full Auction Sniper walkthrough.]] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment