選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

28KB

FOCUS Architecture — Chapter-06: SOLID Without Dogma

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

SOLID Without Dogma In this chapter, you’ll: name, in front of a change that hurt, which principle the pain violates, using one test question per principle; slice Rosie’s Tab into per-actor responsibilities, without falling into the tiny-class factory; justify a decision to NOT apply a principle, and say out loud when applying it would be pure ceremony. Rosie’s Tab calculates the total, applies the loyalty discount, formats the receipt, and writes everything to the database: one class serving four different bosses. In chapter 2 you learned to measure the cost of a piece of code by the reach of the changes it drags along. This chapter turns that measure into a five- question filter that, in front of a change that hurt, tells you which coupling force you stepped on. The filter is called SOLID, and it arrives here without the commandment weight people usually hang on it. Recap of chapter 2 in one line: coupling is the reach of a change, cohesion is how much of a file changes together, and axis of change is the reason someone opens the code. That chapter diagnosed the disease. This one delivers the vocabulary for the diagnosis. SOLID (Single responsibility, Open-closed principle, Liskov substitution, Interface segregation, Dependency inversion) bundles five principles that Robert C. Martin compiled in Design Principles and Design Patterns (2000). The acronym

came later: around 2004, Michael Feathers noticed that the rearranged initials spelled “solid,” and the name stuck. The order of the letters is marketing, not hierarchy. Two of the principles are considerably older than the compilation, as you’ll see, and none of them was born as law. One tab, four bosses Before any principle, the code that motivates all of them. The Tab below is in production in Rosie’s Coffee Shop app, and it does everything the word “tab” suggests: Dart // Anti-solution: the Tab that serves four actors in a single file. class Tab { Tab(this.items, {required this.isTenthPurchase}); final List<({String name, int priceInCents})> items; final bool isTenthPurchase; // Calculates the total: finance's rule. int calculateTotal() {

var sum = 0; for (final item in items) { sum += item.priceInCents; } return applyLoyalty(sum); } // Applies loyalty: marketing's rule (went from 10% to 15%). int applyLoyalty(int sum) { if (!isTenthPurchase) { return sum; } return sum - (sum * 15) ~/ 100;

} // Formats the receipt: the printer's layout. String formatReceipt() { final lines = [ for (final item in items) “${item.name}: ${item.priceInCents}", “TOTAL: ${calculateTotal()}", ]; return lines.join("\n”); } // Persists the tab: the DBA's schema. Map<String, Object> toDatabaseRow() { return {“items”: items.length, “total_in_cents”: calculateTotal()}; }

} Read the subgoal comments and notice who’s in charge of each chunk. The total rule belongs to Rosie’s finance side. The loyalty rule belongs to marketing, which adjusts the percentage whenever it wants to drive traffic. The receipt layout belongs to the printer and to the accountant’s requirements. The persisted row’s schema belongs to whoever owns the database. Four groups of people, each with its own agenda, and each requests changes in the same file. Keep that count in mind. Now the scene. Marketing just bumped loyalty from 10% to 15%, a one-character edit in applyLoyalty , and that’s the version you just read. The following week, a customer completes her tenth purchase, pays for an $11.95 cappuccino with a $6.00 cheese bread, and asks for the receipt to get reimbursed by the company she works for. Her company’s accounting turns it down: the printed items add up to 1795 cents, the TOTAL says 1526, and no line on the paper explains where the 269 went. The receipt has been wrong since the 10% days; marketing’s change only widened the hole until someone noticed. The fix the accountant is asking for looks trivial: one “LOYALTY: -269” line before the TOTAL. Try implementing it inside this class. The discount doesn’t exist as a number anywhere: it gets swallowed inside applyLoyalty , which hands back the already- reduced sum to calculateTotal . To give the receipt the line the accountant demands, you touch marketing’s function and finance’s function, and toDatabaseRow changes behavior right along with them, because it also calls calculateTotal . One boss’s requirement forces you to edit two other people’s code and changes a third person’s output. That cascade has a name, and the name is the subject of the next section.

