Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

22KB

FOCUS Architecture — Chapter-04: Simplicity Is a Decision: KISS and YAGNI

  • Source: /library/FOCUS Architecture/source-file.pdf
  • PDF pages: 57–80
  • Pages without text: none

Simplicity Is a Decision: KISS and YAGNI In this chapter, you’ll: list Fowler’s four costs of speculative functionality from memory and point to where each one shows up in real code; tell the origin of KISS and YAGNI with a name, a place, and a year; apply the “do I need this now?” test to a pull request and separate what YAGNI cuts from what YAGNI never cuts. You open a file to add a price field and find a pricing engine with support for three currencies, five tax regions, and scheduled promotions, all written by someone who swore they were helping. Nobody asked for any of it, and you’re still going to pay for every line. This chapter hands you the cheapest tool in the trade: the test for deciding what not to build. The first three chapters made the diagnosis. Chapter 2 measured the cost of change: the price of touching one line is set by the tangle it crosses, not by the size of the edit. Chapter 3 showed AI multiplying the speed at which that tangle grows and laid out the guardrails that limit the damage. Part II starts here, and it starts with the scissors. Before you learn to structure what you build, you learn to refuse what doesn’t need to exist, because the easiest line to maintain is still the one nobody wrote.

The engine nobody asked for Rosie asked for one thing: the menu in the app needs to show the price of each item. Cappuccino at $11.95, cheese bread at $6.00, house coffee at $8.00. She changes those numbers by hand, two or three times a year, whenever milk gets more expensive. Rosie’s Coffee Shop takes one currency, runs out of one address, and schedules exactly zero promotions. The teammate who picked up the ticket handed back the file below. Read it slowly; the whole chapter’s pain lives inside it. Dart // multi-currency support (the coffee shop has one) enum Currency { usd, eur, brl } const exchangeRates = <Currency, double>{ Currency.usd: 1.0, Currency.eur: 1.09, Currency.brl: 0.19, }; // tax by region (the coffee shop has one address)

enum Region { southeast, south, northeast, north, midwest } class TaxRule { const TaxRule(this.region, this.rate); final Region region; final double rate; } const taxRules = [ TaxRule(Region.southeast, 0.12), TaxRule(Region.south, 0.11), TaxRule(Region.northeast, 0.09), TaxRule(Region.north, 0.08), TaxRule(Region.midwest, 0.10), ];

// scheduled promotions (Rosie changes prices by hand, whenever she wants) class ScheduledPromotion { const ScheduledPromotion({ required this.item, required this.discount, required this.start, required this.end, }); final String item; final double discount; final DateTime start; final DateTime end; bool activeAt(DateTime instant) => !instant.isBefore(start) && !instant.isAfter(end);

} // base price per item, in dollars, before any adjustment class BasePrice { const BasePrice(this.item, this.priceInDollars); final String item; final double priceInDollars; } class PricingEngine { PricingEngine({ required this.basePrices, required this.defaultCurrency, required this.region, this.promotions = const [],

}); final List basePrices; final Currency defaultCurrency; final Region region; final List promotions; // looks up the item's base price double _basePriceOf(String item) { for (final price in basePrices) { if (price.item == item) { return price.priceInDollars; } } throw ArgumentError(“Item not on the menu: $item”);

} // applies the most aggressive scheduled promotion active at the instant double _withPromotion(String item, double value, DateTime instant) { var bestDiscount = 0.0; for (final promotion in promotions) { if (promotion.item == item && promotion.activeAt(instant) && promotion.discount > bestDiscount) { bestDiscount = promotion.discount; } } return value * (1 - bestDiscount); }

// adds the tax for the configured region double _withTax(double value) { for (final rule in taxRules) { if (rule.region == region) { return value * (1 + rule.rate); } } return value; } // converts from the base currency (dollar) to the requested currency double _inCurrency(double valueInDollars, Currency currency) => valueInDollars / exchangeRates[currency]!; // the method the menu calls to display a price

double priceOf(String item, {Currency? currency, DateTime? instant}) { final now = instant ?? DateTime.now(); final base = _basePriceOf(item); final promotional = _withPromotion(item, base, now); final taxed = _withTax(promotional); return _inCurrency(taxed, now == instant ? currency! : defaultCurrency); } } That’s 95 lines to answer “how much is the cappuccino?” The code works, it compiles without a single warning, and every block carries a polite comment explaining its own sub-goal. That’s exactly why it’s dangerous: nothing in it looks wrong. The right question isn’t “is this well written?” It’s “who asked for it?” Nobody asked for currency conversion. Nobody asked for a regional tax rate. Nobody asked for a promotion calendar. Each of those three axes is speculative functionality: code written for a need nobody has today, a bet on a future imagined by whoever wrote it. Ask the author and the defense comes pre-loaded: “what if Rosie opens a location in São Paulo? What if the next location is in a different region instead? What if she wants to run a winter

