Sandi Metz Dismantles Procedural Code Bloat With Small Objects

RailsConf 2014 - All the Little Things by Sandi Metz — Confreaks

Confreaks

Tolerating code duplication is far cheaper than adopting the wrong abstraction, because decomposing conditionals into small, single-responsibility objects is what makes software easy to extend.

AI-assisted summary of RailsConf 2014 - All the Little Things by Sandi Metz — original source. Check the original for full context.

Source published:

Key ideas

  • Sandi Metz refactors the 43-line procedural conditional in the Gilded Rose Kata into small, polymorphic objects, showing how intermediate complexity spikes before reaching ultimate simplicity. (0:00)
  • Flog complexity tracking reveals that while intermediate refactoring steps temporarily increased code complexity, the final design reduced average class complexity from 50 to 7, slashing cognitive load by roughly 75%. (33:07)
  • Metz demonstrates Kent Beck's principle of 'make the change easy, then make the easy change,' spending the bulk of the process preparing the architecture so the new 'Conjured' feature could be added in a single isolated class. (35:41)
  • The presentation defines strict rules for safe inheritance: hierarchies must be shallow and narrow, subclasses must exist at leaf nodes, and every subclass must use all superclass behavior. (29:09)

Detailed notes

## Background Published by Confreaks on YouTube on May 21, 2014, this video captures Sandi Metz's presentation at RailsConf 2014. The talk addresses the gap between object-oriented theory and practical coding reality, demonstrating how to transform a tangled, conditional-heavy codebase into simple, interchangeable objects using straightforward refactoring strategies.

## TL;DR Using the classic Gilded Rose Kata, Sandi Metz demonstrates how to systematically refactor a gnarly, 43-line nested conditional into a clean, object-oriented design. She introduces practical diagnostic techniques—such as code complexity metrics and the visual 'squint test'—while cautioning against premature abstractions and the instinct to cram new logic into legacy conditionals. By deliberately enduring a temporary spike in intermediate complexity, Metz breaks the monolith down into small, polymorphic classes coordinated by an item factory and a narrow inheritance hierarchy. Once the system satisfies the Open-Closed Principle, adding a new 'Conjured' feature requires only a concise new subclass and a single configuration entry.

## Atmosphere & style The presentation is warm, candid, and deeply engaging, striking an effortless balance between seasoned technical authority and self-deprecating humor. Metz's delivery feels like an intimate masterclass from a battle-tested mentor who openly admits to being 'Boolean-impaired' while reassuring developers through the uncomfortable, messy middle steps of refactoring. The audience reacts with knowing laughter, collective groans at legacy complexity metrics, and attentive applause, mirroring an emotional arc that moves from legacy dread to the crisp satisfaction of elegant simplicity.

## Bulletin - [00:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=0s) Sandi Metz refactors the 43-line procedural conditional in the Gilded Rose Kata into small, polymorphic objects, showing how intermediate complexity spikes before reaching ultimate simplicity. - [33:07](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1987s) Flog complexity tracking reveals that while intermediate refactoring steps temporarily increased code complexity, the final design reduced average class complexity from 50 to 7, slashing cognitive load by roughly 75%. - [35:41](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2141s) Metz demonstrates Kent Beck's principle of 'make the change easy, then make the easy change,' spending the bulk of the process preparing the architecture so the new 'Conjured' feature could be added in a single isolated class. - [29:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1749s) The presentation defines strict rules for safe inheritance: hierarchies must be shallow and narrow, subclasses must exist at leaf nodes, and every subclass must use all superclass behavior.

## Key takeaways - [00:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=0s) The fundamental core of good object-oriented design is simply making smaller classes and smaller methods that know as little about each other as possible. - [02:14](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=134s) Complexity metrics like Flog offer an objective ABC (Assignments, Branches, Conditionals) evaluation of code quality and cognitive burden. - [03:44](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=224s) The subjective 'squint test' uses visual geometry and syntax highlighting variations to detect nested conditionals (shape) and mismatched abstraction levels (color). - [06:05](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=365s) Code bases fall into an accretion trap because developers tend to tack new logic onto existing conditionals rather than introducing new objects next to bloated legacy code. - [09:56](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=596s) Refactoring procedural code requires introducing seams using messages to separate decision-making paths and allow rapid red-to-green test cycles. - [13:08](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=788s) Duplication is far cheaper than the wrong abstraction; developers should resist premature DRYing until patterns and domain concepts fully emerge. - [15:59](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=959s) Intermediate steps during refactoring routinely cause a temporary spike in complexity metrics before leading to ultimate structural simplicity. - [19:25](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1165s) Repeating method prefixes or suffixes (such as normal_tick or brie_tick) are an unmistakable code smell signaling a tortured object struggling to break free. - [25:55](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1555s) Replacing explicit branching with duck typing and polymorphic roles lets calling code focus on what it does rather than why it switches. - [25:55](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1555s) A class that acts only as a forwarding Middleman should either be removed or refactored into a stateless factory module. - [29:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1749s) Inheritance is safe and effective when class hierarchies are shallow and narrow, subclasses sit at leaf nodes, and all superclass behavior is fully consumed. - [30:44](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1844s) Implementing a no-op default tick on the base Item class eliminates unnecessary subclasses like Sulfuras that have no state mutations. - [32:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1929s) Separating configuration data (mapping raw strings to class constants) from the matching algorithm decouples business logic from data evolution. - [33:07](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1987s) Breaking monolithic procedures into small, single-purpose classes drastically cuts cognitive load by allowing engineers to reason about one isolated component at a time. - [35:41](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2141s) Following Kent Beck's maxim—'Make the change easy, then make the easy change'—ensures new features can be added via isolated extension without modifying existing code.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=0s) The Core Advice: Make Smaller Things

Sandi Metz begins by reflecting on why writing object-oriented software feels so difficult in practice. Looking at the applications developers actually build, good intentions almost inevitably give way to frustration: developers consistently write code that they eventually grow to hate. Metz explains that her primary job these days is simply thinking about how to write better code, and that reflection has brought her to a remarkably straightforward conclusion. Whenever people ask her how to write proper object-oriented software, she boils her guidance down to a single principle: “I say make smaller things, that's all it is.” By that, she means writing smaller classes and smaller methods, while ensuring they know as little about each other as possible.

Lately, that philosophy has turned into a personal obsession with conditional logic. Horrified by the sheer volume of gnarly conditionals littered throughout real-world codebases, Metz found herself constantly pondering when conditionals ought to be replaced by small objects, the exact mechanics of making that transformation, and the eventual impact on the codebase. While attending a conference in Miami in November, she inflicted this obsession upon Jim Weirich, who pointed her toward the well-known Gilded Rose Kata. Metz admits with a laugh that because she does not get out much, she had never heard of it before.

Rather than searching online to read the standard problem descriptions and walkthroughs, Metz deliberately treated the kata like a genuine legacy production issue where the only available sources of truth were the existing code and the test suite. She checked out Jim Weirich's repository, examined the codebase, and realized it provided the perfect structural skeleton to hang her ideas on. Making only minor modifications to make the code easier to present on slides, she preserved the authentic core of the kata to explore how conditional bloat can be systematically dismantled.

- Metz's foundational rule for object-oriented design is to create smaller classes, create smaller methods, and keep their knowledge of one another minimal. - The Gilded Rose Kata was introduced to Metz by the late Jim Weirich during a conversation in Miami. - Metz approached the kata strictly as an authentic production challenge, deliberately avoiding web searches and relying exclusively on the implementation code and tests.

---

[02:14](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=134s) The Gilded Rose and Complexity Metrics

The Gilded Rose domain model centers on a single `GildedRose` class. It manages three core attributes—`name`, `quality`, and `days_remaining`—initialized upon instantiation, and exposes a solitary business method called `tick`. Metz reveals the implementation of `tick`, first displaying half of the method and then the remainder. She tells the audience not to bother trying to read the text on screen; her goal is simply to show the sheer physical size and shape of the logic, which spans a continuous 43-line `if` statement.

Metz jokes that understanding such nested logic feels overwhelming because she is famously "Boolean-impaired." Acknowledging that her subjective impression of difficulty might not be universally trusted, she turns to objective tooling and runs the Flog complexity metric against the class. Metz explains that a metric is essentially crowdsourced consensus about code quality, measuring an ABC score that tallies assignments, branches, and conditionals. Higher scores denote greater complexity, signaling code that is significantly harder to comprehend and reason about. When run against the kata, Flog gives the overall `GildedRose` class a score of 50, with the `tick` method alone accounting for 45—a result greeted by audible groans from the audience. As Metz notes: “A metric is a crowdsourced idea about something.”

```mermaid flowchart TD accTitle: Flog Complexity Metric Breakdown accDescr: Diagram showing how Flog evaluates code complexity and the resulting scores for Gilded Rose. subgraph Metric["Flog ABC Evaluation"] A["Assignments"] --> D["Composite Score"] B["Branches"] --> D C["Conditionals"] --> D end subgraph Results["Gilded Rose Results"] D --> E["GildedRose Class: Score 50"] D --> F["tick Method Alone: Score 45"] end ```

- The `GildedRose` class maintains three state attributes: `name`, `quality`, and `days_remaining`. - The `tick` method consists of a single 43-line nested conditional statement. - Flog measures complexity using the ABC model (Assignments, Branches, Conditionals), where higher point values correlate directly with increased cognitive load. - The `GildedRose` class received a Flog score of 50, with 45 points concentrated entirely inside `tick`.

---

[03:44](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=224s) The Squint Test and Code Smells

Before proceeding with the code, Metz introduces her favorite subjective diagnostic for complexity: the "squint test." In her consulting career, clients never hire her when their codebases are healthy, nor do they ever showcase the clean, elegant sections they are proud of. Instead, they lead her straight to the most heinous, tangled corners of their applications—areas encumbered by deep, confusing histories. When developers walk her through these embarrassing sections, they inevitably feel compelled to explain every historical compromise and architectural accident that caused the code to rot. Metz finds that these long-winded explanations eventually turn into white noise, reminiscent of the classic Gary Larson *Far Side* cartoon where the owner talks to Ginger the dog and all the dog hears is "blah, blah, blah, Ginger." Inevitably, Metz is startled back into focus when the client points at the screen and asks, "So, what do you think we should do about this line of code?"

To survive these consulting sessions without needing to master months of legacy context upfront, Metz invented the squint test. By leaning back in a chair and squinting at the screen, a developer deliberately ignores the syntax and evaluates two primary visual signals: changes in shape and changes in color. Metz explains the mechanical reality behind these two cues: “Changes in shape mean you have nested conditionals and they are always going to be hard to reason about.” Meanwhile, changes in color in a syntax-highlighted editor indicate that the method mixes vastly different levels of abstraction, making the narrative flow almost impossible to follow.

Looking at the Gilded Rose implementation through this lens exposes severe code smells. The 43-line method contains 16 distinct `if` statements loaded with inequality operators (`!=`) and logical ANDs (`&&`). It relies heavily on arbitrary magic numbers and references three distinct magic strings: `"Aged Brie"`, `"Sulfuras, Hand of Ragnaros"`, and `"Backstage passes to a TAFKAL80ETC concert"`.

```mermaid flowchart LR accTitle: The Squint Test Diagnostic accDescr: Visual cues observed during the squint test and the code smells they reveal. subgraph VisualCues["Squint Test Observations"] S["Changes in Shape"] C["Changes in Color"] end subgraph Diagnosis["Architectural Smells"] S -->|"Indicates"| SC["Deeply Nested Conditionals<br/>(Hard to reason about)"] C -->|"Indicates"| CC["Mixed Levels of Abstraction<br/>(Confusing narrative flow)"] end ```

- The squint test relies on visual pattern matching: evaluating indentation geometry (shape) and syntax highlighting variation (color). - A shifting shape reveals deeply nested branch logic. - Shifting colors indicate inconsistent levels of abstraction colliding within the same method. - The Gilded Rose `tick` method contains 16 conditional checks, coupled logical operators, uncounted magic numbers, and three magic strings (`Brie`, `Sulfuras`, and `Backstage passes`).

---

[06:05](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=365s) The Failure to Add "Conjured" and the Growth of Code

Turning to the test suite, Metz finds that the existing tests pass, but there are six skipped tests. The active tests cluster neatly around the magic strings, along with a category called "Normal"—an item type never explicitly named in the `tick` conditionals, which Metz correctly deduces must be handled by an implicit `else` branch. A look inside the tests reveals the basic domain behavior: given an item initialized with a name, an expiration date, and a quality score, executing `tick` degrades both the days remaining and the quality by one. The system behaves like an inventory manager selling perishable goods like milk, cheese, or eggs that deteriorate over time.

Inspecting the six skipped tests, Metz discovers the true challenge: the introduction of a new item type called `"Conjured"`. Realizing she is expected to add this feature, she initially tried to do what most developers instinctively do: modify the existing 43-line conditional. The result was pure failure. Every time she modified the `if` statement to get a Conjured test to pass, another test broke. After hours of fruitless tweaking, she stepped back to ask why she felt compelled to wedge new logic into that mess in the first place. The answer lies in human psychology: when given a feature request, developers search for the code that looks most similar to the new requirement and bolt their changes onto it. Novice developers especially fear creating new objects, so they simply tack another branch onto an existing `if` statement. Over time, classes reach a tipping point where their sheer mass prevents anyone from imagining alternative designs. Metz captures this reality with an unmistakable comparison: “Nobody adds a 10-line helper class to a 5000-line active record object.”

Recognizing that following the existing pattern only exacerbates the rot, Metz abandons the attempt to wedge Conjured directly into the conditional. Instead, she commits to strict refactoring—altering the internal arrangement of the code without modifying external behavior—using the passing test suite as a protective wall at her back. Rather than treating `tick` as a massive, procedural script, she sets out to remodel it along genuine object-oriented lines, transforming a monolithic procedure into a set of small, focused objects communicating via messages.

- The test suite contains passing tests for the magic strings and "Normal" items, plus six skipped tests defining requirements for a new item type called "Conjured." - Standard items behave like perishable goods, decrementing both quality and days remaining upon each `tick`. - Attempting to implement "Conjured" directly into the 43-line conditional repeatedly broke existing test cases. - Software growth follows an accretion trap: developers feel an implicit bargain to follow existing patterns, causing large classes to continuously expand because nobody creates small classes next to bloated ones. - True refactoring requires keeping external behavior identical while reorganizing internal structure, using tests as a protective safety net before attempting to add new features.

[09:56](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=596s) Creating Seams and Scrambling Toward Green

Object-oriented design relies heavily on messages because they provide indirection, allowing developers to substitute different objects at the back end. Messages create seams where alternative behavior can be introduced, but procedural code lacks seams altogether. To begin refactoring procedural code, Sandi Metz explains that she must explicitly introduce a seam.

```mermaid flowchart TD accTitle: Refactoring Flow for Normal Items accDescr: Step-by-step progression from creating a seam to simplifying logic under test coverage. subgraph S1["Seam Creation"] A["Intercept 'Normal' path"] --> B["Bail / Send message to self"] end subgraph S2["Red-to-Green Cycle"] B --> C["4 tests fail"] C --> D["Decrement quality by 1"] D --> E["Decrement days_remaining by 1"] E --> F["Handle zero-quality edge case"] end subgraph S3["Refactoring"] F --> G["All Normal tests green"] G --> H["Simplify: deduct days first"] H --> I["Single abstraction level"] end ```

To create this first seam, Metz intercepts the "Normal" item case and immediately bails out of the main routine. Running the test suite immediately yields four failing tests, confirming that the seam successfully intercepts the execution path. Unwilling to add more code into the existing `tick` method, she sends a message to self and watches the tests remain failed.

Metz then cracks open the failing tests one at a time. The first requirement is simple: quality decreases by one, and `days_remaining` decreases by one. Writing that logic makes the first test pass. The next test states that once the item reaches the sell-by date, quality drops by two instead of one. After updating the code, she expects two tests to pass and two to fail. Unexpectedly, a third test passes on its own. Metz embraces this enthusiastically, remarking to the audience's laughter: “I don't need to understand it, I've got tests.”

Addressing the final failing test—which requires that quality must not drop below zero—she initially wraps the implementation inside a protective check. Once everything is green, she stresses an essential mindset rule for refactoring:

> “Red is not when you ponder the abstraction. Red is when you scramble toward green.”

With the tests green, Metz refactors the freshly written, messy code. She openly confesses to being "Boolean-impaired" and having written code she did not fully understand just seconds earlier. Examining the green logic, she notices that `days_remaining` is always decremented by one, and that a zero quality means the method can bail immediately. Removing the nested conditions, she re-evaluates the drop-by-two scenario and realizes it is not two separate cases, but rather an initial deduction of one with a second deduction applied conditionally past the sell-by date. Flattening this logic leaves a method where everything sits at the same level of abstraction.