Try it: run the cascade at https://focus.kodel.com.br/en/dart/06-01 or https://focus.kodel.com.br/en/ts/06-01. Step 1 prints the monolithic Tab ’s receipt, with the items adding up to 1795 and the TOTAL saying 1526, no discount line. Step 2 prints the same order in the sliced version you’re about to build next, with the LOYALTY line closing out the math. Compare the two receipts before you go on. Slice by actor, not by verb The first principle in the acronym is the most quoted and the worst read of all five. SRP (Single Responsibility Principle) says, in Martin’s own formulation (2000): a class should have one, and only one, reason to change. Notice what the sentence doesn’t say. It doesn’t say “a class does one thing.” Doing is about code; reason to change is about people. In later writing Martin himself tied off the loose end: reason to change is synonymous with actor, the group of people who ask for that kind of change. Finance is one actor. Marketing is another. The SRP question was never “how many things does this class do?” It’s “how many bosses does this file have?” The distinction matters because the two readings slice the Tab in different places. “A class does one thing” slices by verb: sum, discount, format, save, each verb in its own class, with no stopping rule. One reason per class slices by actor, and the Tab has exactly four: Dart // Finance: how the sum becomes a total.

class TotalCalculator { TotalCalculator(this.policy); final LoyaltyPolicy policy; int calculate(List items, {required bool isTenthPurchase}) { var sum = 0; for (final item in items) { sum += item.priceInCents; } return sum - policy.discount(sum, isTenthPurchase: isTenthPurchase); } } // Marketing: how much discount loyalty gives.

class LoyaltyPolicy { int discount(int sum, {required bool isTenthPurchase}) { if (!isTenthPurchase) { return 0; } return (sum * 15) ~/ 100; } } // Receipt printer: the receipt layout, with the discount on its own // line. class ReceiptFormatter { String format( List items, { required int discountInCents,

required int totalInCents, }) { final lines = [ for (final item in items) “${item.name}: ${item.priceInCents}", if (discountInCents > 0) “LOYALTY: -$discountInCents”, “TOTAL: $totalInCents”, ]; return lines.join("\n”); } } // DBA: the persisted row's schema. class TabRepository { final _rows = <Map<String, Object»[];

void save(List items, int totalInCents) { _rows.add({ “items”: items.length, “total_in_cents”: totalInCents, }); } } It’s the same Tab from the previous section, business line by business line (sum, discount, receipt, database), now with one file per boss. The accountant’s requirement turned trivial. The discount is now a number with its own name, one that leaves LoyaltyPolicy and enters the formatter as a parameter; the LOYALTY line cost one if in the layout, without touching finance’s rule or marketing’s. When marketing tweaks the percentage again, the edit happens in a class whose only boss is marketing. The receipt still adds up, because whoever prints it receives the discount ready-made instead of having to guess it. The same slice in TypeScript shows where the translation hurts and where it doesn’t: TypeScript // Marketing: how much discount loyalty gives.

class LoyaltyPolicy { discount(sum: number, isTenthPurchase: boolean): number { if (!isTenthPurchase) { return 0; } return Math.floor((sum * 15) / 100); } } Two differences, and neither is about design. The first is division: Dart’s ~/ truncates on its own, and TypeScript needs Math.floor so it doesn’t hand back 269.25 cents. The second is the named parameter: in Dart, {required bool isTenthPurchase} forces the caller to write isTenthPurchase: true at the call site, and TypeScript has no such feature, so the boolean goes in by position and readability drops a notch. The actor is still just one, and that’s what the SRP measures. The rest of this chapter’s listings run in Dart, with the same correspondence holding. Slicing works, and it’s exactly because it works that it turns into a habit hard to break. Look at what happens when the knife keeps going after the actors run out: Dart

// The overdone slice: nine lines no actor asked for. class SubtotalCalculator { int calculate(List items) { var sum = 0; for (final item in items) { sum += item.priceInCents; } return sum; } } It looks professional. “Summing items is one responsibility, discounting is another,” says the colleague in the review, and the sum gets its own file. Ask the actor question before you approve it: who asks for a change to the subtotal? Finance. Who asks for a change to the total? Finance. Same boss, same reason, same class. The split doesn’t eliminate a single reason to change; it just spreads the same reason across two files that now need to change together. That’s new coupling dressed up as organization. The slice goes back inside TotalCalculator in the next commit, and chapter 4 already gave you the name for the rule that justifies