promotion?” None of those questions is absurd, and that’s exactly what makes speculation so seductive. Real coworkers write code like this, with good intentions and a future in mind. The problem isn’t how plausible the guess is. The problem is the price, and the next section measures that price in four installments. Meanwhile, the task Rosie actually asked for, the price on the menu screen, got pushed two days further away: that’s how long the engine took to build. The four costs I’ve written this engine before. In my case it was a plugin system: a dynamic loader, an extension registry, contract versioning, all “for the future,” because the product would supposedly become a platform someday. The future arrived and asked for none of it. Years later I deleted the whole system myself, and no plugin beyond my own two examples had ever existed; the only lesson left standing is the subject of this chapter. It wasn’t an execution mistake; the code was good. It was a decision mistake. Martin Fowler breaks that mistake into four costs, in the Yagni entry (You Ain’t Gonna Need It) of his bliki, the blog-wiki hybrid he’s kept since 2003, where each entry gets revised in place instead of turning into a new post (martinfowler.com/bliki/Yagni.html). Let’s measure each cost against the PricingEngine you just read. The first is the cost of build: the hours spent analyzing, coding, and testing a feature nobody uses. On the pricing engine, that was two days of work for 95 lines, of which the menu exercises half a dozen. Everything else is effort paid for a hypothesis.

The second is the cost of carry: the tax that speculative functionality charges everyone who reads, edits, or debugs the code from then on, even without ever using it. It’s the easiest cost to underestimate, because it never shows up on any invoice. Whoever opens the file to fix a price has to understand Currency , Region , TaxRule , and ScheduledPromotion before finding the line that matters. Add to that the extra surface for defects. Look at the end of priceOf : the expression now == instant ? currency! : defaultCurrency looks like the finesse of someone who handled every case, and it actually blows up at runtime if someone passes instant without passing currency . The bug lives in a parameter no real call ever uses. No multi-currency, no bug. The third is the cost of delay: the value the requested feature failed to generate while the speculative one was being built. The price on the screen was worth money on Wednesday; it shipped on Friday. Fowler insists this is the decisive cost, because it delays exactly the thing somebody is waiting to use. The fourth is the cost of repair: when the future finally shows up, it almost never has the shape the guess predicted, and the structure built ahead of time has to be twisted to fit. If Rosie opens that next location in a different region, her real tax bill won’t be a single rate tied to a spot on the map; it’ll be a mix of state, county, city, and product category that this five-line table can’t represent. The engine didn’t get any work done early. It created the work of tearing itself back down. Keep the order in mind: build, carry, delay, repair. The reference table at the end of the chapter lists all four again, each one pointing back to the line of PricingEngine where you saw it. Where the acronyms came from

KISS is the older of the two acronyms. “Keep It Simple, Stupid” is credited to Kelly Johnson, chief engineer at Lockheed’s Skunk Works in the 1960s, and the original context explains the meaning better than any definition could: Johnson’s airplanes had to be repairable by an average mechanic, in the field, with the tools that mechanic already had. Simple there wasn’t an aesthetic compliment. It was an operating requirement: a design that needs a genius to maintain has failed, no matter how gracefully it flies. The maxim carried that same sense into software, and that’s the sense this book uses. YAGNI was born inside a project you can date exactly: Chrysler’s C3, the payroll system that served as the cradle of Extreme Programming in the late 1990s. Whenever someone argued for a hypothetical capability (“we’re going to need this when…”), Kent Beck gave back the same answer: “you aren’t gonna need it.” The answer turned into an acronym on the team, and the acronym turned into a published practice in Extreme Programming Installed (Ron Jeffries, Ann Anderson, and Chet Hendrickson, 2001). Jeffries’s own wording is the working definition this chapter applies: “Always implement things when you actually need them, never when you just foresee that you need them.” The word carrying the whole sentence is foresee. YAGNI doesn’t forbid building; it forbids building on a forecast. The only legitimate trigger is a present need, with the name of whoever asked for it attached. The six lines the menu asks for

Apply Jeffries’s sentence to the engine: what’s left once you cut everything that exists on a forecast? This is what’s left. Dart const pricesInCents = <String, int>{ “cappuccino”: 1195, “cheese bread”: 600, “house coffee”: 800, }; int priceOf(String item) => pricesInCents[item]!; Six lines of code. Every cut has a name. Cutting multi-currency erased Currency , exchangeRates , _inCurrency , and the optional- parameter bug from the previous section. Cutting the regional tax erased Region , TaxRule , the rate table, and _withTax . Cutting the promotion calendar erased ScheduledPromotion , _withPromotion , and the dependency on DateTime.now() , which had turned the price into a function of the clock. And one cut came free: the price became an integer in cents, because double only existed to accommodate exchange rates and tax rates, and floating-point money is a pain you don’t need to buy today.

