Back to HackerTubeYouTubeNovember 13, 2026Duration:38:47

Make the change easy: Sandi Metz refactors Gilded Rose

RailsConf 2014 - All the Little Things by Sandi Metz

CConfreaks

Full transcript

English
Watch on YouTube
Contents

Make the change easy before making it

Source 00:54

A new requirement often arrives in code that is already hard to change. Should we add another branch because that is where similar behavior lives? Sandi Metz's answer in this RailsConf talk is to first rearrange the existing behavior under tests, then add the feature in the simpler structure. Her general advice is to make smaller classes and methods that know as little about one another as possible.

Jim Weirich pointed Metz to the Gilded Rose kata, a programming exercise. She deliberately treated his slightly adapted version as production code, using only its code and tests to discover its behavior. The example is a demonstration of a route through a particular codebase, not a recipe that makes every conditional wrong.

A passing suite reveals both a safety net and a missing feature

Source 02:14

Gilded Rose stores a name, quality, and days remaining; a tick advances the item's state. Its original tick contains a 43-line conditional. Metz's Flog score was 50 for the class and 45 for tick. She describes Flog as an ABC complexity measure that counts assignments, branches, and conditionals: higher scores suggest more work to reason about, but a score is still only one signal.

View full image

Her informal “squint test” looks at code from a distance: changes in shape expose nesting, and changes in color can suggest mixed levels of abstraction. Here she counts 16 conditional statements, repeated special-name strings and magic numbers. Brie, Sulfuras and Backstage passes have groups of tests; Normal has tests even though its name is absent from the original conditional.

The existing tests pass, but six Conjured tests are skipped. One Normal test says that a tick reduces both quality and days remaining by one, like stock with a sell-by date. When Metz tries to satisfy Conjured directly, a change that fixes one test breaks something else. She observes that following an existing pattern can reinforce a bad one: an already large conditional attracts another branch and grows harder to escape.

Capture one path and get back to green

Source 09:19

Metz postpones the feature. Refactoring, in her definition, changes the arrangement of code without changing its behavior. Tests provide feedback, though they protect only the behavior they actually exercise.

A seam is a point where behavior can be redirected. She intercepts the Normal path and returns early; four Normal tests fail, confirming that the path she intended to capture is being exercised. She then delegates that path to a separate method and again checks the four failures. The image shows the initial interception before the delegation.

View full image

Working one test at a time, she restores the ordinary quality and day decrement, then the faster decline at the sell-by boundary, and handles a quality already at zero. One additional test passes along the way. Her first implementation is awkward, but with the suite green she can safely reshape it: decrement days first, return early if quality is zero, then subtract one from quality and another when the updated remaining days meet the displayed boundary.

View full image

This is the code shown in the talk, not a proof of a universal lower bound: its zero check precedes two possible decrements. Metz's practical sequence is capture the path, see the expected failures, get the tests green with a simple implementation, then improve its structure while checking the tests.

Keep duplication until the shape of the abstraction is clear

Source 13:31

She turns the dispatch into a case statement and intercepts Brie. Seven failing tests give her confidence that she has reached that path; she implements its behavior one test at a time. Normal and Brie now have visibly similar algorithms driven by different values. Metz resists immediately combining them because other cases may reveal what the common abstraction should actually be.

View full image

Her point is not that duplication is desirable forever. DRY means “don't repeat yourself,” and Metz considers it useful, but says an incorrect abstraction can cost more to untangle than temporary duplication. To avoid losing track, she suggests marking related copies with the same numbered duplication tag.

Sulfuras provides a surprise: its three tests pass with an empty method because the tested behavior is to do nothing. She extracts Backstage too. Once the new dispatch and methods cover the paths, she deletes the old conditional. For unfamiliar code without a strong suite, she recommends characterization tests: record observed behavior at the boundary before rearranging it. Their confidence remains limited by the cases they cover.

Small methods help, even when the route gets messier

Source 17:47

The resulting class has a flatter shape and related operations cluster together. The class Flog score falls from 50 to 40; its most complex method, Backstage, scores 12. Metz considers the method-level view useful because each method can be considered separately.

View full image