reverting it: YAGNI (You Aren’t Gonna Need It). Slicing without an actor asking for it is building presumed capacity, this time in the shape of a class. First question in the filter, then: how many actors ask for changes in this file? More than one, and the SRP is violated; the cascade pain is a matter of time. Exactly one, stop slicing, even if the class “does two things.” Read the code through the OCP and LSP lenses The next two principles are older than Martin’s compilation, and in this section you won’t write new code for them: you’ll reread the code you just sliced. OCP (Open-Closed Principle) comes from Bertrand Meyer, in Object-Oriented Software Construction (1988): software entities should be open for extension and closed for modification. LSP (Liskov Substitution Principle) comes from Barbara Liskov’s talk “Data Abstraction and Hierarchy” (1987): if one type substitutes another, the program can’t tell the difference. Reread TotalCalculator with these two lenses. It receives the loyalty policy ready-made instead of knowing the percentage itself. The day marketing invents a new policy, the calculator doesn’t get edited: it receives a different policy. Extension without modification, the OCP in one sentence. And the swap only works if every policy behaves the way the original one promised: if one of them returns a negative discount, or one larger than the sum, the total breaks and the caller notices. Substitutability, the LSP in one sentence. No new hierarchy was created to satisfy either principle. They don’t ask for structure; they ask that the existing structure respect two forces, the direction of who knows whom, and the confidence that the swap is safe.

If you still suspect these principles are language syntax tied to inheritance, Go takes that suspicion apart: Go // The calculator depends on an implicit interface: any type that has // Discount qualifies, without declaring that it implements anything. type DiscountPolicy interface { Discount(sumInCents int) int } type Loyalty struct{} func (Loyalty) Discount(sumInCents int) int { return sumInCents * 10 / 100 } type NoDiscount struct{}

func (NoDiscount) Discount(sumInCents int) int { return 0 } // Composition instead of inheritance: the calculator carries the // policy. Swapping the policy doesn't edit a single line here. type TotalCalculator struct { Policy DiscountPolicy } func (c TotalCalculator) Calculate(pricesInCents []int) int { sum := 0 for _, price := range pricesInCents { sum += price } return sum - c.Policy.Discount(sum)

} Go has no inheritance, and Loyalty never declares anywhere that it implements DiscountPolicy : it just needs the Discount method with the right signature, and the interface is satisfied implicitly. Even so, both forces are fully present in the code above. The dependency direction points from the calculator to the interface, never to a concrete policy, and that’s what keeps the calculator closed for modification when a new policy shows up. Substitutability is the behavior contract between Loyalty and NoDiscount : either one drops into the other’s place without Calculate noticing. The syntax changes from one language to the next; the forces OCP and LSP name stay the same. Anyone who concludes that “Go doesn’t need SOLID” is looking at the absence of extends , when they should be looking at the dependency arrow the code draws. Two more questions for the filter. OCP: does extending require editing what already works? LSP: can I swap the implementation without the caller noticing? Narrow the contract and flip the arrow What’s left is the pair FOCUS leans on at full strength, and it lives at the app’s most unstable boundary: persistence. ISP (Interface Segregation Principle) says no client should depend on methods it doesn’t use. DIP (Dependency Inversion Principle) says business rules shouldn’t depend on infrastructure detail; both should depend on an abstraction. And abstraction, in the DIP sense, means depending on the contract that declares the behavior, never on the implementation that fulfills it. The term doesn’t require the abstract keyword: a three-line interface is abstraction enough.

In practice the two principles arrive together, because whoever defines the contract is whoever consumes it. The CloseTab use case needs a single persistence operation, so it declares a contract that size: Dart // The narrow contract: only what the use case demands (ISP). abstract interface class TabRepository { void save(List items, int totalInCents); } // The use case depends on the abstraction, not the implementation // (DIP). class CloseTab { CloseTab(this.repository); final TabRepository repository; void execute(List items, int totalInCents) {

repository.save(items, totalInCents); } } // The implementation knows the contract; the reverse never happens. class SqlTabRepository implements TabRepository { final _rows = <Map<String, Object»[]; @override void save(List items, int totalInCents) { _rows.add({ “items”: items.length, “total_in_cents”: totalInCents, }); } }

The contract has one method because the use case uses one method. If the concrete repository offers twenty operations, the ISP tells the contract to ignore nineteen of them; a fat contract forces every consumer to know about methods it never asked for, and any change to them propagates to callers who never invoked them. The DIP lives in the direction of the arrows, and a diagram shows the inversion better than any prose. Before, the use case knows the implementation: After, both point at the contract: The implementation’s arrow flipped direction: instead of being known by the use case, it now knows the contract. That inversion is what gives the principle its name. Swapping the SQL database for an in-memory implementation in tests, or for a different database in production, becomes a decision the use case never finds out about. Who instantiates SqlTabRepository and hands it to CloseTab ’s constructor? Chapter 9 answers with the Composition Root: the single point in the program, usually startup, where concrete implementations get created and wired to whoever depends on them. And why is the repository the only place in the