Try it: open https://focus.kodel.com.br/en/dart/04-01 and run it. Then try adding back just the dollar-to-euro conversion, without touching tax or promotions. Count how many lines came back and how many of them today’s menu actually calls. That answer is the cost of carry, measured by you. The same solution in Go earns a cultural aside worth the detour. Go var pricesInCents = map[string]int{ “cappuccino”: 1195, “cheese bread”: 600, “house coffee”: 800, } func priceOf(item string) int { return pricesInCents[item] }

Try it: the Go version runs at https://focus.kodel.com.br/en/go/04-01. The code is almost the same, and the almost is the point. Go is the language that turned refusing features into a design philosophy: released in 2009, it spent 13 years saying no to generics, until Go 1.18 arrived in 2022 with a minimal design, only after real use cases had piled up. Rob Pike gave a whole talk about that stance, “Simplicity is Complicated” (dotGo, 2015): every refused feature is a deliberate decision, and the language’s simplicity is the accumulated result of those refusals. You don’t need to adopt Go to take the lesson. You only need to notice that the same test that shrank PricingEngine works at the scale of a programming language: the question is never “would this be useful?” because almost anything would be. The question is “does anyone need this now?” That test deserves to leave the prose and become a flowchart, because it has three exits, not two:

You already know the bottom two exits from this chapter. The top exit is the safety catch of the next section, and it’s the one that separates someone who understood YAGNI from someone

who just memorized the acronym. What YAGNI doesn’t cut Every sharp argument cuts both ways, and YAGNI has been used to justify every untested hack in existence. Fowler closes that door in the same entry that defines the four costs, with a distinction this book treats as law. YAGNI applies to presumptive capability: system-visible functionality nobody has asked for yet, like multi-currency, regional tax, and the promotion calendar. YAGNI doesn’t apply to internal quality: effort that adds no functionality at all but keeps the software easy to change, like tests, refactoring, and clean design. The test for the price calculation isn’t a bet on an imagined future; it’s what guarantees the present. Cutting it in YAGNI’s name is quoting Fowler to disobey Fowler. The ruler that tells the two cases apart fits into one question: if the future never arrives, does this turn into waste? Multi- currency will happen someday: without that São Paulo location, every line of it is dead weight. The price test will never need it: it pays for itself the first time a menu price changes, new location or not. The same logic undoes the “KISS equals simplistic” reading. The six-line menu isn’t the lazy version of the engine; it’s the version without the complexity that never paid off. Simplistic is code that cuts what pays off, the test, the precise name, the boundary, just to look short. Johnson wasn’t asking for crude airplanes; he was asking for airplanes the field mechanic could actually fix. And when the need finally arrives?

The classic critique of YAGNI deserves its own section: “refusing today creates rework tomorrow, once the need arrives; building it alongside everything else would have been cheaper.” The hidden premise is that changing the system costs a lot, and that’s where this book’s answer leans on chapter 2, which measured the cost of change as a function of the tangle, not the size of the edit. If adding multi-currency a year from now requires rewriting half the system, the critique holds, and the real problem is that cost of change, not the refusal. In a system where each feature lives in its own independent slice, the cost of adding multi-currency once the São Paulo location actually happens is close to what it would cost today, with one advantage a guess never has: the real shape of the requirement, in hand. Chapter 11 builds those slices, and that’s where this answer settles the bill. Notice how the pieces fit together, because that fit is the central argument of Part II. YAGNI without cheap-to-change architecture really is a risky bet. Cheap-to-change architecture without YAGNI drowns in speculative code. The two practices pay for each other: you refuse the guess because you know the late addition is cheap, and the late addition is cheap because the system isn’t buried under guesses. Pitfalls “YAGNI, so I’m not writing a test.” This is the previous section turned upside down, and it’s the most expensive pitfall in this chapter. Tests and refactoring are internal quality; Fowler’s rule explicitly excludes them from the cut. The way out is mechanical: before you invoke YAGNI, run the diagram’s flow. If what you want to cut is a test, a refactor, or clean design, YAGNI doesn’t even have an opinion.