[13:08](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=788s) The Brie Case and Resisting Premature DRYing

Having established the workflow on the Normal case, Metz rapidly repeats the sequence for Aged Brie. She inserts a `case` statement to intercept Brie items, triggering seven test failures. Writing the minimal code to satisfy the tests one by one, she swiftly reaches a passing state.

Looking at the resulting code side-by-side with the Normal item logic, Metz notices a striking similarity: ``` Normal Item Algorithm Shape <---> Aged Brie Algorithm Shape [Decrement days remaining] [Decrement days remaining] [Adjust quality by base delta] [Adjust quality by base delta] [Double adjustment past sell-by] [Double adjustment past sell-by] ``` While the input data differs (one increments while the other decrements), the fundamental shape of the algorithm is identical. This presents a common developer trap: the urge to immediately DRY (Don't Repeat Yourself) up the code. Metz cautions strongly against this impulse. Leaving the refactoring path to clean up duplication at this early stage is a distraction, because much more information about the overall system is about to surface.

Metz emphasizes this core design principle:

> “It is far cheaper to keep duplication than it is to have to mess with the wrong abstraction.”

Explaining why DRY is hammered into beginner programmers, she jokes that “it's because they can't understand anything else.” Novices cannot grasp subtle design trade-offs, but they can easily spot duplicate lines. For experienced developers, tolerating duplication while waiting for the correct abstraction to emerge is a crucial skill. For those anxious about forgetting duplicated code, Metz recommends using a numbered tag system in source files (such as `# dup 6`) rather than reaching prematurely for an abstraction.

[15:59](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=959s) Eliminating the 43-Line Conditional and the Refactoring Dip

Continuing through the remaining item types, Metz encounters "Sulfuras, Hand of Ragnaros". Expecting three failing tests after stubbing out an empty method, she is amused when all three tests immediately pass. Investigating the test cases reveals that Sulfuras items never change state at all—a fact that was completely obscured inside the original 43-line conditional. Next, she implements the "Backstage passes" logic, writes the corresponding passing code, and returns the entire test suite to green.

With every execution path captured by dedicated methods on the `Gilded Rose` class, the original 43-line procedural conditional becomes dead code. Metz deletes it outright:

> “You can do characterization tests around the edges, so that you'll have green, you'll have a wall at your back for tests, and then you can refactor your way, to the point where you can delete the code that you don't understand.”

Evaluating the transformation using visual and empirical measures reveals striking improvements: - Applying the "squint test" shows that the new code structure is flat rather than deeply nested, and related concerns are clustered. - The original class scored a Flog complexity of 50. - The new class scores a total Flog metric of 40 across all methods. - The single most complex method in the new version (`Backstage`) scores only 12, demonstrating that individual units of code are substantially simpler.

Metz poses the obvious question: if small methods are demonstrably simpler, why don't developers routinely refactor their code this way? While one factor is the human habit of simply mimicking existing patterns (adding another branch to an existing conditional), there is a deeper reason. Plotting the Flog complexity across the 10 sequential refactoring steps reveals that every single intermediate step made the code objectively *more* complex before the final simplification was achieved. Developers who lack the confidence of object-oriented principles often abandon refactorings midway through this complexity spike.

[19:25](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1165s) The Open-Closed Principle and the "Tortured Object"

Sandi Metz reflects on why so many developers resist refactoring into tiny methods: when people haven’t experienced the payoff, the necessary intermediate steps look like pure friction or academic nonsense. Metz admits she only tolerates the spike in intermediate complexity because she has seen the payoff repeatedly: “I can promise you that if you can see far enough to see to the end, this intermediate complexity leads to ultimate simplicity.” With the initial method extractions complete, she circles back to the original task—implementing the new "Conjured" item behavior. Adding an extra branch directly into the existing conditional structure would be trivial, but Metz flatly rejects that option: “The answer to that is ‘no’, I should not do that.”

She explains that the code is not Open-Closed—open for extension, but closed for modification. This is the "O" in SOLID, derived from decades of hard-earned industry experience:

> “It’s one of the pieces of cumulative wisdom created by folks who’ve written a mountain of object-oriented code, and they have experienced every possible kind of programming pain.”

Metz describes object-oriented design not as rigid dogma, but as a practical style guide for organizing code. If you ignore it, you simply earn the right to rediscover all of that programming pain on your own. At the macro level, the goal is adding new features without altering existing code—a world where tests stay permanently green, safety is guaranteed, and unintended side effects vanish. At the micro level, she gives an unmistakable diagnostic rule:

> “And on the micro level, what that means here, right now, in this code, is that when we see methods that have a repeating prefix or repeating suffix, there is a tortured object in there that’s trying to get out.”

When code contains method names like `normal_tick`, `brie_tick`, and `backstage_tick`, developers face a fork in the road. Choosing procedural comfort requires keeping all switching logic and the resulting behaviors visible in one place. By contrast, trusting objects means extracting those distinct prefixes into standalone classes, each holding its own `tick` method, while the enclosing class merely keeps an instance of the chosen object.

---

[23:36](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1416s) Extracting Specialized Item Classes and Embracing Trust

To break the procedural chokehold, Metz demonstrates the extraction of the first domain class: `Normal`. She strips the `normal_` prefix to leave a clean `tick` method, houses it in a dedicated `Normal` class, adds initialization boilerplate and attributes, and updates `GildedRose` to instantiate `Normal` and forward the `tick` call. This immediately introduces a burst of intermediate mess. Because `Normal` now uses an item object while `Brie` still calculates days and quality internally on `GildedRose`, the public API of `GildedRose` must dynamically check whether it has an underlying item object before returning `quality` and `days_remaining`. Metz reassures the audience that while this looks untidy, it is purely temporary.

```mermaid flowchart TD accTitle: Transition from procedural methods to item objects accDescr: Shows how conditional tick methods are migrated out of GildedRose into standalone classes.

subgraph Before["Procedural GildedRose"] GR1["GildedRose"] --> M1["normal_tick"] GR1 --> M2["brie_tick"] GR1 --> M3["backstage_tick"] GR1 --> M4["sulfuras_tick"] end

subgraph Intermediate["Intermediate Object State"] GR2["GildedRose"] -->|"instantiates & forwards"| N["Normal #tick"] GR2 -->|"legacy inline"| B["brie_tick"] GR2 -->|"forwarding boilerplate"| S["Sulfuras #tick (empty)"] GR2 -->|"legacy inline"| BS["backstage_tick"] end

Before -->|"Extract objects"| Intermediate ```

Metz then applies this pattern across the rest of the items: `Brie`, `Sulfuras`, and `Backstage`. The `Sulfuras` case in particular demands faith in the refactoring process:

> “I have an empty method, look what I have to do. Make a new class, put the method in, pry this method open, get an instance of that class, forward the message. You can be forgiven for being suspicious about this, but if you trust the refactorings, you have confidence that this is going to turn out well in the end.”

Refusing to deviate or take shortcuts, she creates empty forwarding calls for `Sulfuras` and extracts `Backstage`. Once all four item classes exist, she rewinds earlier decisions. The separate private helper methods inside `GildedRose` are removed, and the instantiations are placed directly into the case statement branches.

- `Normal`, `Brie`, `Sulfuras`, and `Backstage` each become distinct classes encapsulating their specific `tick` behavior. - Intermediate complexity spikes as accessor methods (`quality`, `days_remaining`) are forced to handle both inline calculations and delegated item states. - Duplication across the item classes is accepted temporarily, honoring the principle that duplication is cheaper than the wrong abstraction until concrete shapes emerge.

---

[25:55](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1555s) Resolving Middleman into an Item Factory

With each case statement branch now doing the exact same thing—instantiating an item and calling `tick`—the underlying abstraction reveals itself. Metz abstracts away the case statement completely by dynamically resolving the item class from the input name via a `class_for` lookup helper. Once `class_for(name)` returns the target class, `GildedRose` can instantiate it and invoke `tick` without caring which specific item class is running:

> “So now, I have separated the reason I’m switching from the thing I do when I switch. And I can just forget about what’s inside that class_for method.”

From the perspective of `GildedRose`, the concrete classes (`Normal`, `Brie`, `Backstage`, `Sulfuras`) do not matter; what matters is the *role* they play. It treats them as a polymorphic duck type that honors the single-method API of `tick`.

```mermaid flowchart LR accTitle: Transformation from Middleman to Factory Module accDescr: Shows GildedRose morphing from a middleman object into an Item Factory module that hands back Duck types.

subgraph Problem["Middleman Smelling Object"] Caller["Caller / Foo"] -->|"sends tick"| OldGR["GildedRose Instance"] OldGR -->|"forwards tick"| ConcreteItem["Specific Item Instance"] end

subgraph Solution["Factory Pattern"] Client["Client Code"] -->|"GildedRose.for(name,...)"| Mod["GildedRose (Module)"] Mod -.->|"determines class & returns"| DuckItem["Item Role (Duck Type)"] Client -->|"calls tick directly"| DuckItem end

Problem ==> Solution ```

At this stage, Metz examines the role of `GildedRose`. An instance of `GildedRose` exists only to hold an item and forward `quality`, `days_remaining`, and `tick` to it. She identifies this exact code smell as the "Middleman":

> “When an object’s only purpose is to forward messages somewhere else, you have to wonder if it justifies its existence.”

While an unnecessary middleman should usually be removed, `GildedRose` still performs one crucial task: given a raw string like `"Normal"`, it figures out which class plays the item role. Metz refactors `GildedRose` from a stateful instance class into an item factory module. She provides a `.for` factory method (noting with a laugh that while some prefer defining a `.new` method on modules, she “just can't bear that”), eliminates the intermediate reader methods, and strips away all forwarding boilerplate. Callers simply invoke `GildedRose.for(...)` to receive the appropriate item object directly and talk to that item themselves.

- The case statement is eliminated in favor of a clean `class_for` lookup method. - The item classes form a polymorphic duck type adhering to the `tick` interface. - `GildedRose` transforms from an instance-based Middleman smell into a focused Item Factory module. - All forwarding boilerplate (`quality`, `days_remaining`) is deleted from `GildedRose`, since callers now communicate directly with the returned item.

[29:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1749s) Safe Inheritance: Cleaning Up Duplicated Item Roles

Turning her attention to the set of classes playing the item role, Sandi Metz addresses the code duplication that has been tolerated throughout the refactoring. Every one of these classes shares identical state and behavior that can easily be consolidated into a proper inheritance hierarchy. By introducing a shared `Item` superclass, the duplicated setup is pushed up, allowing the individual classes to become straightforward subclasses. Metz directly tackles the common dogma in modern software development regarding inheritance: “Now, despite what you may have heard, inheritance is not evil, and I can tell you exactly when it's safe to use it.”

She lays out strict, concrete criteria for when inheritance is the best tool for the job: - **Shallow and narrow hierarchies**: The inheritance tree must not be deep, nor should it branch out broadly across many levels. - **Subclasses at leaf nodes**: Subclasses must reside at the outer perimeter or leaf nodes of the object graph, meaning they sit at the very edge of the dependency tree and do not know about other objects down the line. - **Complete usage of superclass behavior**: Every subclass must use all the code and behavior defined in the superclass, rather than picking, choosing, or turning off inherited capabilities.

Metz emphasizes that if a problem exhibits these exact characteristics, there is simply no better object-oriented design pattern than inheritance.

```mermaid flowchart TD accTitle: Refactored Item Hierarchy and Factory Dispatch accDescr: Class inheritance structure showing Item base class and subclasses with dynamic lookup table.

subgraph Factory["Dynamic Resolution"] Lookup["Configuration Hash\n{'Aged Brie' => Brie,...}"] Lookup -->|"instantiates"| Item end

subgraph Hierarchy["Inheritance Hierarchy"] Item["Item (Superclass)\n- quality, days_remaining\n- tick (does nothing)"] Item --> Brie["Brie\n+ tick"] Item --> Backstage["Backstage Pass\n+ tick"] Item --> Conjured["Conjured\n+ tick"] end ```

[30:44](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1844s) Fixing the Superclass API and Eliminating the Sulfuras Class

While inheritance is not inherently evil, Metz quips that developers sometimes can be, warning how easy it is to get inheritance wrong. Examining the newly formed tree, she points out a flaw in the public interface: while the `Item` superclass exposes `quality` and `days_remaining`, each of the subclasses defines an additional public method, `tick`. For the superclass to legitimately fulfill the item role, it must also implement `tick`. Developers often implement abstract methods by raising an error (such as `raise NotImplementedError`), but here Metz identifies an elegant default behavior: `tick` can simply do nothing.

```ruby class Item def tick # do nothing end end ```

Once `Item#tick` is implemented as a no-op, an interesting consequence arises regarding `Sulfuras`. The `Sulfuras` subclass had only overridden `tick` to do nothing—the exact behavior now provided by the superclass. As a result, the entire `Sulfuras` class becomes completely redundant. Metz deletes it entirely, noting that the intermediate complexity introduced earlier in the refactoring instantly vanishes.

[32:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1929s) Extracting Business Logic from Configuration

Looking at the factory dispatch method, Metz notes that the remaining `case` statement actually conflates two entirely different responsibilities: it holds the string-to-class mappings, and it contains the algorithm that evaluates and instantiates them. She argues that `case` statements belong in the domain of business logic, whereas mapping arbitrary strings to class constants is purely configuration data.

She extracts the mappings out of the algorithmic code into an independent lookup hash:

```ruby ITEM_CLASSES = { 'Aged Brie' => Brie, 'Backstage passes to a TAFKAL80ETC concert' => Backstage } ```

Metz observes that separating the data from the resolution algorithm allows each to evolve independently. In real-world production systems, as mappings grow, this hash naturally transitions into an external YAML configuration file; if changes become even more frequent, it can easily move into a database table, all without altering the matching algorithm.

[33:07](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1987s) Measuring Success: Squint Tests and Flog Scores

With the refactoring complete, the Gilded Rose module consists of a base `Item` class, three lightweight subclasses with single-responsibility `tick` methods, and a configuration hash used to match strings to the correct classes. Metz compares the final design against the original monolithic conditional using the squint test. Although nesting classes inside a module gives a superficial appearance of visual depth, each class contains only a single level of indentation. Crucially, comparing the intermediate "small methods" version to the final "small objects" version reveals that the syntax highlighting colors are tightly clustered together, proving that code that changes together has been gathered into cohesive units.

Turning to Flog complexity metrics, she invites the audience to guess the new score. An audience member suggests 15, which Metz applauds as remarkably close. While the total score across all files is 33, Metz argues that looking at the gross number misrepresents the true cognitive load: - In the initial single-class implementation, a developer had to hold the entire complex class in their head all at once. - In the refactored version of small, decoupled objects, an engineer only needs to understand one class at a time. - The single most complex class in the new design is `Backstage`, which registers a Flog score of only 12. - The average complexity across the entire set of classes drops to 7.

Metz contends that breaking the problem into many small objects has effectively reduced cognitive complexity by 75%.

[35:19](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2119s) Implementing the "Conjured" Feature

With the architecture properly arranged, Metz circles back to the original assignment that kicked off the entire exercise: adding support for "Conjured" items. She asks the audience to take a moment and visualize what implementing the requirement looks like now.

Instead of wading through a labyrinth of nested conditionals, adding the feature requires writing a concise `Conjured` subclass inheriting from `Item` with its own specialized `tick` method, and adding one entry to the configuration lookup hash. In a single slide, all tests pass effortlessly without touching any existing business logic.

```ruby class Conjured < Item def tick @days_remaining -= 1 return if @quality == 0

@quality -= @days_remaining <= 0? 4: 2 @quality = 0 if @quality < 0 end end ```

[35:41](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2141s) Core Design Lessons: DRY, Abstraction, and Making the Change Easy

Metz summarizes the fundamental principles demonstrated by the refactoring process:

1. **Beware the wrong abstraction**: Beginners are taught to be DRY (Don't Repeat Yourself) at all costs. While duplication is not desirable, Metz warns that premature or incorrect abstraction is far worse: “Trying to fix a problem by increasing the complexity of the wrong abstraction is like chasing a beach ball in the outgoing tide. Every time you take a stroke, it recedes ahead of you, and pretty soon, you're out way over your head.” When forced to choose, choose duplication over the wrong abstraction. 2. **Design for the open/closed principle**: Do not try to guess the future ahead of time. Instead, allow incoming business requirements to serve as the design impetus, providing the necessary information to reshape code so it can adapt easily. 3. **Kent Beck’s maxim**: Quoting Kent Beck, Metz shares his advice on approaching code changes: “Make the change easy, this might be hard, and then make the easy change.” Metz highlights that 99% of her talk was dedicated to doing the hard work of making the change easy, after which implementing the actual feature took just one slide. 4. **Rely on small objects and design principles**: Structuring code around small objects with a single responsibility makes software straightforward and malleable. Metrics like Flog are helpful aids, but because both metrics and human opinions are fallible, developers should master object-oriented principles to understand the true consequences of their design choices.

[37:56](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2276s) Concluding Remarks and Acknowledgments

Wrapping up the presentation, Metz introduces her published work, *Practical Object-Oriented Design in Ruby* (POODR), and mentions that she is actively writing her next book. She notes an upcoming public training course she will be teaching in London in June or July. Finally, she thanks the RailsConf audience and expresses gratitude to the late Jim Weirich for introducing her to the Gilded Rose Kata, concluding the talk to loud applause.

## Notable quotes > [13:08](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=788s) It is far cheaper to keep duplication than it is to have to mess with the wrong abstraction. — Sandi Metz

> [06:05](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=365s) Nobody adds a 10-line helper class to a 5000-line active record object. — Sandi Metz

> [09:56](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=596s) Red is not when you ponder the abstraction. Red is when you scramble toward green. — Sandi Metz

> [25:55](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1555s) When an object’s only purpose is to forward messages somewhere else, you have to wonder if it justifies its existence. — Sandi Metz

## Mentioned Sandi Metz, Jim Weirich, Kent Beck, Gary Larson, Gilded Rose Kata, Flog, Practical Object-Oriented Design in Ruby, RailsConf

## Video description Theory tells us to build applications out of small, interchangeable objects but reality often supplies the exact opposite. Many apps contain huge classes of long methods and hair-raising conditionals; they're hard to understand, difficult to reuse and costly to change. This talk takes an ugly section of conditional code and converts it into a few simple objects. It bridges the gap between OO theory and practice and teaches straightforward strategies that all can use to improve their code.

Sandi Metz, author of "Practical Object-Oriented Design in Ruby", believes in simple code and straightforward explanations. She prefers working software, practical solutions and lengthy bicycle trips (not necessarily in that order) and consults and teaches on all things OOP.

Help us caption & translate this video!

http://amara.org/v/FG0O/

[Source](https://www.youtube.com/watch?v=8bZh5LMaSmE)

中文简报

Sandi Metz 以小型对象化解面向过程的代码膨胀 容忍代码重复的代价远低于采用错误的抽象,因为将条件分支拆解为职责单一的小型对象,才能让软件易于扩展。 Sandi Metz 借助经典的 Gilded Rose Kata,演示了如何系统化地将一段混乱的 43 行嵌套条件分支重构成整洁的面向对象设计。她介绍了代码复杂度指标以及直观的“眯眼测试”(squint test)等实用诊断技巧,同时告诫不要过早进行抽象,也不要本能地将新逻辑塞入遗留的条件语句中。通过有意识地承受中间过程带来的短暂复杂度激增,Metz 将这个庞大代码块拆解为由物品工厂(item factory)和狭窄继承层级协调的多个小型多态类。一旦系统满足开闭原则,添加新的“Conjured”(魔法制造)特性便只需要一个简洁的新子类和一条配置项。 Sandi Metz 将 Gilded Rose Kata 中 43 行的面向过程条件分支重构为多个小型多态对象,展示了在达到最终的简洁之前中间复杂度是如何激增的。 (0:00) Flog 复杂度追踪显示,尽管中间重构步骤暂时增加了代码复杂度,但最终设计将平均类复杂度从 50 降至 7,将认知负荷削减了约 75%。 (33:07) Metz 践行了 Kent Beck 提出的“先让改变变得容易,再做容易的改变”原则,将大部分过程用于筹备架构,从而使新增“Conjured”特性的工作得以在一个独立的类中完成。 (35:41) 演讲阐述了安全继承的严格规则:层级结构必须浅而窄,子类必须位于叶子节点,且每个子类都必须使用父类的全部行为。 (29:09)

Reading summary

Sandi Metz argues that writing maintainable object-oriented software requires breaking procedural code into small, single-responsibility objects communicating via messages. Using the Gilded Rose Kata, she demonstrates that enduring intermediate complexity to make code open for extension ultimately yields simpler systems that are easy to modify. Visual Diagnostics and the Accretion Trap (3:44) Metz diagnoses procedural complexity using the 'squint test,' evaluating code shape for nested conditionals and editor syntax colors for clashing levels of abstraction. In the Gilded Rose Kata, the 43-line monolithic tick method scores 45 out of 50 total Flog complexity points, crammed with 16 branch checks, magic numbers, and arbitrary strings like 'Aged Brie' and 'Sulfuras'. When tasked with adding a 'Conjured' item, standard developer habits prompt modifying the existing conditional, which continuously broke passing tests. Metz explains this accretion trap: engineers default to mimicking existing patterns rather than adding small classes alongside large, bloated structures, causing legacy codebases to continually rot. Enduring Intermediate Complexity to Create Seams (9:56) Because procedural scripts lack the seams inherent in object messaging, Metz refactors by systematically intercepting cases—starting with Normal items—and working back from failing tests to green before restructuring. She insists developers must scramble toward green rather than pondering abstractions while failing, only then flattening the logic to consistent abstraction levels. Tracking complexity across the sequential refactoring steps revealed that intermediate steps made the code objectively more complex under Flog metrics before collapsing into simplicity. Metz notes that inexperienced programmers often abort refactoring during this dip. She also warns against premature DRYing (Don't Repeat Yourself), maintaining that tolerating duplication is far cheaper than binding code to the wrong abstraction. The Open-Closed Principle and Factory Extraction (19:25) Noticing method names with repeating prefixes such as normal_tick and brie_tick, Metz identifies the smell of a 'tortured object' trying to escape. To honor the Open-Closed Principle—extending behavior without modifying existing logic—she extracts standalone Normal, Brie, Sulfuras, and Backstage classes, each encapsulating an independent tick method. This intermediate step temporarily turns GildedRose into a Middleman smell that merely forwards calls. Metz resolves this by transforming GildedRose into an Item Factory module using a class_for lookup mechanism. Callers invoke GildedRose.for(...) to receive a polymorphic duck type directly, eliminating instance delegation boilerplate entirely. Safe Inheritance and Separating Configuration from Logic (29:09) To clean up remaining role duplication, Metz introduces an Item superclass. She outlines strict conditions for safe inheritance: hierarchies must be shallow and narrow, subclasses must occupy leaf nodes without outward dependencies, and subclasses must utilize all superclass behavior. Implementing a no-op tick method on Item as default behavior renders the Sulfuras subclass redundant, allowing its complete deletion. Metz also decouples business algorithms from configuration by extracting class-matching logic into an independent ITEM_CLASSES lookup hash. This separation allows mapping data to evolve into configuration files or databases without modifying dispatch mechanisms. Empirical Simplification and Effortless Extension (33:07) The final refactoring drops the average class Flog complexity score from the monolithic 45 down to 7, with Backstage as the most complex unit at 12. Metz argues this modularity reduces cognitive overhead by roughly 75%, because an engineer needs to comprehend only one isolated class at a time. With architecture arranged around small objects, Metz implements the initial 'Conjured' requirement by adding a single slide of code: a new Conjured subclass inheriting from Item and an entry in the lookup hash. All tests pass without altering existing classes, embodying Kent Beck's maxim to make the change easy before making the easy change. Metz concludes that while both human intuition and automated metrics like Flog are imperfect, adhering to small objects and single-responsibility boundaries keeps software malleable. Relying on concrete requirements rather than speculative designs provides the necessary information to craft durable abstractions.

精炼阅读摘要

Sandi Metz 提出,编写高质量面向对象软件的核心法则在于构建职责单一、互不知晓的小类与小方法,并通过重构消除条件逻辑的膨胀。她以 Gilded Rose Kata 为例指出,在添加新功能前应先通过承担暂时的中间复杂度来重组代码,使系统满足开闭原则,从而让变更变得简单可靠。 代码腐化陷阱与复杂度诊断 (2:14) 在真实业务中,面向对象代码常因堆砌逻辑而逐渐失控。Metz 观察到,开发者在面对新需求时往往会寻找相似逻辑并直接追加分支,导致大类体积失控膨胀,陷入“没有人会向 5000 行的模型中添加 10 行辅助类”的恶性循环。以 Gilded Rose 为例,该类管理名称、品质与剩余天数三个属性,但单一的 tick 方法包含了一段长达 43 行的嵌套条件分支,内含 16 个条件判断、大量魔法数字以及针对 Aged Brie、Sulfuras 和 Backstage passes 的硬编码字符串。当 Metz 尝试直接在原有条件语句中增加新类型 Conjured 时,修改一处就会破坏其他测试,导致扩展彻底受阻。 为了不依赖长期历史背景也能快速评估代码问题,Metz 提出了“眯眼测试(squint test)”作为主观诊断手段。该方法通过脱离具体语法观察代码的形状和颜色:代码缩进形状的剧烈变化代表存在难以推导的深层嵌套分支;而高亮颜色的杂乱交织则说明同一方法混杂了截然不同的抽象层级。从量化指标来看,评估赋值、分支与条件语句的 Flog 指标将原有 GildedRose 类评为 50 分,其中 tick 方法单独占据 45 分,证明其逻辑复杂度极高且极难维护。 重构缝隙与抵制过早抽象 (9:56) 在完整的测试保护下,Metz 拒绝继续在原方法内修补,而是通过引入“缝隙(seams)”将面向过程的脚本重构为消息驱动的面向对象结构。她首先拦截常规商品(Normal)分支,主动让测试变红,并坚持“处于红灯状态时不应思考抽象,而应全力奔向绿灯”的原则,编写最简代码使测试逐一通过。随后,她在绿灯状态下抚平分支结构,分离出以自身为消息接收者的独立方法。在处理 Aged Brie 时,尽管其算法与 Normal 的骨架高度一致(均先扣减天数、再调整品质,过期后双倍调整),Metz 坚决反对过早抽取公用逻辑进行 DRY(Don't Repeat Yourself)。 Metz 指出,初学者常将 DRY 奉为不可违背的教条,但实际上“保留重复代码的代价,远低于维护一个错误抽象的代价”。在系统完整样貌尚未清晰浮现前强行消除重复,会使后续架构调整极为被动。她建议在重构初期用注释标记重复,耐心等待业务模式完全展开后再提炼抽象。随着所有商品分支均被独立方法捕获,原本 43 行的庞大条件语句被彻底删除,该类总 Flog 复杂度从 50 下降至 40,最复杂的单个方法降至 12 分。然而,绘制重构过程的复杂度曲线显示,所有中间步骤在最终简化达成前都经历了客观复杂度的上升,这也是许多开发者半途而废的根本原因。 识别受折磨对象与工厂化演进 (19:25) 完成方法提取后,类中出现了 normal_tick、brie_tick 和 backstage_tick 等一系列具有相同前缀的方法名。Metz 强调,重复的前缀或后缀是微观层面的典型异味,代表内部存在一个“试图挣脱出来的受折磨对象”。为了遵循开闭原则(Open-Closed Principle)——对扩展开放、对修改关闭,必须将这些逻辑剥离为具有独立 tick 方法的专门类。在逐步抽取 Normal、Brie、Sulfuras 和 Backstage 等类的过程中,由于部分计算已外置而部分仍在宿主类中,GildedRose 的公共 API 一度需要动态兼容不同状态,引发了短暂的结构混乱,但这种过渡是走向终极简洁的必经之路。 当各个分支统一为实例化特定对象并调用 tick 时,GildedRose 本身退化为了一个仅负责转发消息的“中间人(Middleman)”。为消除该代码异味,Metz 将 GildedRose 彻底重构为一个商品工厂模块,提供统一的 .for 工厂方法。原先复杂的条件派发被 class_for 查找逻辑取代,调用方直接获取具备多态特征的鸭子类型对象并与之通信,不仅彻底解耦了具体商品实现,也完全消除了在中间层维护数据读取和转发方法的冗余工作。 安全继承准则与配置分离 (29:09) 针对各商品类中残留的重复状态与行为,Metz 重新审视了常被行业视作禁忌的继承机制,指出继承本身并非坏事,但必须满足严格的安全边界:继承层次必须浅而窄;子类必须处于对象图的叶子节点且不产生外部复杂依赖;子类必须完全利用父类的全部行为,而非选择性屏蔽。依据这套标准,她引入了基类 Item,并将默认的 tick 实现为一个空操作(no-op)。这一规范使得原本唯一职责就是执行空操作的 Sulfuras 类变得完全多余,Metz 随即将其彻底删除,直接利用父类多态完成处理。 在工厂派发逻辑中,Metz 进一步将条件分支中的字符串与类映射关系剥离。她认为条件评估属于算法控制逻辑,而“字符串到类常量的映射”纯属配置数据,两者混在一起会阻碍代码演进。通过将映射提取为独立的散列表常量,业务逻辑与配置实现了物理隔离。在生产环境中,该散列表可无缝迁移至 YAML 文件或数据库配置,而无需改动核心匹配算法。最终的眯眼测试显示,所有子类的缩进仅有一层且语法高亮高度聚合,全类平均 Flog 复杂度骤降至 7 分,较初始单体状态实现了约 75% 的认知负荷缩减。 让变更变得简单:新特性落地 (35:19) 在系统架构改造完成后,Metz 回到最初的任务——支持品质下降速度加倍的 Conjured 新商品类型。与最初在 43 行嵌套逻辑中艰难挣扎并不断导致测试失败的经历不同,面对经过面向对象洗礼的架构,实现该需求仅需两步:编写一个继承自 Item 的 Conjured 子类并重写 tick 方法,随后在工厂的配置散列表中增加一行键值映射。整个变更仅用了一张幻灯片的代码量,所有现有测试保持绿灯,无需侵入或修改任何现有商品逻辑。 这一重构过程验证了 Kent Beck 的名言:“先让变更变得简单(这可能很难),然后再做简单的变更。”Metz 总结称,面向对象设计不是脱离实际的教条,而是行业先辈在经历无数痛苦后总结的实用风格指南。试图通过向错误的抽象增加复杂度来解决问题,就像在退潮中追逐沙滩球,越用力陷得越深。开发者应当依靠小型单一职责对象、合理的度量工具以及开闭原则,顺应新需求提供的信息去重塑系统,而非盲目推测未来。 Metz 强调,依赖直觉或过早追求 DRY 容易让人落入错误抽象的陷阱。只有建立坚实的测试防护网,勇敢接纳重构过程中的短暂复杂度,并通过小类与小方法使设计符合开闭原则,软件才能在长期演进中维持低认知负荷与高扩展性。

Detailed notes — zh

## Background 该视频由 Confreaks 频道于 2014 年 5 月 21 日在 YouTube 上发布,记录了 Sandi Metz 在 RailsConf 2014 上的演讲。根据页面简介,该演讲直面现实中充斥着臃肿大类、冗长方法与复杂条件分支的代码现状,通过现场将一段丑陋的条件分支代码重构为若干简单对象,架起面向对象理论与工程实践之间的桥梁。 ## TL;DR Sandi Metz 以经典的“金光玫瑰”(Gilded Rose Kata)为案例,演示了如何将一个包含 43 行深层嵌套条件分支的遗留方法逐步重构为高度解耦的面向对象架构。她先通过切出“接缝”并依靠测试保护,将面向过程的脚本拆分为短小方法,随后告诫开发者警惕过早抽象与对 DRY 原则的教条盲从,容忍重构过渡期复杂度的临时上升。接着,她通过识别重复命名中的“受折磨对象”,将行为多态化并引入工厂模块消除了中间人与分支逻辑,安全地利用浅而窄的继承提取出公共基类。最终,系统认知复杂度大幅下降 75%,并以仅添加几行新代码的优雅姿态轻松实现了原先棘手的新业务需求。 ## Atmosphere & style 演讲伴随着轻快的爵士乐开场,整体风格幽默风趣、循循善诱且充满实战底气。Sandi Metz 极擅长用接地气的比喻化解严肃的软件工程难题,例如自嘲“布尔逻辑认知障碍”、用“眯眼测试”审视代码轮廓,以及将未抽取的类形容为“急于逃离的受折磨对象”。现场互动热烈、笑声频频,听众跟随她一步步经历从面对遗留烂代码的绝望、重构中途指标飙升的忐忑,再到最终删掉废码、小对象轻盈落地的豁然开朗,整个过程极具戏剧张力与治愈感。 ## Bulletin - [00:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=0s) Sandi Metz 提出面向对象代码恶化的核心解法是“拆解为小类与小方法,并最小化彼此认知”,并通过经典的“金光玫瑰”(Gilded Rose Kata)展示如何解构复杂条件分支。 - [17:47](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1067s) 在重构过程中,重构指标往往会呈现暂时的“中间复杂性”恶化波峰;但只要跨越这一阶段,代码结构便能达到真正的简单与解耦。 - [34:05](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2045s) 将 43 行嵌套条件分支重构成一组单一职责的小对象后,系统认知复杂度(Flog 度量)峰值从原始单一大类的 100+ 分断崖式降至 12 分(平均 7 分),降幅达 75%。 - [35:19](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2119s) 重构践行了 Kent Beck 的名言:“先让变更变容易(这可能很难),再去做容易的变更”;架构理顺后,新增 Conjured 需求仅用极短时间即无缝完成接入。 ## Key takeaways - [00:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=0s) 面向对象软件问题的根治之道,在于创建更小的类和更小的方法,并让它们彼此之间知道得越少越好。 - [02:32](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=152s) 借助“眯眼测试(Squint Test)”通过观察代码的缩进形状变化与语法高亮色彩分布,可快速识别代码嵌套层级与抽象层次的混乱程度。 - [06:17](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=377s) 大类具有“重力法则”,当代码体积突破临界点后,开发者往往因缺乏安全感而不敢拆分新类,导致在既有条件逻辑上不断堆砌分支并形成恶性循环。 - [09:56](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=596s) 在面向过程的代码中,需要通过在顶层截获逻辑并向自身发送消息来人工制造“接缝(Seam)”,以此引入间接性并隔离执行路径。 - [11:46](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=706s) 红绿测试循环有明确的心态边界:“红灯不是用来思考抽象的,红灯是用来飞奔奔向绿灯的”,唯有在测试变绿后才拥有安全感从容优化抽象。 - [13:28](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=808s) 过早 DRY(消除重复)极其危险,保留局部的代码重复其代价远低于收拾一个错误抽象所造成的烂摊子。 - [16:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=960s) 表征测试(Characterization Tests)是重构最坚固的防线,能为开发者提供直接删除大块冗长遗留废码的底气。 - [19:25](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1165s) 重构通向极致简单的必经之路是忍受“中间复杂性”,代码度量在中间步骤的短暂恶化是打破僵化结构的正常现象。 - [22:18](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1338s) 代码中重复的前缀或后缀命名模式(如 normal_tick、brie_tick),往往是“内部困着一个想要挣脱出来的受折磨对象”的坏味道信号。 - [25:21](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1521s) 应当将“进行分支选择的原因”与“分支触发后所执行的具体行为”彻底解耦,通过工厂方法解析角色,主流程仅依赖多态鸭子类型(Duck Type)。 - [27:19](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1639s) 若一个类存在的唯一意义就是将消息原样转发给另一个对象,则构成了“中间人(Middleman)”坏味道,应将其精简改造为只负责类查找的模块工厂。 - [29:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1749s) 继承本身并不邪恶,只要满足继承树浅而窄(shallow and narrow)、处于对象图末端叶子节点(leaf nodes)、且子类完全复用基类所有行为这三大安全法则即可放心使用。 - [30:40](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1840s) 通过统一基类 API 契约并提供默认的空操作实现(noop),可以顺理成章地消除行为与基类 100% 重叠的多余子类(如 Sulfuras)。 - [32:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1929s) 将业务派发逻辑与静态配置映射数据解耦为 Hash 配置字典,有助于将算法与数据分离,并让数据具备独立演化的能力。 - [34:05](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2045s) 小对象架构将开发者的心理模型从“一次性理解整个庞大上下文”转变为“只需理解当前正在处理的微小独立对象”,从而大幅降低认知负担。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=0s) 编写面向对象代码的核心建议与“金光玫瑰”的挑战 演讲在一段轻松的爵士乐开场后正式开始。Sandi Metz 抛出了一个核心观察:大家都觉得面向对象(OO)代码很难写,看看我们平时开发的各类应用程序就知道了。大家初衷都是好的,但写出来的代码往往不可避免地在日后演变成自己最讨厌的模样。作为专注于思考“如何编写更好代码”的顾问与讲师,Sandi 发现大家引发的诸多软件问题其实都指向同一个极为简单的解法。正如 Sandi Metz 所指出的:“I say make smaller things, that's all it is. Make smaller classes, make smaller methods, and let them know as little about each other as possible.” 过去几个月里,Sandi 始终痴迷于研究条件分支(conditionals)。现实中充斥着各类恶心、盘根错节的条件逻辑,她不断追问自己:究竟何时该用微小的对象去替代这些分支?具体应当如何做?这么做之后代码又会发生怎样的变化?2013年11月在迈阿密的一场会议上,她将自己的这套痴迷一股脑倒给了社区好友 Jim Weirich(Ruby 社区知名开发者)。Jim 随即向她推荐了“金光玫瑰”(Gilded Rose Kata)。 虽然这是一个知名的编程练习题,完全可以在 Google 上搜出全盘的题目说明与解法,但 Sandi 选择不查任何现成资料。她希望将这个问题当作真实的生产环境故障来对待,将手头现有的代码和测试作为唯一的信息源。她从 Jim 的代码仓库(repo)中克隆了代码,并在保留核心逻辑的前提下做了极小幅度调整,使其更易于在演讲幻灯片中讲解。 ```mermaid flowchart TD accTitle: Gilded Rose Kata 初始结构 accDescr: 展示 Gilded Rose 类及其包含的核心属性与复杂的 tick 方法 subgraph GR["GildedRose 类"] A["属性: name, quality, days_remaining"] B["initialize 初始化方法"] C["tick 方法 (包含 43 行嵌套条件判断)"] end A --> B B --> C ``` - **初始类的属性**:包含商品名称(`name`)、品质(`quality`)和剩余天数(`days_remaining`),均通过构造函数赋值。 - **核心方法**:包含一个名为 `tick` 的步进更新方法,该方法竟是一个长达 43 行的巨大 `if` 逻辑块。 --- ### [02:32](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=152s) 代码度量:Flog 评分与“眯眼测试” 面对长达 43 行的 `if` 语句,Sandi 自嘲自己天生有“布尔逻辑认知障碍(Boolean-impaired)”,主观感觉这代码难如登天,因此她求助于量化工具。她使用 Ruby 生态中的复杂度分析工具 Flog 进行了检测。Flog 是一套 ABC 指标工具(统计赋值 Assignments、分支 Branches、条件 Conditionals 并进行累加),分数越高代表代码越复杂、越难理解和推演。分析结果显示:`GildedRose` 类的总分为 50 分,而单单 `tick` 这一个方法就占了整整 45 分,台下观众闻之发出同情的呻吟。 除了客观的数字化指标,Sandi 还分享了自己在咨询工作中总结出的主观诊断方法。各家公司通常只在业务陷入水深火热、代码濒临崩溃时才会花钱请她去现场协助。客户往往向她展示最恶劣、历史包袱最重、完全失控的代码片段。开发者在被迫向他人解释自己写出的糟糕代码时,都会有一种共通的心理防卫反应:不仅解释功能,还会滔滔不绝地解释“当年为什么不得不写成这样”的背景与借口。这些漫长混乱的解释常让 Sandi 走神,联想到 Gary Larson 漫画里那只名叫 Ginger 的小狗——听人类说话全变成了“blah blah blah Sandi blah blah blah”,直到对方突然来一句“那你看这一行代码我们该怎么办?”才把她猛然惊醒。 为了从海量陌生上下文的恐惧中解脱,Sandi 总结出了高效的“眯眼测试”(Squint Test):后仰身体,眯起双眼观察屏幕上代码的轮廓、形状与色彩分布。 - **形状变化(Changes in shape)**:若缩进层次繁复多变,意味着存在深层嵌套条件分支,推理难度极高。 - **颜色变化(Changes in color)**:在语法高亮下,颜色跳跃意味着代码混杂着不同抽象层次的逻辑,阅读时难以理清脉络。 - **遗留代码现状**:代码中包含 16 处 `if` 语句(多处涉及不等号与 `&` 联合判断)、各种散落的魔法数字,以及 3 个四处流窜的魔法字符串(Magic Strings):`"Aged Brie"`、`"Sulfuras"` 和 `"Backstage passes"`。 --- ### [06:17](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=377s) 失败的修补:大类的重力法则与破窗效应 Sandi 随后开始查阅测试套件。现有测试能够通过,且大多围绕着魔法字符串展开;但唯独有一组名为 `"Normal"` 的测试,其名称在整个 `tick` 的 `if` 逻辑中从未直接出现,显然是被隐式归入了某个未被明说的 `else` 分支。测试表明系统的基本业务类似杂货铺:给定商品名称、品质与剩余天数,每调用一次 `tick`,品质和保质期便同步衰减 1 点,类似牛奶、鸡蛋等易腐商品的过期逻辑。 真正棘手的问题在于测试文件中包含 6 个被跳过(skipped)的测试,针对的是一个名为 `"Conjured"`(魔法制造)的商品。Sandi 意识到自己的任务是在现有代码中支持 `"Conjured"` 相关的业务逻辑更新。她起初非常顺从地尝试直接去修改现有的 43 行 `if` 块,但在反复折腾了数小时后彻底败下阵来:每当她解开一个 `"Conjured"` 测试并修改条件分支试图让其通过时,都会瞬间破坏现有的其他测试用例。 这促使 Sandi 深入反思开发者为何总会强行往已经臃肿不堪的逻辑里塞代码:这是由于缺乏安全感与追随现有模式的本能。初学者往往畏惧新建对象,总是寻找与新需求最接近的代码块并将其塞入;如果该处已经有 `if`,就顺手再并入一个新分支。代码随之不可逆转地膨胀,最终到达一个临界点(Tipping Point)——代码体积大到让后续开发者根本不敢想象把它写到别的地方去。正如 Sandi Metz 所总结的:“Nobody adds a 10-line helper class to a 5000-line active record object. They just get bigger. Once they reach a certain size, they just get bigger.” ```mermaid flowchart LR accTitle: 遗留代码滚雪球循环 accDescr: 展示由于开发者不敢创建新对象导致现有类不断膨胀的恶性循环过程 A["新功能需求"] --> B["在现有类中寻找相似逻辑"] B --> C["在现有条件上添加分支"] C --> D["类体积突破临界点"] D -->|"代码庞大阻碍重组"| B ``` 由于无法直接遵循旧模式写出正确逻辑,Sandi 决定转换策略:在真正添加新功能之前,先严格按照重构的定义(在不改变现有外部行为的前提下调整代码内部结构)重构这套逻辑。测试套件就是重构时“身后的坚固后盾”。她指出当前的 `tick` 方法本质上是一段冗长陈旧的过程式(procedural)脚本,完全不具备面向对象的特征;而在真正的面向对象体系中,核心应当是由众多各司其职的小对象构成,并通过在对象之间传递消息(message passing)来驱动整体业务。 ### [09:56](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=596s) 在面向过程的代码中制造接缝:Normal 物品的重构 面向对象的设计核心在于消息传递,消息带来了一层间接性,使得系统可以在背后自由替换不同的对象。正如 Sandi Metz 所指出的:“Messages create seams so that you can do a different thing, and there is no seam here because this is a procedure.”(消息创造了接缝,让你能够去做不同的事,但这里根本没有接缝,因为这完全是一段面向过程的代码)。因此,要对 Gilded Rose 展开重构,首要任务就是人工切出一条“接缝”(seam)。Sandi 在 `tick` 方法的最上方插入对“Normal”(普通商品)的类型分支拦截,一旦匹配到就立即提前退出(bail)。代码刚一改动,运行测试套件立刻出现了 4 个失败用例,这完全在预期之内。 Sandi 明确拒绝在原本已经臃肿不堪的 `tick` 方法内部继续追加业务逻辑,而是选择“向自身发送一条消息”(send a message to myself),也就是调用一个独立提取出来的新方法。此时测试依然保持 4 个失败,证明执行路径已被精准截获。随后她打开第一个测试用例,着手编写最小实现使之通过:普通商品的 `quality` 每日递减 1,`days_remaining` 每日递减 1,编写对应代码后该测试顺利变绿。接着处理第二个用例:商品已经过了销售截止日(sell-by date),此时 `quality` 应当以双倍速度下降(即每日减 2)。编写处理代码后,出人意料的事情发生了——预期本应还有两个失败,结果某个尚未仔细检视的测试用例居然也顺带变绿了。Sandi 幽默地对此表示大家最喜欢这种意外之喜,她笑着说:“I don't need to understand it, I've got tests.”(我不需要弄懂它,反正我有测试)。针对最后一个边界情况(当品质已经为 0 时不可再降),她直接在外层套上一个 `if` 判断拦截,整个测试集瞬间全面回归绿灯。 ```mermaid flowchart TD accTitle: 接缝重构法提取独立处理逻辑 accDescr: 描述 Sandi Metz 如何在 tick 方法顶部切出分支、捕获执行路径并利用测试重构普通商品逻辑的过程。 subgraph Seam["制造接缝 (Seam)"] A["在 tick 顶部判断 item 类型"] --> B["匹配 Normal 则直接向自身发消息并提前退出"] end subgraph RedGreen["红绿循环快速变绿"] B --> C["捕获执行路径 (4 个测试红灯)"] C --> D["为单个测试写糙快代码,不求优雅"] D --> E["所有 Normal 测试变绿"] end subgraph CleanUp["绿灯后精简重构"] E --> F["统一递减 days_remaining"] F --> G["消除深层 if 嵌套,理顺递减逻辑"] end ``` ### [11:46](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=706s) 绿灯法则与心智模型的重构 回到绿灯状态之后,代码其实写得并不聪明,甚至略显笨拙,但这恰恰是极限重构的精髓所在。Sandi Metz 强调,很多人在红灯阶段浪费了太多精力去构思完美的设计,但这完全违背了 TDD 和重构的节奏。关于红绿测试周期的状态心态,Sandi Metz 指出: > “Red is not when you ponder the abstraction.” > “Red is when you scramble toward green.” 在红灯时,程序员唯一的目标就是以最快速度夺取那颗“挂得最低的绿灯果实”。只有在重构绿灯亮起时,才有安全感去从容审视代码抽象。Sandi 自嘲自己是个“布尔逻辑障碍者”(Boolean-impaired),刚刚为了赶进度写出来的条件语句连她自己回头看都觉得一头雾水。但如今身处绿灯的安全区,便可以大刀阔斧地整理逻辑脉络。 她观察到不论任何分支,`days_remaining` 总是固定减 1,于是将其直接提取到方法的最开端,让代码的故事线变得平铺直叙。接着,如果 `quality` 已经降至 0,方法完全可以直接提前退出,这样一来原本嵌套在最外层的大块 `if` 逻辑便能彻底扁平化。最后,审视剩余的品质扣减情况:并非真的存在两种完全独立的减 1 与减 2 分支,而是所有普通物品默认基础扣减 1,若已超过销售截止日期,则额外再补扣 1。这一调整将代码提升至完全一致的抽象层级,原本晦涩的深层嵌套瞬间演变成了一段清晰易读的代码叙事。 ### [13:28](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=808s) Brie 物品的提取与“错误抽象”的巨大陷阱 完成普通商品的梳理后,Sandi 加快节奏处理下一个物品分类——Brie(布里干酪)。她在分支跳转中加入了 Brie 的分支,原本针对该物品的 7 个测试用例随之全部报错飘红,这再次验证了执行路径被成功拦截。依照相同的节奏,她每次只专注于让一个测试通过,很快便为 Brie 拼凑出了简洁的独立处理方法,全套测试随之再次变绿。 然而此时出现了一个极具诱惑力的技术岔路口。当普通物品与 Brie 的代码并列展示时,两者的逻辑骨架极其相似:都在每日改变天数,都在特定条件下按步长更新品质。过去这些细节深埋在 43 行盘根错节的巨型 `if` 结构里时,没人能看出两者的共同模式,而一旦提取为短小方法,算法结构的相似性便跃然纸上。对于许多被过度灌输“DRY”(Don't Repeat Yourself,不要重复自己)原则的程序员而言,此时极易停下主线工作,转而顺着分支去合并两者的公共逻辑。Sandi 坚决制止了这种冲动,并给出了整场演讲中最核心的论断: > “It is far cheaper to keep duplication than it is to have to mess with the wrong abstraction.”(保留重复代码的代价,远远低于去收拾一个错误抽象所造成的烂摊子。) Sandi 进一步剖析了软件教学中的认知偏误:之所以把 DRY 原则作为第一条铁律教给新手,是因为初学者对面向对象的其他高级概念一无所知,“他们什么都不懂,但谢天谢地,他们能认出重复代码”。然而成熟的工程师必须具备足够的经验与胸襟去容忍局部的重复,静待更多上下文显露,从而发现真正贴切的高级抽象。如果实在担心重复代码遗失,大可使用带数字编号的“重复标记”(dup tags,例如在注释里写上 `dup 6`)像数据库主键一样将分散各处的重复项关联起来,这也远好于过早提取抽象。 - 初学者规则局限:DRY 原则是初学者唯一能够快速掌握的代码评判标准,但这不应成为限制成熟工程师架构决策的教条。 - 容忍暂时的重复:在重构主线未完成前,盲目消除重复往往会导致抽象失真,破坏后续对算法全貌的洞察。 - 落地技巧:遇到多处重复时,在代码中加上诸如 `dup <id>` 的文本标签进行跟踪定位,而不是急于合并不成熟的代码块。 ### [16:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=960s) 攻克 Sulfuras、Backstage 与彻底删除遗留废码 在坚持保留暂时重复的策略下,Sandi 继续处理传奇武器“Sulfuras”(萨弗拉斯,炎魔拉格纳罗斯之手)。按照推断,为其设置空方法垫片(shim)后应该有 3 个测试报错。然而运行测试后,台下发出了一阵笑声——所有测试居然直接全绿通过。查看测试断言才发现,作为一件传说级物品,Sulfuras 的所有属性在系统内永不改变。在此之前面对那 43 行恐怖的嵌套代码时,没有任何人能直观看出“原系统对 Sulfuras 原来根本什么都没做”。一个空方法便轻而易举地解决了这件原本看起来最复杂的物品。随后,她如法炮制为 Backstage(后台门票)实现了阶梯式加速增长并在过期后归零的逻辑。 至此,外部的 `case` 分支已经完整捕获了所有可能进入的业务类型,各个子方法各司其职,测试套件维持完全绿灯。而原先 `tick` 方法内残存的那个长达 43 行、无人能真正读懂的怪物 `if` 树,瞬间沦为了无用代码。Sandi 毫不犹豫地当场将其整块选中并直接删除。她坦言,如果没有强大的测试套件支撑,直接删掉大段不理解的代码足以让任何人吓破胆(freak you out);但若守着无法理解的代码永远不敢动手,软件就注定走向腐化。通过编写外围的表征测试(characterization tests),开发者便拥有了一面牢不可破的“测试防线”,支撑自己一路重构,直至有底气将遗留废码送进垃圾桶。 ### [17:47](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1067s) 眯眼测试、代码度量与“中途恶化”的重构险峰 删掉大块坏代码后,Sandi 带着听众对比了重构前后的代码形态。她使用“眯眼测试”(squint test)来审视代码轮廓:左侧的原代码充斥着层层深陷的对角线缩进与杂乱的语法高亮;而右侧的新代码整体形态扁平垂直,同类颜色规律地聚拢在一起,一眼就能看出小方法的极简之美。 为了避免“美丑”沦为单纯的主观审美偏好,Sandi 引入了 Ruby 静态复杂度度量工具 Flog 的分数。尽管度量指标并不绝对完美,但它汇聚了群体对代码复杂度的客观量化共识: 1. 原始的单一巨型类在 Flog 体系下的得分为 50 分。 2. 重构为一组小方法后,整体得分降至 40 分。 3. 更关键的是,这个 40 分是全类多个微小方法的累加,其中复杂度最高的方法(Backstage)仅有 12 分,代码的局部可读性与维护难度大幅降低。 既然小方法的优势如此显著,为什么现实中绝大多数程序员依然对这种重构望而却步?除了人类倾向于“依照既有代码惯性继续堆砌”的心理以外,Sandi 揭示了一个更为深刻且残酷的重构规律:从臃肿的条件分支迈向一组小方法共经历了 10 个演进步骤,而在展示所有中间步骤的 Flog 分数图表时,整个重构过程呈现出陡峭的波峰,所有中间步骤的代码复杂度全都在不断飙升,甚至远超最初的 50 分。很多程序员在半路上因为看到代码指标恶化、方法变多而陷入自我怀疑并中途放弃,但真正理解面向对象原则的设计者深知,这种局部的“暂时恶化”是打破僵化结构通向真正简明架构的必经险峰。 ### [19:25](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1165s) 中间复杂性与开闭原则(Open/Closed Principle) 在重构过程中,开发者往往不得不面对一段代码变得更冗长、更零碎的过渡期。Sandi Metz 指出,正是因为她坚信重构的威力,才能够从容耐受这种“中间复杂性(intermediate complexity)”。对于尚未体会过小方法价值的人来说,这些中间步骤看起来就像是某种不切实际的空想原则,显得迂腐且不能改善代码。但她向听众承诺:“如果你能看得很远,能看到最终的结局,这种中间复杂性最终会带来极致的简单(this intermediate complexity leads to ultimate simplicity)。” 随着前序准备工作就绪,Sandi Metz 回到了最初的业务目标——实现新需求“魔化物品(Conjured)”。面对眼前的代码,最直接、最容易的做法往往是在现有的条件分支里再硬塞一个分支。然而她直截了当地给出了否定的答案:“不,我不应该那样做。”原因在于当前代码违反了面向对象设计中最核心的开闭原则(Open/Closed Principle,即 SOLID 中的字母“O”)——对扩展开放,对修改关闭。 Sandi Metz 强调,面向对象设计原则并非凭空捏造的高深理论,而是那些写过海量面向对象代码、踩遍所有编程苦坑的前辈们总结出的智慧结晶。他们将避免痛苦的经验凝聚成了一套组织代码的“风格指南”;开发者当然可以选择无视它,但代价就是亲自把所有的痛苦重新体验一遍。从宏观层面看,这套准则的核心诉求非常简单:当系统需要增加新功能时,不应该要求你修改已有代码。她让全场听众闭上眼睛想象这样的世界:如果每次新增特性都不必触碰旧代码,测试就永远是一片代表通过的绿色,开发永远安全无虞,也永远不会引发千里之外莫名其妙的副作用。 - 开闭原则要求在无需修改现有稳定代码的前提下,通过添加新代码来引入新行为。 - 面向对象设计原则的本质是一份关于“如何权衡并组织代码”的实践风格指南。 - 遵循开闭原则能从根本上消除因修改遗留逻辑而意外破坏无关模块的风险。 --- ### [22:18](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1338s) 释放“受折磨的对象”:从过程走向对象 宏观原则落实到微观代码层面时,有着极为具体的代码坏味道信号。Sandi Metz 观察到,当前方法名中反复出现诸如 `normal_tick`、`brie_tick` 这样带有重复前缀或后缀的命名模式,她风趣地调侃道:“只要在代码里看到重复的前缀或后缀,就说明里面困着一个受折磨的对象正拼命想要逃出来(there is a tortured object in there that's trying to get out)。” 在这个节点上,程序员面临着一个深远的抉择:究竟是继续编写过程式脚本,还是开始信任对象?很多开发者有一种不安全感,必须把所有的条件判断和执行细节全部摊在眼皮底下才放心——既想在同一个文件里看清在什么条件下跳转,又想看清跳转后执行了什么具体动作。这种控制欲会迫使程序员在原处不断塞入新方法(例如添加 `conjured_tick`)。但面向对象设计告诉我们,不同的前缀(`normal`、`brie`)搭配相同的后缀(`tick`),正意味着它们应该独立成为具体的类,而原来的 `GildedRose` 应该只持有一个响应 `tick` 消息的实例。 ```mermaid flowchart TD accTitle: 识别重复前缀并提取独立对象 accDescr: 展示从带有重复前缀的过程式方法到提取独立领域对象的转变过程 subgraph Smelly["坏味道:过程式方法簇"] M1["normal_tick"] M2["brie_tick"] M3["backstage_tick"] end subgraph Clean["面向对象:独立对象协作"] GildedRose["GildedRose 实例"] Item["Item 角色 (Duck Type)"] GildedRose -->|"tick"| Item Item -.->|"多态实现"| C1["NormalItem"] Item -.->|"多态实现"| C2["BrieItem"] Item -.->|"多态实现"| C3["BackstageItem"] end Smelly ==>|"识别受折磨的对象并提取类"| Clean ``` 构想这种结构往往比实际写代码难得多。Sandi Metz 现场演示了具体的抽取过程: 1. 新建 `Normal` 类,将原有的 `normal_tick` 简化为 `tick` 搬入其中,并补齐初始化参数与属性访问器。 2. 在原有的 `GildedRose#normal_tick` 中实例化 `Normal` 类并将消息转发过去。 3. 容忍新的中间复杂性:此时只有 `Normal` 变成了独立对象,其他物品(如 Brie)依然在 `GildedRose` 内部计算,为了兼容公开接口,不得不编写临时逻辑去判断“如果有 item 则读其属性,否则读自身属性”。 4. 顺藤摸瓜处理其他类型:提取 `Brie`,转移 `tick`;甚至对于什么都不用做的空方法 `Sulfuras`,也毫不犹豫地抽取一个类并进行转发。 面对把一个空方法大费周章地拆出新类并在原处做转发的做法,Sandi Metz 坦承很多人会心生怀疑,但她坚定地表示:“如果你信任重构,你就会确信这在最后一定会带来好结果。我绝不分心,也不走弯路,我要沿着这条路一直走到重构完成。” --- ### [25:21](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1521s) 消解分支与引入工厂:分离“条件判断”与“具体行为” 当所有物品都被包裹成独立的类后,Sandi Metz 开始回退之前的妥协决策。早前为了不让 `case` 语句过于膨胀,她把逻辑抽成了单独的方法;而现在每个分支里面只剩下一句简单的实例化与转发调用。她果断删掉了那些临时方法,将极度简化的代码直接放回 `case` 分支中。 接着,她着手消除分支中的重复。通过观察可以发现,无论进入哪个分支,最终的目的都是根据名称获取对应的类并进行实例化。如果把根据名称解析类名的逻辑独立出来,代码就能发生质的飞跃。她通过提取一个 `class_for(name)` 方法,将庞杂的 `case` 语句完全转化为类名查找逻辑。 “通过这种方式,我将进行分支选择的原因,与分支触发后所执行的具体行为彻底解耦了(separated the reason I'm switching from the thing I do when I switch)。”在重构后的世界里,外界根本不需要关心 `class_for` 内部到底有多复杂,它只需要返回一个能响应 `tick` 消息的正确对象。至此,早期为了兼顾未重构分支而写的那堆恶心的条件判断属性代码全部被彻底删除。对于 `GildedRose` 来说,它面对的不再是具体的某几个类,而是一个抽象的“Item 角色”——只要能够响应既定的公共消息,它就是一个合格的鸭子类型(Duck Type)。 - 拆分核心关注点:一处负责根据输入解析“是谁”,另一处负责让那个对象“做它该做的事”。 - 消除条件级联:不再需要在业务主流程中维护冗长的 `case` 或 `if-else`。 - 确立角色与协议:调用端只依赖物品对象必须响应的公开 API,而不在乎其具体属于哪个类。 --- ### [27:19](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1639s) 消除中间人(Middleman)与 Gilded Rose 模块化工厂 在消除了所有分支后,Sandi Metz 退后一步审视当前的代码架构:客户端向 `GildedRose` 发送消息,而 `GildedRose` 只是机械地把所有消息一五一十地转交给它所持有的具体 item 对象。她直言不讳地指出:“当一个对象存在的唯一目的就是把消息转发给别人时,你不得不怀疑它是否还有存在的必要。”在面向对象重构语汇中,这种代码坏味道被称为“中间人(Middleman)”。 ```mermaid flowchart LR accTitle: 消除中间人坏味道并引入工厂模式 accDescr: 展示从中间人转发消息的无用类演进为只负责生成对象的模块工厂 subgraph Before["演进前:中间人模式 (Middleman)"] ClientA["客户端"] -->|"tick / quality"| Middleman["GildedRose 实例"] Middleman -->|"转发所有调用"| TargetItem["具体物品对象"] end subgraph After["演进后:模块工厂模式 (Item Factory)"] ClientB["客户端"] -->|"GildedRose.for(item)"| Factory["GildedRose 模块"] Factory -.->|"返回"| NewItem["具体物品对象"] ClientB -->|"直接调用 tick / quality"| NewItem end ``` 然而,`GildedRose` 并非一无是处,它依然承担着一项至关重要的职责:根据给定的字符串名称(如 `"Normal"`),解析并找到扮演该 Item 角色的具体类。因此,Sandi Metz 指出了一个“程序员应该热爱的词”——工厂(Factory)。`GildedRose` 的本质不是一个物品实例,而是一个物品工厂(Item Factory)。 明确了这一职责后,她对 `GildedRose` 进行了最后的改造: - 将 `GildedRose` 从普通 `class` 重构为一个 `module`。 - 定义模块工厂方法 `for(name,...)`(Sandi Metz 调侃道有人喜欢在 module 上定义 `.new`,但她实在受不了这种写法,不过大家见仁见智)。 - 彻底删除了内部存储实例变量的需求以及相应的属性读取器(`attr_reader`)。 - 拔除所有纯粹充当传话筒的 Middleman 转发方法。 重构之后,调用方式转变为向 `GildedRose` 模块发送 `.for` 请求,该工厂随即返回对应的真实物品对象;客户端拿到真实对象后,直接与物品本身进行所有交互。至此,`GildedRose` 的结构被彻底理顺,从一个充斥着长方法与复杂条件逻辑的怪兽,蜕变成了一个权责清晰的工厂模块与一群高度内聚的小对象。 ### [29:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1749s) 继承的安全法则与 Item 基类的提取 Sandi Metz 将注意力转向了扮演 `item` 角色的各个类。长期以来,代码中一直容忍着大量的重复——这些类都具有相同的属性和行为。为了清理这些重复,Sandi 决定建立一个继承层次结构:创建一个精简的 `Item` 基类,将共用代码向上推移到基类中,随后将这些子类中的重复代码统统删除,让它们继承自 `Item`。面对行业中对继承的普遍偏见,Sandi 斩钉截铁地指出:“Now, despite what you may have heard, inheritance is not evil, and I can tell you exactly when it's safe to use it.” Sandi 总结了使用继承时必须满足的安全法则,甚至在台上将这套标准连说了两遍:继承树必须既浅又窄(shallow and narrow),绝不能又深又宽;子类必须处于对象图的最末端叶子节点(leaf nodes),即它们是对象边界上的实体,不再对系统中的其他事物保持感知;同时,所有子类都必须完全使用基类中的所有行为。Sandi 强调:“If that is the problem that you have, there is no better solution than inheritance, and you are free to use it.” ```mermaid flowchart TD accTitle: 继承安全使用边界示意图 accDescr: 展示合法使用继承的三个核心要素:浅而窄的层级、处于对象图叶子节点、完全使用基类代码 subgraph Rule["安全使用继承的三大条件"] direction TB A["浅而窄的层级<br/>(Shallow & Narrow)"] B["处于对象图叶节点<br/>(Leaf Nodes)"] C["完全复用基类行为<br/>(Subclasses use all behavior)"] end Rule --> Match{"完全契合条件?"} Match -->|"是"| Valid["最佳解决方案:继承合法有效"] Match -->|"否"| InValid["避免继承:改用组合或其它模式"] ``` - **浅而窄(Shallow and Narrow)**:继承层级不能深(避免多层派生),层级跨度也不能过宽。 - **叶子节点定位**:子类位于对象图边缘,处于树形调用的末端,本身不依赖更底层的其它复杂业务对象。 - **完全行为复用**:所有子类均需消费并复用超类中定义的所有逻辑,不得出现空置或半吊子复用。 --- ### [30:40](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1840s) API 一致性重构与消灭 Sulfuras 类 尽管继承本身并不邪恶,但 Sandi 开玩笑说“但有时我们人类是邪恶的”,而且继承很容易被用错。审视当前的继承树时,她发现了一个刺眼的设计瑕疵:基类 `Item` 的公开 API 只有 `quality` 和 `days_remaining`,而派生出的 4 个子类却多出了一个公共方法 `tick`。Sandi 认为,基类既然扮演 item 角色,就必须在公共契约中声明并实现 `tick`。对于基类该如何实现 `tick`,虽然可以抛出异常提示“子类必须实现此方法”,但在当前业务语境下,有一个极为优雅的默认行为:什么都不做(Do nothing)。 让 `Item#tick` 默认“什么都不做”的改动,瞬间引出了一项惊人的连锁简化。Sandi 发现传奇物品 `Sulfuras` 的 `tick` 方法重写了基类,做的事情却恰好是“什么都不做”。这意味着 `Sulfuras` 的实现与 `Item` 基类完全一致,直接用 `Item` 替代 `Sulfuras` 是完全等价的。Sandi 随即彻底删除了整个 `Sulfuras` 类,并在现场感叹道:“There is no more Sulfuras class.” 在重构流程中临时引入的中介复杂性,就这样自然而然地消解了。 ```mermaid flowchart TD accTitle: Sulfuras 重构与类消除流程 accDescr: 展示 Item 基类补齐空操作 tick 方法后,Sulfuras 子类失去存在必要并被直接删除的过程 subgraph Old["原有结构"] I1["Item 基类<br/>(无 tick 方法)"] S1["Sulfuras 子类<br/>(tick 方法: 什么都不做)"] I1 --> S1 end subgraph New["重构后结构"] I2["Item 基类<br/>(tick 默认行为: 什么都不做)"] I2 -.->|"行为完全一致<br/>直接删除子类"| S2["(Sulfuras 已移除)"] end ``` - **公共接口统一**:基类与子类应共享相同的方法契约,`Item` 统一向外界暴露 `tick` 接口。 - **默认多态行为(Null Object 倾向)**:对于无需更新状态的特殊实体,基类提供空操作(noop)是最自然的默认回退。 - **消除多余抽象**:当子类的特化行为与基类默认实现 100% 重合时,子类便失去了存在价值,直接移除能立刻降低认知负担。 --- ### [32:09](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=1929s) 提取配置数据与最终小对象全景 消除 `Sulfuras` 之后,Sandi 进行了最后一项代码梳理。原来的 `case` 语句混杂了两种不同的信息:一组“字符串到具体类的映射表”,以及将它们匹配关联起来的执行算法。Sandi 认为 `case` 语句应当承载业务逻辑,而这类映射纯粹属于配置信息。为此,她提取出了一个简单的 Ruby Hash 配置字典,并将算法改造成基于该 Hash 的单一查找逻辑。她补充说,在真实项目中,这个 Hash 可以独立于算法演进——如果经常变动,可以抽离成 YAML 文件;如果变更更频繁,则可以进一步移入数据库持久化。 至此,整套重构宣告彻底完成。代码形态从原先的大方法、大条件分支,彻底转化成了一组小对象集合。在 `Gilded Rose` 模块内部,只保留一个基础的 `Item` 类、三个包含各自 `tick` 实现的子类,以及一份用于决议类映射的轻量配置。如果对小对象结构做“眯眼测试(Squint Test)”,虽然缩进看起来多了一层,但那纯粹是因为外层包裹了模块(Module),其实质嵌套深度只有一级;与早期小方法方案相比,虽然代码垂直行数略微拉长,但语法高亮色彩的聚集度极高,充分证明“因相同原因而发生变化的事物”已被极其紧凑地凝聚在了一起。 - **职责切分**:将易变、静态的映射字典与消费该字典的派发算法彻底剥离。 - **数据独立演化**:配置哈希可独立按需迁移(代码常量 -> YAML 配置文件 -> 数据库记录)。 - **色彩高内聚**:眯眼测试下色彩色块的高度聚集,反映出逻辑与关注点的强内聚度。 --- ### [34:05](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2045s) 复杂度 Flog 评分断崖式下降 75% 进入评分环节,Sandi 留出时间让台下观众竞猜小对象重构后的 Flog 复杂度得分。台下有观众喊出“15 分”。Sandi 赞许这是一个极其出色的猜测,随后公布了整体数值:从最原始的 100 多分,降到提取小方法后的 33 分。然而 Sandi 尖锐地指出,最终方案总分显示的数字其实严重高估了真实代码的认知负担。 原因在于心理模型的承载方式:最初的方案是一个包含了所有逻辑的单一大类,开发者必须将整个类的上下文硬塞在大脑中;小方法方案虽然好转,但仍是一个大类,人们依然得通盘理解它。而最终方案是由若干微小、独立的类构成的生态,开发者在阅读和修改时根本不需要同时在脑中展开所有类,只需要理解当下正在处理的那一个最复杂的对象即可。在这个系统中,最复杂的类是处理后台门票的 `Backstage` 类,其 Flog 分数仅为 12 分(非常接近观众猜测的 15 分),而整个小对象集群的平均复杂度只有 7 分。Sandi 总结道:“And so, I contend to you, the complexity has fallen by 75% because I made many small objects.” | 方案演进阶段 | 架构组织形式 | Flog 总体评分 / 认知瓶颈 | 单一上下文认知模型 | |:--- |:--- |:--- |:--- | | **初始版本** | 单一大类 + 巨大条件分支 | 100+ 分 | 必须同时在脑海中维持所有复杂判断分支 | | **中间版本** | 单一大类 + 大量子方法 | 33 分 | 仍需通读单一大类,理解各子方法交互 | | **最终版本** | 一组独立单一职责的小对象 | 峰值 12 分 / 平均 7 分 | 认知复杂度骤降 75%,单次只需理解当前处理的独立小类 | --- ### [35:19](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=2119s) 实现 Conjured 需求与面向对象设计总结 完成整个架构重构后,Sandi 闭环回到了最初的技术需求:实现 `Conjured`(魔法物品)的特化衰减逻辑。借助已经解耦的小对象结构,实现新需求轻而易举:只需要新建一个 `Conjured` 子类,在其 `tick` 方法内写入专属衰减逻辑,再将其名字添加到类型映射配置中,全部测试便瞬间绿灯通过。Sandi 用这一极速落地印证了重构的真正威力,并引用 Kent Beck 的名言做出总结: > “Kent Beck has a wonderfully succinct way to put this. He says, ‘Make the change easy, and then make the easy change.’ He actually put it a little bit longer. He said, ‘Make the change easy, this might be hard, and then make the easy change.’ And so, we spent 99% of this talk making the change easy, and then it took one slide to make the easy change.” 在全场总结中,Sandi 为现场开发者梳理了一套清晰的认知路径: 1. **警惕错误的抽象**:初学者常被灌输 DRY 原则(Don't Repeat Yourself),但如果必须在“重复”与“错误的抽象”之间做抉择,应当毫不犹豫选择重复。试图通过增加复杂度来修补错误的抽象,“就像在退潮时追逐一个沙滩球”,每划水一步它就离你更远,最终会彻底陷入灭顶之灾。 2. **践行开放/封闭原则(Open/Closed Principle)**:不必强行预知未来,而是编写能随未来变化而平滑自适应的代码。新需求不仅不是负担,反而是催生重构的宝贵信号,指引你如何调整代码结构。 3. **坚持小对象与单一职责**:设计底层的一切支柱,就在于打造职责单一的小型对象。 4. **信任面向对象原则而非盲从度量**:度量指标有用但会失真,个人主观偏好同样不精准;中间重构过程往往会临时增加复杂度,但只要掌握了面向对象设计法则,就能自信地穿透复杂性,写出直观、易变且优雅的代码。 最后,Sandi 在笑声中推介了自己已出版与正在撰写的书籍,并宣布了即将于伦敦开展的公开课。演讲在对提供 Gilded Rose Kata 题目的 Jim Weirich 的致谢声与全场热烈的掌声、爵士乐尾声中圆满落幕。 ## Notable quotes > [13:28](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=808s) It is far cheaper to keep duplication than it is to have to mess with the wrong abstraction. — Sandi Metz > [00:00](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=0s) I say make smaller things, that's all it is. Make smaller classes, make smaller methods, and let them know as little about each other as possible. — Sandi Metz > [06:17](https://www.youtube.com/watch?v=8bZh5LMaSmE&t=377s) Nobody adds a 10-line helper class to a 5000-line active record object. They just get bigger. Once they reach a certain size, they just get bigger. — Sandi Metz ## Mentioned Sandi Metz, Jim Weirich, Gilded Rose Kata, Flog, Gary Larson, Kent Beck, RailsConf, Practical Object-Oriented Design in Ruby ## Video description Theory tells us to build applications out of small, interchangeable objects but reality often supplies the exact opposite. Many apps contain huge classes of long methods and hair-raising conditionals; they're hard to understand, difficult to reuse and costly to change. This talk takes an ugly section of conditional code and converts it into a few simple objects. It bridges the gap between OO theory and practice and teaches straightforward strategies that all can use to improve their code. Sandi Metz, author of "Practical Object-Oriented Design in Ruby", believes in simple code and straightforward explanations. She prefers working software, practical solutions and lengthy bicycle trips (not necessarily in that order) and consults and teaches on all things OOP. Help us caption & translate this video! http://amara.org/v/FG0O/ [Source](https://www.youtube.com/watch?v=8bZh5LMaSmE)

Source quotes

[13:28] Sandi Metz: It is far cheaper to keep duplication than it is to have to mess with the wrong abstraction. [0:00] Sandi Metz: I say make smaller things, that's all it is. Make smaller classes, make smaller methods, and let them know as little about each other as possible. [6:17] Sandi Metz: Nobody adds a 10-line helper class to a 5000-line active record object. They just get bigger. Once they reach a certain size, they just get bigger.

Source quotes

[13:08] Sandi Metz: It is far cheaper to keep duplication than it is to have to mess with the wrong abstraction. [6:05] Sandi Metz: Nobody adds a 10-line helper class to a 5000-line active record object. [9:56] Sandi Metz: Red is not when you ponder the abstraction. Red is when you scramble toward green. [25:55] Sandi Metz: When an object’s only purpose is to forward messages somewhere else, you have to wonder if it justifies its existence.

Original transcript

[0:00] (jazzy music) [0:17] Sandi:So you'd think that writing [0:20] object-oriented code was hard. [0:23] All you have to do is look at our apps, alright? [0:26] We mean well, and we write code that almost always [0:29] inevitably we eventually come to hate it. [0:35] And the more I think about this, these days, [0:36] somehow my job is to think about how to write better code. [0:39] And the more I think about it, the more I think that [0:42] all of the problems we cause have the same simple solution, [0:47] and that when people ask me now [0:49] how to write object-oriented code, [0:51] I give them one small piece of advice. [0:54] I say make smaller things, that's all it is. [0:58] Make smaller classes, make smaller methods, [1:00] and let them know as little about each other as possible. [1:04] And lately I've been on a quest. [1:06] I've had this obsession for the last couple of months, [1:08] and it's been about conditionals. [1:10] There's a lot of code out there [1:11] with nasty conditionals in it, [1:12] and I've been wondering, when should I replace [1:15] conditionals with small objects, [1:17] and how should I do this, [1:19] and what would happen to my code if I do? [1:22] And I was really confident in Miami in November [1:24] and I inflicted this obsession upon Jim Weirich, [1:26] whom some of you probably knew, [1:29] and he pointed me in the direction of the Gilded Rose. [1:33] Now this is a Kata, it's apparently [1:34] really well known, but I don't get out much, [1:37] so I had... (laughter) [1:38] never heard of it. [1:40] And so, it is so famous that you can just Google it, [1:43] and get an explanation of the problem, but I didn't do that. [1:46] I wanted to treat this problem as if [1:48] it was a real production problem, [1:50] and that my only source of information [1:51] was the test and the code. [1:54] And so I looked at his. [1:56] I checked it out of his [repo], [1:58] and I looked at the problem, [1:59] and I was so interested in it that it became the skeleton [2:02] around which I have hung the ideas for today's talk. [2:06] I have altered his code just a little bit, [2:09] but it's just to make it easier to talk about. [2:11] This really is the Gilded Rose Kata, [2:13] and here's how it goes. [2:14] There's a Gilded Rose class, and it's structured like this. [2:17] It has attributes for name, quality, and days remaining, [2:20] it sets those in an initializer, [2:22] and then there's a tick method. [2:24] Now here's the tick method, well actually, no, [2:27] that's just the first half of it, here's the rest. [2:32] Now, I know you can't read this. [2:34] Well, don't even try, even if you can, alright? [2:36] This is the whole method. [2:37] I just want you to get some sense [2:39] of the size and shape of it. [2:39] It's a 43-line "if" statement. [2:43] And this seems really, really hard to me, [2:45] but I am known to be Bullion-impaired. [2:48] (laughter) [2:50] So, I know that my subjective sense of how difficult this is [2:52] to understand is probably not correct, [2:55] and so instead I used some metrics. [2:57] I ran a complexity metric called "Flog" against it. [3:00] So Flog is a metric. OK, what's a metric? [3:02] A metric is a crowdsource idea about something. [3:06] Right? I have my own opinion about how complex this is, [3:09] but I can use this sort of wisdom-of-the-crowd metric, [3:12] the Flog metric, which scores... [3:14] It's an ABC metric, so it scores assignments, [3:17] branches, and conditionals. [3:18] It just counts things, and adds them up. [3:20] Higher scores are worse. [3:21] They indicate a more complex code, [3:23] a code that's going to be harder [3:25] to understand and reason about. [3:26] And so, Flog says the Gilded Rose class scored a 50, [3:30] and that one method tick scored a 45. [3:33] (moans) Yeah, just hurts, doesn't it? [3:36] So, Flog says it's complicated, [3:38] but before we go on I want to introduce [3:40] a very subjective metric about complexity. [3:44] So, I spend a lot of time these days [3:45] going to places and looking at code I know nothing about. [3:51] People call me up, and I go to their shop, [3:52] and I spend a few days. [3:54] And as you might imagine, [3:55] no one calls me if things are going well. [3:59] (laughter) Alright? [4:01] And when I get there, they don't ask me [4:03] to look at the code they're proud of. [4:06] They ask me to look at the most heinous bits of their apps, [4:09] the things that have sort of complex, [4:11] lengthy contexts in history. [4:13] Code that has just absolutely gotten out of hand. [4:16] And not only are the explanations long and confusing [4:19] because the problem is hard, [4:21] but they do that thing that we all do, [4:22] you know that thing you do when you have to [4:24] explain a bit of code that you wrote [4:26] that you're embarrassed about to someone? [4:28] You don't just tell them how it works. [4:30] You feel compelled to explain [4:31] all the reasons why it got that way. (laughter) Right? [4:34] You laugh. I do it. I know you do it, too, right? [4:37] It just hurts. We hate that. [4:38] And so, these explanations are long [4:40] and confusing and they have lots of [4:41] sort of sideways kind of information. [4:44] And there's a point in time, I really mean well, [4:46] but there's a point in time during every [4:47] explanation when I start feeling like that dog, [4:50] Ginger, in this Gary Larson's cartoon [4:54] where it starts turning into, blah, blah, blah, [4:56] Sandi, blah, blah, blah. (laughs) (laughter) [4:58] And then suddenly I get startled back [5:00] into awareness when I hear them say, [5:01] "So, what do you think we should do [5:03] "about this line of code?" (laughs) (laughter) [5:07] And it used to terrify me, right? [5:08] I felt like I had to understand everything [5:10] in order to help with anything. [5:12] But it turned out that after a few trips, [5:14] I realized that there was a really [5:15] simple thing I could do to help me [5:17] identify code that they could benefit from changing. [5:19] And I call this the "squint test". (laughter) [5:24] Here's how it works. [5:25] You squint your eyes, you lean back, [5:29] and you look at the code. [5:30] And we're looking for changes in shape, (laughter) [5:36] and changes in color. [5:39] Changes in shape mean you have nested conditionals [5:41] and they are always going to be hard to reason about. [5:44] Changes in color mean that your code is [5:45] at differing levels of abstraction, [5:48] and it means the story it tells [5:50] is going to be hard to follow. [5:52] Now, what is it about this code? [5:53] Well, it has 16 if statements, [5:55] some of those are not equal to them, [5:56] and connect something with an "&", [5:58] there are three magic strings, they're used all over, [6:00] and a number of magic numbers, [6:01] I don't even know how many. (laughter) [6:04] Now, at least it has tests. [6:06] Oh, I'm sorry, here are the magic strings. [6:07] These three things: [6:08] Brie, Sulfuras, and Backstage passes, [6:11] whatever that means. [6:13] And it does have tests, and they pass. [6:17] Now, there are six skipped tests, alright? (laughter) [6:21] So, I don't know what that's about. [6:24] And so, I pry open the code, I just look at this first test. [6:26] Oh, sorry, the tests cluster around the magic strings, [6:31] except for this set, which is for [6:32] something called "Normal", [6:34] which is never mentioned in the "if" statement. (laughter) [6:37] I suspect there's something in an [L's] branch [6:40] somewhere that matters here, alright? [6:41] So, I pry open the test and I look at it. [6:42] Here's one, they all look just like this. [6:46] I'm selling something, ok? [6:50] Given a Gilded Rose that has this name, [6:51] attribute, and quality, those are our three [add-a-readers], [6:55] When I tick, in this case, quality goes down by one, [6:59] days remaining goes down by one, [7:02] they both go down by one. [7:03] So, it's as if I'm selling milk, [7:04] or eggs, or cheese or something that has a sell-by date, [7:06] that's going to expire, where they go bad at some date. [7:10] OK, so, I'm still exploring around, [7:12] I don't even know what my job is yet, [7:15] and I look at the six skipped tests, [7:17] and there is something called "Conjured", (laughter) [7:22] and they all follow the same pattern, [7:23] all of the tests look like given that [7:24] when I tick, I see this change. [7:26] And at this point, I realize, holy crap, [7:30] I'm supposed to change this code. (laughter) [7:33] And so I tried, I tried, [7:36] very obediently, I tried, but I was a miserable failure. [7:41] I couldn't do it. [7:43] That 43 lines of statement defeated me. [7:44] Every time I went, I would like pry open a Conjured test [7:47] and I'd go make some change in that "if" statement [7:49] to make that test pass, it would break something else. [7:51] I spent hours on it. [7:53] Now, I am impaired, but really, it was hard. [7:54] It would be hard for you, too, I think. [7:56] And so, if changing that "if" statement [7:58] was so hard, you have to ask, [8:00] why was I trying? Why did I try to do, [8:02] what possessed me to try to alter [8:03] that incredibly complicated bit of code? [8:09] And the answer is, I felt like I was supposed to. [8:12] And here's what happens, right? [8:13] You write some code, someone asks for a change. [8:16] What do we do? [8:17] You go look around at the code base [8:18] for a code that's the closest thing [8:20] to the new thing you're trying to do, [8:22] and you put the new code there. [8:24] That's how we behave. [8:25] Novices especially, they're afraid to make new objects, [8:28] so they just go put more code in where they can [8:30] find a thing like the thing they're trying to add, [8:32] and if that place already has an "if" statement, [8:34] they just put another branch on it, right? [8:36] That's how it works. [8:38] And what happens is, so the natural tendency [8:40] of code is to grow bigger, [8:41] and bigger, and bigger. [8:42] And there comes a point, right? [8:43] It gets bigger, and bigger, and bigger. [8:44] And there comes a point where it tips, [8:46] and at that point it's so big that [8:48] you cannot imagine putting code anywhere else. [8:50] We have a bargain to follow the pattern, [8:53] and if the pattern is a good one, code gets better. [8:55] And if the pattern is a bad one, we exacerbate the problem. [8:59] Nobody adds a 10-line helper class to a [9:01] 5000-line active record object. [9:04] They just get bigger. [9:05] Once they reach a certain size, they just get bigger. [9:09] And so, I could not follow the pattern. [9:12] I was not good enough to follow the pattern, [9:14] and so I decided I was going to make a new pattern, [9:17] That I was going to refactor this code. [9:19] Now, this is real refactoring according [9:21] to the definition of refactoring, [9:23] I'm going to refactor this code, [9:25] I'm going to change its arrangement [9:26] without altering its behavior. [9:28] I'm not going to try to add Conjured, [9:30] I'm going to try to move this code [9:31] around so that I can add Conjured. [9:34] And for refactoring, for refactoring [9:36] it's like this test with a wall at your back. [9:38] You've got to have tests, [9:39] or you don't know what you're doing. [9:41] And so, I'm just going to start at the top. [9:43] I'm going to start with these Normal tests, [9:44] and I've got this code. [9:45] This is what tick looks like. [9:47] Now this is a big, long procedure. [9:49] This is not object-oriented code. [9:51] In object-oriented code, you have lots of little objects, [9:54] and you send messages between them. [9:56] And those messages give you a level of [9:57] indirection so that you can substitute [9:59] different objects at the back. [10:01] Messages create seams so that you can do a different thing, [10:04] and there is no seam here because this is a procedure. [10:08] And so the first thing I have to do [10:09] if I want to refactor is I have to make a seam, [10:13] and I'm going to do that just by [10:15] tracking Normal and Bailing. [10:17] At this point, four tests should fail, and they do, alright? [10:23] And I am not about to add more code to the tick method, [10:26] so I'm just going to send a message to myself, [10:29] and four tests should still fail, and they do. [10:33] And so, now that I believe that I have [10:34] caught that execution path, [10:36] I'm going to just break open the first test [10:38] and I'm going to write the code and make it pass. [10:41] Quality goes down by one, that's easy enough. [10:45] I can write that code. [10:47] Days remaining goes down by one, and that test passes. [10:52] Alright? One down, three to go. [10:54] Here's the next test. [10:56] In this case, it looks like I'm out of time, [10:58] I'm on the sale-by-date, so now quality goes down by two. [11:03] So, I'll just make sure my old test keeps on passing, [11:07] and I'll write code to make this test pass. [11:10] And so, now I think two tests should pass, [11:12] so I should have two failures. [11:14] But something I just did made some test [11:17] I haven't looked at pass, and we love that. [11:20] I'm not even going to look at it. (laughter) Alright? [11:24] I don't need to understand it, I've got tests. (laughter) [11:26] OK, so I'm going to just go, [11:28] I'm going to make this one pass, alright? [11:30] I'll just open the last one. [11:32] So, this one says if the quality is already zero, [11:37] don't change it. [11:39] And so I'm just going to wrap this whole thing [11:41] in an "if" statement, and not do anything if quality is zero. [11:46] OK, so now I'm back to green. [11:49] That code was not smart or clever, [11:51] but that's the whole point. [11:53] Once I get to green, I can now refactor. [11:56] So my goal is to get to green as quickly as possible. [11:58] Red is not when you ponder the abstraction. [12:02] Red is when you scramble toward green. [12:05] You're trying to reach to look for the [12:06] lowest hanging green here, [12:08] and so I got there, I'm at green now, [12:10] and I confessed to you already that I am Bullion-impaired, [12:13] and I have written code that even [12:14] I at this moment do not understand, [12:17] but now I'm green, so I can refactor. [12:20] It looks to me that they always [12:22] subtract one from days remaining, [12:24] so I'm going to do that first. [12:25] I like that story better. [12:28] It looks to me like they don't do anything, [12:31] I can just bail if quality is zero, [12:33] so I can take that whole outer [12:34] nesting out of that "if" statement, [12:36] and now once I get to here, [12:37] I can ponder these two remaining cases. [12:39] Are there two cases here? [12:42] Is there a case where I subtract one from quality [12:43] and two from quality, or is this, I don't think so. [12:46] Now that I look at it this way, [12:48] I think I always subtract one from quality, [12:51] and there's a special case in which [12:52] I subtract another, if I'm past the sell-by date. [12:56] And so, I can just delete all that, [12:57] and now I have this, which is all the same [13:00] level of abstraction, and I can understand it. [13:02] I love the story this code tells. [13:04] It's very simple. [13:05] It was easy to get here, and now my test will pass. [13:08] Alright, so we're going to do this [13:09] over and over again, much more quickly than this one. [13:11] I'm going to just take you through a quick reprise here. [13:13] So, I create a seam, I send a message to myself, [13:17] I tracked all the execution paths into here, [13:19] I wrote some code, I hated it, [13:20] I got to green as quick as possible, [13:21] and then I used green to let me [13:22] refactor to code that was sensible, [13:25] and now Normal is done. [13:27] All the Normal tests passed. [13:28] So, now I'm just going to bust right [13:29] through all the other cases. [13:31] Here's Brie, there's a whole bunch of stuff. [13:33] I'm going to turn that into a case [13:34] statement so I can track Brie. [13:37] There are seven tests, and they're all failing now, [13:38] so I have confidence that I have caught them. [13:41] I'm going to write the code, [13:42] but you don't even need to look at it [13:43] because you can see how easy it is, right? [13:45] Now that I am only having to write code for [13:48] one test at a time, it's pretty simple to write the code. [13:50] What I end up with looks like this. [13:53] And now Brie is done. [13:55] And now a really interesting thing happens. [13:59] When this stuff was buried in the [14:01] 43-line "if" statement, [14:02] I had no idea the ways in which Normal and Brie were like, [14:06] but now that I'm using this, they seem a lot alike to me. [14:10] Now, there's differences sort of in the driving data here, [14:12] but the algorithm, you can see the shape [14:14] of the algorithm here, and the algorithm is really the same. [14:17] And it is very tempting. [14:18] We've had the DRY rule browbeat into us so strongly, [14:22] it's very tempting at this point. [14:23] I'm on a road, I'm on a refactoring road. [14:25] It's very tempting now to go on a tangent [14:28] and try to clean this up. [14:29] Because we believe the greatest, [14:30] we've been taught like the greatest virtue is DRY. [14:33] And I will tell you that's the wrong idea here. [14:36] I'm about to get a lot more information [14:37] about what this algorithm looks like, [14:39] and I need to finish the refactoring I'm on [14:42] before I go on any tangents, so I'm going to notice [14:44] that similarity and keep the duplication, [14:47] and just keep on going down this path to see where it leads. [14:50] And this brings me to my first big point of this talk. [14:52] It is far cheaper to keep duplication [14:56] than it is to have to mess with the wrong abstraction. [14:59] The first rule we teach novices [15:00] is don't repeat yourself - DRY. [15:03] But have you ever thought about why we teach them that rule? [15:05] It's because they can't understand anything else. (laughter) [15:09] They don't know anything, but by God, [15:10] they can recognize duplication, and it's a good rule. [15:13] I'm not saying it's a bad rule, [15:15] but I'm saying that now you're grown up, you know more, [15:20] and you have enough experience [15:22] now to tolerate a little duplication [15:24] and wait on a better abstraction. [15:26] It's really hard to deal with the wrong abstraction. [15:29] I often make a "dup tag". [15:31] You know how you can make a "to do" tag? [15:33] Like people say, "Oh, I'm going to [15:35] "lose track of my duplication." [15:36] Well, fix that problem. [15:37] Make a "dup tag" and give every "dup" a number, [15:40] so if you have the same code in two or three places, [15:43] like if that's the sixth instance of duplication, [15:45] give it an ID like a database, [15:46] and put "dup six" in a bunch of places in your code. [15:49] You'll know. [15:50] You'll see the duplication if you change a part, [15:51] like fix the problem of not being able to find it, [15:54] rather than reaching too soon for an abstraction. [15:56] It's much easier to deal with a duplication. [15:59] Alright, so moving on. [16:00] Here's Sulfuras. There's three tests. [16:02] You would think if I put my shim in and put an empty method, [16:06] I would have three test failures. (laughter) [16:12] And yet they all pass. [16:13] So, what's this about? [16:14] Well, I look at the test and I realize [16:16] in all the tests [unintelligible] that [16:17] nothing happens if it's Sulfuras. [16:20] And again, I had no idea when I was [16:22] looking at that 43 lines of statement [16:23] that somehow it all asserted that nothing happened, [16:26] so it turns out this is the code that makes the tests pass. (laughter) [16:30] How nice is that? We love that, OK? [16:33] So here's Backstage, and there's a whole bunch of these, [16:35] and it looks like this. [16:37] That's the code that makes the tests pass. [16:39] And so now, we're totally back to green. [16:41] This looks exactly like it did when I started. [16:44] And this is what I got. [16:46] I put this case statement in the front, [16:48] and it tracked all the execution paths. [16:50] I have a bunch of methods that look like this [16:52] that I created and added to the Gilded Rose class, [16:55] and the rest of the tick methods still [16:57] contains that monstrous 43-line "if" statement, [17:00] which I don't understand, but I no longer need, [17:02] so I'm just going to delete it, it's gone. [17:06] Now if you don't have good tests, this may freak you out, (laughter) [17:11] but then if you feel freaked out by this [17:13] and you don't have good tests, [17:14] you're really making a choice here, right? [17:16] If you have code that you don't understand [17:19] and that you're afraid to change, [17:20] you can keep it forever, if you think that's a good idea, [17:24] or you can put some kind of test [harness) [17:27] around it so that you can refactor, [17:29] but keeping it forever is not really a good choice, [17:31] so you want to get to the point where [17:32] you have confidence that you can safely refactor, [17:34] and it means you never have to understand that code, [17:37] you can do characterization tests around the edges, [17:39] so that you'll have green, [17:41] you'll have a wall at your back for tests, [17:42] and then you can refactor your way, [17:43] to the point where you can delete [17:44] the code that you don't understand. [17:47] And the moral of this story is that [17:48] small methods are simple. [17:51] Here we have it. [17:52] This is the code we just wrote. [17:53] This is the squint test version, don't try to read it. [17:55] This is the code we just wrote on the right, [17:56] and this is how we start it on the left. [17:58] You notice that the shape is flat, [18:02] and the colors are starting to cluster. [18:04] Now, again I believe in metrics, because [18:06] I know that my personal notion of what [18:08] is simple or complex is just my opinion, [18:10] and I totally know that metrics are fallible, [18:14] but human opinion is no more precise. [18:18] And that metrics are kind of a crowdsource idea [18:20] of what a bunch of people thought a metric could be. [18:22] It is a useful data point for me to compare to my own. [18:26] The original class Flogged to 50, [18:27] and this new class Flogs to 40, [18:30] but that overstates its complexity [18:32] because now there's a bunch of methods, [18:33] and the most complex method is Backstage and if Flogs to 12. [18:36] This code is way simpler. [18:39] Well, this is great, and you'd think that [18:41] everyone would just do this. [18:43] And so, it's an interesting question why they don't. [18:46] Now, one of the things I already told you, [18:47] I already gave you one reason, right? [18:48] We do more of what's there. [18:50] And so, the tendency is to add more [18:53] to the "if" statement, if that's there, [18:54] but I think there's another reason [18:55] why we don't undertake these refactorings, [18:58] and it's because of this. [18:59] I'm just going to make the 50 smaller and move it over. [19:02] It took me 10 refactoring steps to get [19:05] from the big conditional to a bunch of small methods, [19:09] and here's the Flog score of all the intermediate steps. [19:15] All the intermediate refactorings [19:16] made code more complicated. [19:19] I know that I'm going to get to that 40. [19:21] I understand the principles of object-oriented design, [19:24] and I know the value of small methods, [19:25] and because of that, I believe in the refactorings, [19:29] and that lets me tolerate the intermediate complexity. [19:31] But if you don't know, if you haven't learned [19:35] about the value of small methods, [19:36] it's hard to undertake those intermediate steps. [19:38] They seem like academic things that [19:43] people will do that are for some pie-in-the-sky [19:48] principle that don't improve code, [19:51] but I can promise you that if you can see [19:53] far enough to see to the end, [19:55] this intermediate complexity leads to ultimate simplicity. [19:59] And, so now I'm going to circle back around my task, [20:02] now that I've done this refactoring, [20:04] I can circle back around my original task [20:05] which is to implement Conjure. [20:08] How should I do this? [20:10] Here's what I got, I've got this. [20:13] Should I do that? [20:17] It would be easy, it would be really easy. [20:20] The answer to that is "no", I should not do that. [20:23] That is not the way I should solve this problem, [20:26] and it's because this code is not open/closed. [20:29] It is not open for extension, and closed for modification. [20:34] Open/closed supplies the "O" in [solid], and it is, [20:37] and I'm going to say it right out loud, [20:38] it's a principle of object-oriented design. [20:43] It's one of the pieces of cumulative wisdom [20:46] created by folks who've written [20:47] a mountain of object-oriented code, [20:49] and they have experienced every [20:51] possible kind of programming pain. [20:54] And over time, they have noticed some principles, [20:58] and they developed a style guide [21:01] about how to organize code. [21:03] That's what object-oriented design is. [21:05] That's what the rules of object-oriented design are. [21:07] It's a style guide about how to organize code [21:10] with all the obvious tradeoffs, [21:12] all the places where you can make your own decisions. [21:14] In this case, you can feel free to ignore their discoveries, [21:19] in which case you'll get to experience [21:20] all that pain over again for yourself. [21:23] That's what will happen. [21:24] On the macro level, this style guide says [21:26] it's best to arrange code so that adding new behavior [21:30] does not require that you edit existing code. [21:34] I know that seems impossible. [21:35] I'm going to say it again, right? [21:37] Open/closed says you ought to be able to add new behavior [21:40] without editing existing code. [21:43] Now, forget about how impossible that seems for a minute. [21:45] I just want you to imagine something for me. [21:47] Imagine the world, imagine your apps, if that is true. [21:53] Imagine that you can add new behavior [21:55] without editing existing code. [21:57] Think about what that means. [22:00] It means you always have green tests, [22:02] it means you are always safe, [22:04] it means you never cause some [22:06] distant and unrelated side-effect. [22:08] That is a sweet, sweet world, [22:10] if your code is open/closed. [22:12] And so, on the macro level, we are trying to get [22:14] to the point where we can add new behavior, [22:16] without editing existing code. [22:18] And on the micro level, what that means here, [22:20] right now, in this code, [22:22] is that when we see methods that have [22:24] a repeating prefix or repeating suffix, [22:27] there is a tortured object in there [22:30] that's trying to get out. (laughter) [22:34] Right here, in this place, [22:36] you're about to make a decision [22:38] that's going to have consequences [22:40] that echo through your code base forever. [22:43] Are you going to write procedures, [22:45] or are you going to trust objects? [22:48] If you insist on having all the logic visible, [22:50] right here where you can see it, [22:52] you are insisting really on knowing [22:54] both the condition on which you switch, [22:56] and the thing that you do, [22:57] the action that you take when that switch happens. [23:00] If you're uncomfortable, and unless [23:02] you know both those things at once in this file, [23:05] under your eyes, in this code, then you're going to [23:07] be forced to add a new method right here. [23:09] You have to put that conjured tick method right here. [23:12] But if you don't, if you're OK with that, [23:14] you can listen to object-oriented design. [23:16] It says that when you have differing prefixes, [23:18] and common suffixes, then what you really have [23:21] is a normal class that ought to have a method tick, [23:25] and a Gilded Rose ought to be holding on [23:27] to an instance of it. [23:29] And it is real easy to right that code. [23:31] If you can think of that thing, thinking of the thing [23:34] is far harder than writing the code. [23:36] Here's how the code looks. [23:38] I've got this Normal tick method, [23:39] I'm just going to call it "tick", [23:41] I'm going to put in a Normal class, [23:43] I'm going to throw a cruft in there to get [23:44] the initialization and the attributes defined, [23:47] I'm going to go back into Gilded Rose [23:49] and the Normal tick method there, [23:50] I'll get an instance of my new class, [23:52] and I'll forward this message - boom, that's it. [23:56] Alright, well, so I've got this, [23:59] so Normal is an object, but nothing else is, [24:02] and I'm about to go back on the path where [24:03] I have to increase intermediate complexity, [24:07] because look what just happened, alright? [24:08] My new Normal tick method looks like this. [24:10] It uses this item class. [24:12] But Brie, the Brie tick method is still [24:13] calculated inside the Gilded Rose. [24:16] The quality and days remaining are part [24:18] of the public API for Gilded Rose, [24:20] and so now I have to say, well, [24:21] if I have an item, go get the item's quality, [24:23] otherwise, get the one I know about. [24:25] And I have to do the same thing for days remaining, alright? [24:28] It looks messy, but it's short-term, it will go away. [24:32] And so, let's just walk through all the other objects. [24:34] Now that you understand this pattern, [24:35] it's really easy, right? [24:36] Class Brie, move method tick, [24:38] put the cruft in there, forward the message. [24:41] Easy enough. [24:43] This is really interesting. [24:45] Now some trust is coming into play, right? [24:47] I have an empty method, look what I have to do. [24:49] Make a new class, put the method in, [24:50] pry this method open, get an instance [24:52] of that class, forward the message. [24:53] You can be forgiven for being suspicious about this, [24:58] but if you trust the refactorings, [24:59] you have confidence that this is [25:00] going to turn out well in the end. [25:02] I'm not going to diverge, I'm not taking a detour, [25:05] I'm going to go all the way down [25:06] this path and finish this refactoring. [25:09] Backstage, I'll make the tick method, [25:11] I'll make that stuff, I'll do the forwarding. [25:15] Alright, so now they're all objects and I've got this. [25:21] And I'm back here. [25:23] So now, in the beginning, I moved logic [25:27] into methods of their own, [25:28] because I didn't want to put a bunch [25:29] of code in this case statement, [25:31] but now that I have objects, everything is simpler, [25:33] and I'm going to just start rewinding my decisions. [25:37] I'm just going to delete the method, [25:39] and shove the code that used to be in the method [25:41] back up in the branches of the case statement. [25:44] We'll do that. [25:46] Now, earlier I said that duplication was [25:48] cheaper than the wrong abstraction, [25:49] but now we're starting to see abstractions, [25:51] and I'm just going to go and abstract away some duplication. [25:55] I'm going to put the cruft back up in here [25:56] so in Gilded Rose, I no longer need that. [25:59] What I really need to do is be able to get an item. [26:02] And the way I need to get an item is that. [26:04] That's how I'm going to get it. [26:05] If I just knew the class name, [26:06] I could send that message to it, and it would work. [26:09] And it's actually really easy to figure out the class name. [26:12] The code's already here. [26:14] It's this, there it is. [26:17] That will get me the class name back, [26:18] so if I just give that a name, [26:20] I can send that message to myself, [26:22] and now I have the right kind of item. [26:25] I don't need a name anymore, so that got simpler. [26:28] So now, I have separated the reason I'm switching [26:31] from the thing I do when I switch. [26:34] And I can just forget about what's [26:36] inside that class [form] method. [26:37] I don't really care anymore. [26:38] It just answers the right class. [26:40] It's going to work. [26:41] It's going to hand back a thing that can [26:42] answer the message I'm going to send to it. [26:44] And now tick looks like that and these now, [26:46] so I'm rewinding the complexity, [26:48] I don't need this anymore. [26:49] I have items in every case, so I'll get rid of all that. [26:52] And now here's the whole body of code that I have. [26:56] I'm holding an instance of the correct item object, [26:58] and I just sent it the tick method. [27:00] so, we have four different, down at the bottom there, [27:02] you can see we have four different kinds of item classes, [27:04] but from Gilded Rose's point of view, item is a role. [27:08] It doesn't think of it like this, it thinks of it like that. [27:12] You just need someone in there that can answer that API, [27:15] that knows those messages, it's a [duck] type, if you will. [27:19] And if you look at the code I have now, [27:20] the message passing works like this. [27:23] All these messages get forwarded, [27:24] and if you had a Foo that had a Gilded Rose [27:26] that sent those messages, it would look like this. [27:29] And so, now I'm in a situation like this. [27:32] When an object's only purpose is to [27:34] forward messages somewhere else, [27:35] you have to wonder if it justifies its existence. [27:38] This actually is a code [unintelligible] that has a name, [27:40] and its name is Middleman. [27:43] So, if that's all the Gilded Rose does, [27:46] it probably shouldn't exist, but it turns out [27:48] it still does something important. [27:50] Given a string like Normal, it can figure out what [27:55] item class, what class plays the appropriate item role. [27:59] And so now, I'm going to use another word [28:01] that you should love, you should love this word. [28:05] Gilded Rose, the only thing that [28:06] Gilded Rose is is an item factory. [28:09] I just need to figure out how to get the right object, [28:11] and then I can send it a message. [28:13] We've simplified our problem by [28:15] separating the thing I'm switching on [28:17] from the thing I do when I switch. [28:19] We've divided those things in half, [28:21] so I can make the code [unintelligible] less, [28:23] and we can do smaller things. [28:26] I don't need to know what they do, [28:28] I just need to know how to get the right one. [28:31] And so, I'm going to change this code to reflect the reality. [28:33] I'm going to make Gilded Rose a module. [28:35] I'm going to say four, some people put new. [28:37] They make a new method on module, [28:39] and I just can't bear that, [28:41] but it's OK with me if you do it that way. [28:44] So, I have to make it a class method [28:45] because I'm calling it. [28:46] I'm no longer keeping an instance of anything, [28:48] so I don't need an [add-a-reader]. [28:50] All these Middleman messages now, [28:51] since you're really going to talk to the item [28:53] that you get back when you call four, [28:54] all these messages, they just go away. [28:57] So now, this is what we have. [28:59] And the way you use it is you send four [29:01] to Gilded Rose, and it gives back an item, [29:04] and it's the item that you talked to. [29:07] And so, now that we've fixed the Gilded Rose, [29:09] I'm going to turn my attention to the [29:11] classes that play the item role. [29:16] There's a lot of duplication here that [29:18] we've been tolerating for a long time. [29:19] They all have this in them. [29:20] And I'm going to create an inheritance hierarchy, [29:23] and clean that up. [29:25] I'm going to make a little item class, [29:27] push all that stuff up to it, then all these guys, [29:29] I can delete that code from all these guys, [29:31] and make them subclasses of item. [29:34] Now, despite what you may have heard, [29:37] inheritance is not evil, [29:41] and I can tell you exactly when it's safe to use it. [29:43] Now here's what you want. [29:45] You want a shallow, narrow hierarchy, [29:48] you don't want it to be deep, [29:49] and you don't want it to be wide, alright? [29:51] Shallow and narrow, you would like the subclasses to be, [29:54] OK, I will say this twice. [29:56] You would like the subclasses to be [29:57] at the leaf nodes of your object graph, right? [30:01] So, you have objects, and you've got other objects, [30:03] and you've got other objects, [30:04] and down at the end of your sort of tree, [30:06] there are objects that don't know about any other things. [30:09] Right? So we want the subclasses to be [30:11] the leaf nodes of the object we have to be at the edge, [30:14] and we want all the subclasses to use [30:17] all the codes in the superclass. [30:19] Now, I'm going to repeat that again. [30:20] Shallow, narrow, subclasses at the leaf nodes, [30:24] and subclasses use all the behavior in the superclass. [30:26] If that is the problem that you have, [30:29] there is no better solution than inheritance, [30:31] and you are free to use it. [30:34] So, however, although I love inheritance, [30:37] I use it in appropriate ways, and it is not evil, [30:40] but sometimes we are. (laughter) [30:42] You might be. [30:44] And it's easy to get inheritance wrong, [30:46] and this tree has a little problem, [30:50] and it's this, I don't like this. [30:52] The public API of item is quality of days remaining, [30:55] and the public API of those four subclasses [30:57] contains one additional method tick. [30:59] And I think that superclass ought to play the item role, [31:01] which means to me it's got to implement tick. [31:04] And the question then becomes, [31:06] what is the appropriate implementation [31:08] of tick to put in the superclass? [31:10] You could define tick and have it raise an error [31:13] that says subclasses have to implement tick. [31:15] You could do that, I do that sometimes, [31:17] but here I think there's a default implementation [31:19] that's appropriate, and it's this. [31:23] Do nothing. [31:26] It's perfectly OK with me, tick to do nothing. [31:29] And now, I did that because the [31:30] inheritance heirarchy bothered me, [31:32] and I'm just removing code now. [31:35] Now that I've done that, you might notice something [31:37] about Sulfuras' implementation of tick. [31:39] It overrides item, it subclasses item to [31:42] override tick to do exactly what the superclass does. [31:45] And what that means is that here [31:50] it would be equally correct to say this, [31:54] which means that this class is not necessary [31:59] and all the intermediate complexity that I created [32:01] as I was following this refactoring just went away. [32:06] There is no more Sulfuras class. [32:09] So, I'm going to do one last thing. [32:11] We're almost finished here. [32:12] So, this case statement contains [32:14] two different types of information. [32:17] It contains a set of string to class mappings, [32:20] and it contains the algorithm to hook them up. [32:24] And I contend to you that case statements [32:26] are meant for business logic, [32:27] and this doesn't really feel like business logic. [32:30] This feels like configuration information. [32:33] And so, I'm just going to extract configuration data here. [32:37] I'm going to make a hash, and then I'm going to change [32:39] the algorithm to just be the algorithm that uses that hash. [32:44] Now, in real life, this would probably go [32:46] through some transitions where now the hash [32:48] can change independently of the algorithm [32:51] that matches these things up, [32:52] and if you find the hash changing a lot, [32:54] you might be tempted to maybe make it a Yamo file, [32:57] and if you find the Yamo file changing a lot, [32:58] you might be tempted to put in the database. [33:01] Now I can vary that data independently of [33:03] this rule about how they get hooked up together. [33:07] And so, that's it, that's the whole refactoring. [33:10] We've got a bunch of small objects [33:12] now instead of small methods. [33:14] Here's the whole code. [33:15] In the Gilded Rose module, there's an item class, [33:18] and then there's three item subclasses, [33:21] each of which contains a tick method. [33:23] There's a set of configuration information [33:26] that's used by this algorithm to decide [33:28] what item class is appropriate for what string. [33:31] Here's the squint testable version of small objects, [33:35] and this is it compared to the original big conditional. [33:38] Now that's interesting that in the small objects string, [33:40] it looks like it's nested too deep, but it's not. [33:42] I just have the classes inside the modules. [33:44] Right? So, that is only really one level of indenting. [33:47] The more interesting comparison here is [33:48] the squint test between the intermediate solution, [33:51] the small method solution, and the small object solution. [33:54] Notice that small objects is a little bit longer, [33:56] but the colors are clustered more tightly together. [34:00] So we have really distilled the things [34:02] that change together in single places. [34:05] Here's the Flog scores that we used to have. [34:08] So, OK, I have time to make you guess. [34:11] I made a bunch of small objects...what's the Flog score? [34:14] Male:Well, 15. [34:18] Sandi:But you know what? OK, here. [34:20] What's in the intermediate, we'll come back to that. [34:23] I like that 15 guess, that was an excellent guess, [34:25] and you'll know why in a minute. [34:26] Here's the intermediate complexity scores. [34:29] Alright, so I've got this. [34:30] That 33 vastly overstates the complexity [34:33] of the final solution, and it's because of this. [34:36] When you have the first [string] was one class, [34:38] the whole Gilded Rose class, [34:40] and you gotta kind of know all about that class. [34:41] And the second solution of the small methods [34:43] it was the Gilded Rose version two, right? [34:46] It was a single class, and you gotta kind of know, [34:48] you've gotta hold that class in your head. [34:50] This third solution is a bunch of small classes, [34:53] it's a bunch of different classes, [34:54] and you don't need to reason about all of them at once. [34:57] As a matter of fact, you really only need to [34:59] read the most complicated object in there, [35:00] and the most complicated class is a backstage class [35:02] and it Flogs to 12, close to the 15, [35:05] and the average complexity of the set of [35:07] classes in that final solution is seven. [35:10] And so, I contend to you, the complexity [35:13] has fallen by 75% because I made many small objects. [35:19] And so now I'm going to circle back around [35:20] to my task, implement Conjured. [35:23] Take a minute and imagine how to do it. [35:30] There's a code that makes all the tests pass. [35:34] Here's how to use it. [35:38] And now we're done. [35:41] Alright, so, summary. [35:45] When you are new at this, they told you DRY. [35:48] Right? Don't repeat yourself. [35:49] And I'm not saying it's bad, [35:51] and I'm not saying that duplication is good, [35:53] but I'm telling you that if your choice [35:54] is between duplication and the wrong abstraction, [35:57] you should choose duplication. [35:58] Trying to fix a problem by increasing [36:01] the complexity of the wrong abstraction [36:03] is like chasing a beach ball in the outgoing tide. [36:06] Every time you take a stroke, it recedes ahead of you, [36:08] and pretty soon, you're out way over your head. [36:10] It's very hard to fix those problems. [36:14] Next, don't try to get to the future. [36:16] Open/closed, the right code that can [36:18] adapt to the future when it arrives. [36:20] New requirements, this requirement to implement Conjured [36:22] was the impetus to make a change. [36:26] It gives you the information you need about how [36:29] to make a choice about how to rearrange your code now, [36:32] so that you can do the next thing. [36:35] Kent Beck has a wonderfully succinct way to put this. [36:38] He says, "Make the change easy, [36:42] "and then make the easy change." [36:45] He actually put it a little bit longer. [36:46] He said, "Make the change easy, this might be hard, [36:50] "and then make the easy change." [36:52] And so, we spent 99% of this talk making the change easy, [36:56] and then it took one slide to make the easy change. [37:00] At the core of this, at the underpinnings of all this [37:02] is the idea of making small objects, [37:04] making objects that had a single responsibility. [37:07] And finally, trust the principles of object-oriented design. [37:12] They let you predict the consequences [37:14] of your code arrangement choices, [37:17] and learning something about what those consequences are, [37:20] is going to let you raise your game. [37:22] Metrics are useful, but they're fallible, [37:25] but opinions are no more precise, [37:26] so use metrics to give you another body of information [37:30] about how complicated your code is, [37:32] and then learn the rules of object-oriented design [37:35] so that you can choose which direction you want to go in. [37:39] Intermediate refactorings often make code more complicated, [37:43] but if you know the rules, you can [37:44] trust yourself to work through complexity, [37:46] and finally reach more open/closed code that's simpler, [37:50] and smaller, and that lets you have straightforward, [37:53] changeable, beautiful code. [37:56] I'm Sandi Metz, I wrote this book, [38:01] I'm writing this book, (laughter) [38:04] It'll be in the slide deck. [38:06] I'm teaching in London. [38:07] There's a public course in London coming up in June or July, [38:09] in case you're from over there. [38:11] Thanks to you all, and thanks to Jim Weirich [38:13] who gave me this Kata. [38:15] (applause) [38:26] (jazzy music)