app allowed to throw and catch infrastructure exceptions? Chapter 15 closes that boundary. This chapter plants the seed both of those chapters harvest. The filter’s last two questions. ISP: does everyone who depends on this contract use all of it? DIP: does the use case know the implementation? What each principle charges whoever is looking The filter’s five questions measure coupling. There’s a sixth lens, which replaces none of them and answers the question that opened the book: what does each principle charge whoever needs to find where a rule lives? SRP charges the least of all, and that’s why it came first. Slicing by actor turns “where’s the discount rule?” into “who asked for that rule?”, and the second question has an answer outside the code: it was Rosie, in the conversation about loyalty. OCP charges according to whether the extension is real or presumed. When it’s real, the new policy is born in a file with a name of its own, and whoever is looking opens that file; when it’s presumed, the answer is split between a factory, an interface with one implementer, and an extension point nobody used, and the search goes through all of them. LSP charges on the reading of implementations: a substitute that lies forces whoever is looking to check them one by one, because the contract stopped being a reliable summary of what happens. Where LSP holds, reading the contract is enough. ISP and DIP charge in the opposite direction, and they charge little. A narrow contract is a short list of the questions that consumer asks, and the method list becomes an index instead of an inventory. A flipped arrow is the guarantee that the rule can be

read without opening the database: whoever looks for the discount calculation finds the use case, and SqlTabRepository stays out of the way until the day the question is about writing. The cost of carrying what doesn’t matter The ISP argument has a second half, which in 2002 wasn’t urgent. A fat contract charges the compiler, which propagates changes to whoever didn’t ask for them, and it charges whoever reads: twenty methods on screen to find out which of the twenty answers today’s question. For a person that’s time. For a language model it’s a literal budget, because everything it considers at once is measured in tokens, the pieces text is cut into before it enters the count, and the budget is finite. Hence the name the architecture literature settled on: token efficiency, the share of what you read that is actually about the question you’re answering. A one-method contract about tab persistence scores high for whoever wants to know how the tab gets written, and the same holds for the developer who opened the file at eleven at night. It’s the same economy ISP always charged for, now with a unit of measure you can check. How many things you hold at once The previous section’s arithmetic is about volume. There’s another one, about simultaneity, and it has had a name since 1988: cognitive load, the number of things someone has to keep in mind at the same time to finish a task. John Sweller showed, studying how people learn, that this capacity is small and that badly organized material spends it before the person even reaches the problem.

Programming is the extreme case. To answer “why did the combo come out at 15%?”, someone has to hold the tab, the loyalty policy, the point where the two meet, and what they’ve already ruled out along the way. Every jump the architecture forces adds an item to that stack, and the stack overflows silently: the person doesn’t announce that they forgot, they conclude wrongly. It’s the same metric that has run through the book since the F12 test, and it’s why “how many jumps to the code that does something?” is a serious question and not nitpicking. The critique SOLID earned A principle announced as law accumulates enemies, and SOLID accumulated an entire article’s worth. In 2022, Dan North published the CUPID proposal (Composable, Unix philosophy, Predictable, Idiomatic, Domain-based), and along the way called the SRP a “pointlessly vague principle.” His central argument deserves attention: principles are binary rules, ones you either meet or violate, and North prefers properties: gradable qualities that code can have more or less of. That’s the distinction between principle and property running through the whole debate, a binary rule on one side, a continuous scale on the other. Robert Martin answered in “Solid Relevance” (2020, on his blog), where he argues the principles remain valid because the forces they name, coupling and dependency, haven’t aged. Both pieces are published and worth reading: North’s at dannorth.net/blog/cupid-for-joyful-coding, Martin’s at blog.cleancoder.com. You’ve already seen this book’s position in action throughout the chapter, and now it gets a name: the principles work as a coupling heuristic, and CUPID’s properties work as a success ruler. The filter’s five questions are SOLID in heuristic form: none of them says “violate this and get punished”; all of them say “if