There is a less obvious obstacle: her ten intermediate refactoring steps increased the measured complexity before it fell. The chart begins to show that rise. Metz argues that knowing the intended destination helps her tolerate intermediate scaffolding; the observation is about this sequence, not a guarantee that every increase leads to improvement.

View full image

The remaining switch still couples selection to behavior

Source 19:59

Adding Conjured to the new case statement would be easy, but Metz declines: each new type would still require changing that dispatch. She introduces the open/closed principle, an aspiration to extend behavior while minimizing changes to existing behavior. She imagines an ideal with no unrelated regressions; that is a design goal, not a guarantee that additions are safe or tests can be omitted. Design principles involve choices and tradeoffs.

Repeated method suffixes suggest to her that each item type wants its own object with a common tick operation. The decision separates the reason for selecting a type from what that type does after selection.

Move behavior into objects, accepting temporary scaffolding

Source 22:18

Metz moves Normal's method into a Normal class, adds initialization and attributes, and has Gilded Rose create the object and forward tick. Brie remains inside the original class for the moment, so accessors for quality and days remaining temporarily have to choose between the new object and the old state. She repeats the extraction for Brie, Sulfuras and Backstage, even creating a class for the empty Sulfuras method.

View full image

Once every path uses an object, Metz reverses some earlier steps: the small forwarding methods can go, common construction can be extracted, and the temporary conditional accessors disappear. Selection now finds a class; construction produces an item; the item's tick supplies the behavior. The temporary complexity had a specific exit, rather than being an end state.

Treat item as a role and make the middleman a factory

Source 27:00

From the caller's perspective, an item is a role: any object that answers the required messages can play it. Metz calls this duck typing. Normal, Brie and the other implementations can respond differently to the same tick message without the caller needing their internal algorithms.

View full image

An object that only forwards messages is a “middleman” worth questioning. Gilded Rose still has one useful responsibility: selecting the right item class from a name. Metz changes it into a module acting as a factory, which creates and returns the appropriate object. The caller then communicates directly with that returned object. In the shown version the factory method is named for; it chooses a class and calls new on that class. The two names have different roles.

View full image

Use inheritance only when the shared behavior fits

Source 29:16

Now that the item classes are visible, Metz moves common initialization and attributes into an Item superclass. She recommends inheritance under specific conditions: keep the hierarchy shallow and narrow, put subclasses at the leaves of the object graph, and have every subclass use the superclass behavior. These are her conditions for this design choice, not a claim that inheritance is always appropriate.

She wants the superclass itself to play the item role, including tick. An implementation that raises an error for subclasses is one option; here she chooses a default tick that does nothing. Sulfuras overrides tick with the very same no-op, so it can use Item directly and its separate subclass disappears.

View full image

Separate configuration from the selection algorithm

Source 32:12

The remaining case mixes a name-to-class mapping with the algorithm that uses it. Metz extracts a hash, a lookup table, and leaves the factory to look up a specialized class or use a default. The mapping can now change independently of the selection rule. She suggests that frequently changing configuration might eventually move to a file or database, depending on how it changes; the demonstration does not require either.

View full image

Compare the outcome without confusing the measures

Source 33:31

The final small-object version is somewhat longer, but related behavior is grouped more tightly. The apparent indentation includes classes inside a module, not just nested business logic. The slide compares it with the original conditional.

View full image

Metz reports a total Flog score of 33 for the final collection, a most-complex class score of 12, and an average class score of seven. She argues that a reader need not hold every class in mind at once and describes the result as a 75% reduction in complexity. These figures measure different units from the original single class; the percentage is her interpretation of the metric, not a controlled measurement of maintenance effort. She recommends using fallible metrics alongside fallible human judgment.

Conjured is the last step, not the first

Source 35:19

Finally, Metz adds a Conjured subclass and adds its name-to-class entry to the mapping. In the displayed implementation, a tick decreases days, returns if quality is already zero, decreases quality by two, and applies another decrease of two at the displayed remaining-days boundary. She reports that the tests pass. As with Normal, the displayed early return alone does not establish a lower bound for every possible input.

View full image

The mapping change matters: the demonstration approaches open/closed design by separating selection from behavior, but adding this type still edits configuration. Metz closes with Kent Beck's advice to make the change easy first, even if that preparation is hard, and then make the easy change. A real new requirement supplies evidence for today's design; it need not be a prediction of every future requirement.