“I’m never abstracting anything again.” YAGNI refuses presumptive capability, not structure. The function priceOf is an abstraction, tiny and justified by today’s use. Once three screens need the price, extracting a module answers a present need, and YAGNI approves. What it blocks is the plugin engine before the second plugin exists. “The client asked for it, but they won’t actually need it.” YAGNI looks inward, at the capabilities the team presumes it needs; it isn’t a veto over what the user requests. If Rosie asks for scheduled promotions on Thursday, promotions stop being speculation on Thursday. Refusing a real request while quoting the acronym is just stubbornness wearing a principle’s name. Q&A What if I’m almost certain I’ll need it? “Almost certain” is exactly the foresee in Jeffries’s sentence, and the answer is still no. Write the guess down in the backlog in one line; if it comes true, you build it with the real requirement in hand, without paying the repair cost. What you lose by waiting is almost always smaller than the four costs added up from building it now. Doesn’t YAGNI conflict with designing architecture? No, thanks to the distinction from the previous section: architecture that makes change cheap is internal quality, the investment that makes refusal safe. The conflict is with speculative architecture, the plugin engine before the first plugin. Independent slices cost about the same whether you have one feature or twenty; the engine that carried multi- currency support cost 95 lines before it ever ran a single conversion, and the menu needed six.

Doesn’t deleting a finished PricingEngine waste what’s already been paid for? The cost of build is already spent; deleting the code doesn’t refund it. What deletion refunds is the cost of carry on every future reading of the file, and that’s the only cost still open. Code you already paid for isn’t a reason to keep code that’s expensive to keep; economists have a name for the opposite instinct: the sunk cost fallacy. Quick tip Speculation leaves a trace in the history: a file that was born big and never changed again. Run git log --oneline -- path/to/file | wc -l on the suspects; a result of 1 means nobody has needed to touch that file since it was born, and it’s worth asking whether anyone ever needed it at all. Quick reference Cost What it charges Where it showed up in PricingEngine Build Hours of analysis, code, and testing nobody uses 2 days for 95 lines Carry Reading, debugging, and defects for everyone currency! in priceOf Delay Value of the requested feature Requested Wednesday,

stuck in the queue shipped Friday Repair The guess gets the shape wrong; redo it later Regional tax rate vs. flat rate Situation Fix A feature proposal shows up Run the diagram: request, guess, or quality “What if I need it someday?” One-line backlog entry; build it when it arrives Cutting a test or a refactor Refuse: internal quality, YAGNI has no opinion Fear of future rework Make change cheap (chapter 11), don’t guess ahead Exercises

  1. Five proposals came in for the menu code, and each hunk below starts from the original file, independent of the others. For each one, decide: real request, speculation, or internal quality? What do you approve, and what do you send back with “you aren’t gonna need it”? The answer key comes right after; resist looking at it. --- a/menu/menu_price.dart +++ b/menu/menu_price.dart

@@ hunk 1: Rosie added a tea to the winter menu @@ const pricesInCents = <String, int>{ “cappuccino”: 1195, “cheese bread”: 600, “house coffee”: 800,

  • “hibiscus tea”: 700, }; @@ hunk 2: lay the groundwork for foreign currencies @@ -int priceOf(String item) => pricesInCents[item]!; +int priceOf(String item, {String currency = “USD”}) =>
  • pricesInCents[item]!; @@ hunk 3: cover the price calculation with a test @@ +void main() {
  • assert(priceOf(“cappuccino”) == 1195);
  • print(“price calculation ok”);

+} @@ hunk 4: extension point for future tax rules @@ +int withFutureTaxes(int valueInCents) => valueInCents; @@ hunk 5: a name that states the unit of the return value @@ -int priceOf(String item) => pricesInCents[item]!; +int priceInCentsOf(String item) => pricesInCents[item]!; Answer key, hunk by hunk. Hunk 1 is a real request: Rosie added the item, approve it. Hunk 2 is classic speculation, a parameter no call uses that exists purely on a forecast; send it back, and notice how it echoes the PricingEngine bug. Hunk 3 is internal quality: a test for what exists today, approve it without ever invoking YAGNI. Hunk 4 is speculation in its purest form, a function that returns its own argument while waiting for a future; send it back. Hunk 5 is internal quality: renaming the function to tell the truth about cents improves every future reading without adding any capability at all, approve it. 2. Could you run this chapter’s autopsy on code of your own? Pick a repository you maintain, find the file that looks the most like PricingEngine , the one born ready for a future that still hasn’t shown up, and measure the four costs on it: build hours you remember, concepts a new reader has to cross, what sat in the queue at the time, and how much of the guess still matches today’s actual need.

Tip 4 A feature nobody asked for is debt everybody pays. Next chapter: this chapter’s scissors meet their first hard case, and it looks harmless: when the same code shows up twice, is deleting it always the answer?

Powered by TurnKey Linux.