the answer is this, the pain comes from here.” And the result of a good slicing gets measured with North’s ruler: the sliced Tab is more predictable, more idiomatic, and more oriented toward the coffee shop’s domain than the monolithic one. The two schools measure different things. Pitting one against the other wastes both. Here’s my scar from this debate. I inherited a project where the SRP had been read as “a class does one thing” and applied with zeal: more than sixty classes under ten lines each, every Calculator paired with a Validator , a Normalizer , and a Formatter , and not one business rule readable start to finish, because every rule crossed six files. It was this page’s SubtotalCalculator multiplied by sixty. None of those files had an actor; they had verbs. Undoing it cost weeks. Since then, when someone shows me a slicing, I don’t ask what each class does; I ask who asked for it. Pitfalls OCP read as “never edit existing code.” This is the chapter’s most expensive trap. That reading spawns speculative extension hierarchies: interfaces with one implementation, factories for one product, extension points nobody extends, all to avoid touching a file that has tests and would take minutes to edit safely. Chapter 4 already delivered the verdict on presumed capacity: YAGNI. Editing code covered by tests is cheap; maintaining an unused abstraction is expensive and permanent. The OCP pays off when the extension is real and recurring, like the loyalty policy marketing swaps every month, not as insurance against any future edit. SRP by verb. The nine-line class factory from the previous section. The symptom is slicing without an actor: if you can’t say WHO asks for a change in a freshly created class, it shouldn’t

exist. The test question defuses the trap before the commit. Contract as ceremony. After seeing the DIP work, the temptation is to create an interface for every class in the app, “for consistency.” An interface with a single consumer and a single implementation that never swaps is the overdone slice, contract edition. In repositories, FOCUS requires the abstraction, because infrastructure changes for its own reasons and tests need the swap; everywhere else in the app, wait for the second implementation to ask for a seat. Q&A Isn’t the SRP just chapter 2’s cohesion with a different name? It’s cohesion with an operational test. Cohesion says a file’s parts should change together; the SRP says how to test that: count the actors. The ruler is the same, but “how many bosses?” gets answered in a minute, while “is this cohesive?” turns into a meeting. Should I create an interface for every repository from day one? For repositories, yes, and the reason is concrete: database, network, and filesystem change for their own reasons, and your tests will need an in-memory implementation by the first week; the swap isn’t a hypothesis, it’s routine. Outside the infrastructure boundary, the third pitfall’s rule applies: with no second implementation in sight, the contract can wait. If my language doesn’t even have inheritance, does the LSP tell me anything? It tells you everything. The LSP talks about promises, and promises exist everywhere. Wherever there’s a contract and two implementations, there’s the question “does the swap surprise the caller?”, and you just watched Go answer it without a single extends .

Quick tip To count a file’s actors without guessing, ask the history: git log --format=”%s” -- path/to/file.dart lists the commit messages that touched the file. If they alternate between “adjust promo discount,” “change receipt layout,” and “migrate database column,” you’ve got three bosses in one file, and the next chapter of pain is already on the calendar. Tip 6 A good principle is one where you know when NOT to apply it. Quick reference Principle Test question Use in FOCUS SRP How many actors ask for changes in this file? Use cases per actor OCP Does extending require editing what already works? Lens; extend only when it hurts LSP Can I swap the implementation without the caller noticing? Honor the contract ISP Does everyone who Use case’s narrow

depends on this contract use all of it? contract DIP Does the use case know the implementation? Dependency injection Exercises

  1. The OrderRecorder below is in production at Rosie’s counter. It contains TWO violations of principles from this chapter. Name both, fix ONLY the one that causes concrete change pain, and write one sentence justifying why the other one stays as is. Dart // Stores the logged orders. class SqlOrderRepository { final _orders = []; void save(Order order) { _orders.add(order); }

} // Two violations live in this class. Which ones? class OrderRecorder { final _repository = SqlOrderRepository(); void record(Order order) { _repository.save(order); } String formatPromoCoupon(Order order) { return “Come back tomorrow, ${order.customer}: " “10% off your next order!"; } }

Commented answer: the first violation is SRP: recording the order belongs to the counter staff, and the coupon text belongs to marketing, two actors in the same class; marketing changes that text with every campaign, so the pain is concrete and the fix pays off: extract a CouponFormatter whose only boss is marketing. The second violation is DIP: the class instantiates SqlOrderRepository directly, with no contract in between. It stays: the recorder is the only caller, no second implementation exists or is planned, and no actor has asked for the swap. Fixing it now would be contract as ceremony; the DIP’s test question flags the violation, and chapter 4’s YAGNI says shelve it until it hurts. 2. Could you create a second policy for the SRP section’s calculator (a birthday discount, say) and swap it in for loyalty without editing TotalCalculator or ReceiptFormatter ? If either class needs an edit, one of the OCP and LSP lenses will flag exactly where the design leaked. Next chapter: you’ll write business rules as pure functions, and find out that once a rule depends only on its input, the SRP stops being discipline and becomes a consequence.

Powered by TurnKey Linux.