The Day One Line Change Broke Three Screens In this chapter, you’ll: define coupling and cohesion in your own words; spot, inside forty-odd lines, the three reasons for change tangled together in them; predict which parts of a system break when a business rule changes. Rosie is about to ask for the smallest change in the world: 15% off on rainy days. You’re going to make the right change in the wrong place, and three screens you never opened are going to break. This chapter exists so you can name what broke and, next time, predict the break before you touch the code. At the end of chapter 1 I promised an accident. Here it is. A drizzly Thursday, business is slow, and Rosie looks out the window of her coffee shop with the expression of someone who just had an idea that’s going to cost somebody money. “When it rains, nobody comes in. Put a 15% discount on rainy days in the app, should be quick.” She’s right that it’s quick: the rule fits on one line. The problem isn’t the line. The problem is where lines just like it ended up.
The screen that started out reasonable Rosie’s Coffee Shop app has a menu screen. It wasn’t born tangled: it grew out of three reasonable pull requests, the requests to merge a set of changes into the main codebase, each reviewed before it landed. In the first, the screen just listed items and prices. In the second, the loyalty program arrived, and the fastest way to give 10% off to anyone with ten past orders was to calculate it right there, where the price gets displayed. In the third, the team needed to log every order a customer tapped, and the fastest way was to write straight to the database, in the same file. Each step was defensible. Here’s the result: Dart import “package:flutter/material.dart”; import “database.dart”; class MenuScreen extends StatefulWidget { const MenuScreen({super.key}); @override State createState() => _MenuScreenState();
} class _MenuScreenState extends State { final database = Database(); final customerOrderCount = 12; final items = const [ (“House coffee”, 8.0), (“Cappuccino”, 11.95), (“Cheese bread”, 6.0), ]; // calculate discount double priceWithDiscount(double price, int customerOrderCount) { var discount = 0.0;
if (customerOrderCount >= 10) { discount = 0.10; } return price * (1 - discount); } @override Widget build(BuildContext context) { return ListView( children: [ for (final (name, price) in items) ListTile( title: Text(name), // format subtitle: Text(
“$${priceWithDiscount(price, customerOrderCount).toStringAsFix ed(2)}", ), onTap: () { final value = priceWithDiscount(price, customerOrderCount); // write database.insert(“orders”, {“item”: name, “value”: value}); }, ), ], ); } } Read the screen through its three comments, because they mark three different jobs disguised as one. // calculate discount is business logic: Rosie’s loyalty policy, ten orders or more earn 10%, written as a screen method. // format is presentation: the price becomes text with a “$” in front and two decimal places, the
way the designer asked for it. // write is persistence: tapping the item becomes a row in the orders table, with the value already calculated. If Python is the only language you know, don’t get stuck on the Flutter syntax; keep the three labels in mind, because you’re about to meet the exact same three jobs again in an eighteen-line Flask route. Forty-four lines, none of them dumb. And yet this screen already costs money, and to measure that you need the metric that Tip 1 in chapter 1 announced without defining. Cost of change is the total effort needed to make a new decision hold true across the whole system: how many files you need to touch, how many spots you need to check, and how many breaks you need to fix before the system tells one consistent story. Good architecture is the kind that keeps this number small for the changes the business actually asks for. I don’t know of a more direct measure than that. Layers, patterns and diagrams are means; the bill that arrives at the end of the month is the cost of change. Let’s pay that bill now, with Rosie’s request. The new rule fits on one line inside priceWithDiscount . Except I already made this change in this code, counted the spots it touched, and the exact number is four screen files: menu_screen.dart : the screen you just read, where each item’s price shows up; tab_screen.dart : the screen that totals a table’s consumption, with the discount applied item by item; payment_screen.dart : the screen that closes the tab and charges the customer the final amount; report_screen.dart : Rosie’s monthly report, which sums revenue with the discounts already deducted.
Each of these four files redoes the loyalty calculation on its own, and you’ll see the other three copies a few pages from now, with the differences each copy picked up along the way. Worse: after you edit the menu, nothing warns you about the other three. The app compiles. The screen’s tests pass. The tab, the payment and the report simply keep charging the old price, each in its own way, until someone notices the mismatch at the register. Try it: open https://focus.kodel.com.br/en/dart/02-01 and add the rainy-day discount yourself, right inside priceWithDiscount , on the menu tab. The whole app, all four screens, runs in the browser; if you prefer TypeScript, the same coffee shop is at https://focus.kodel.com.br/en/ts/02-
if (customerOrderCount >= 10) { discount = 0.10; } return price * (1 - discount); } In TypeScript, only the signature changes: function priceWithDiscount(price: number, customerOrderCount: number): number . The body is identical, token for token, and that’s why the two symbols share a single listing: copying this calculation is cheap in any language, and cheap is the danger. And here are the three copies, one per screen. Notice that none of them matches the source, and none of them matches each other: On the tab screen: Dart double tabTotal(List prices, int orderCount) { var total = 0.0; for (final price in prices) {
total += price - price * (orderCount >= 10 ? 0.1 : 0); } return total; } On the payment screen: Dart double amountDue(double total, int customerOrderCount) { var factor = 1.0; if (customerOrderCount >= 10) { factor = 0.9; } return (total * factor * 100).roundToDouble() / 100; }
On the report: Dart double revenueWithDiscount(double gross, int monthlyOrderCount) { final disc = monthlyOrderCount < 10 ? 0.0 : 0.10; return gross - gross * disc; } The tab screen turned the if into a ternary and changed the order of the math. The payment screen flipped the logic into a multiplying factor and rounded to cents, something no one else does. The report negated the condition and renamed the parameter to monthlyOrderCount , which doesn’t even describe the same thing anymore. A copy never sits still: each screen pulled the calculation an inch toward its own side, and today the four versions agree by luck, not by design. That’s why the break is silent. There isn’t one place where the loyalty rule lives; there are four places where it got pasted.
The diagram is the map of the accident: four screens hanging off the same rule, and the rule with no fixed address. You felt the pain. Now let’s name it, because pain with a name is a diagnosis. Coupling Coupling (from the Latin copulare, to join) is the degree to which one part of a system needs to change when another part changes. The word describes a chain, not a defect: coupled means it moves together. So far, no crime. In the coffee shop’s code, the four screens are coupled to the loyalty rule, and the diagram shows the whole chain: pull the node in the middle and the four nodes above it move. Except the chain is invisible to the compiler, because the link isn’t a function call, it’s a resemblance between pasted text. Change priceWithDiscount and the compiler doesn’t pull tabTotal along with it; the customer who got overcharged does. Robert C. Martin, in Design Principles and Design Patterns (2000), named the two symptoms you just saw. Rigidity: a simple change forces a cascade of changes in modules that depend on it; the rainy-day discount was one line and became four files. Fragility: a change breaks places with no apparent conceptual
relationship to it; whoever edits the menu has no reason to suspect the monthly report. Martin diagnosed this in enterprise systems twenty-six years ago. His code was different. The chain was this one. I don’t trust the eye to spot coupling, not even mine. After thirty years at this, my heuristic is still mechanical: pick a change the business would genuinely ask for, and count, in the code, how many files it touches. A number is a fact. “This screen is badly coupled” is an opinion people argue about in meetings; “this one-line change touches four files” ends the argument. Cohesion, the other side of the coin If coupling measures what changes together across parts, cohesion measures how much the things inside one part belong to each other. A cohesive screen contains only what shares the same fate; a low-cohesion screen is a house of tenants who don’t know each other. The menu screen is the second case, and its three comments are the proof: loyalty policy, price formatting and database writes share one file without sharing a single reason to live there together. That pair moves like a seesaw. When a screen’s tenants don’t belong to each other, some other part of the system needs them; the discount rule trapped inside the menu forced the tab screen to copy it, and every copy is a new link in the coupling chain. Low cohesion here manufactures coupling there. It isn’t a coincidence, it’s mechanics. And it isn’t a Flutter disease. The same screen, written as a Flask route by someone who came from the world of scripts, has the same three tenants in eighteen lines: Python
import sqlite3 from flask import Flask app = Flask(name) @app.route("/menu/int:customer_order_count”) def menu(customer_order_count): price = 11.95 # calculate discount discount = 0.10 if customer_order_count >= 10 else 0.0 value = price * (1 - discount) # format text = f"${value:.2f}”
con = sqlite3.connect("orders.db")
con.execute("CREATE TABLE IF NOT EXISTS orders (item TEXT, value REAL)")
con.execute("INSERT INTO orders VALUES (?, ?)", ("Cappuccino", value))
con.commit()
con.close()
return text
Eighteen lines, the same three labels, the same future. The day the second endpoint needs the loyalty discount, someone is going to copy the if from this route, and the chain in the diagram starts growing in Python too. The language changes, the framework changes; the seesaw between cohesion and coupling doesn’t. The axis of change There’s one question missing that organizes all of this: who asks for each change? A piece of code’s axis of change is the actor that triggers its edits, the person or role the requests come from. A healthy piece of code has a single reason to change because it answers to a single actor. The idea of reading code through the lens of who asks for it appears in Jimmy Bogard, “Vertical Slice
Architecture” (2018); here it only enters as a diagnostic lens, what to do with that lens is left for Part II. Point the lens at the menu screen and its three tenants get a face: Snippet (label) Who asks for the change Example request calculate discount Rosie, the business owner “15% off on rainy days” format The app’s designer “show cents even when they’re zero” write The DBA / data owner “new column on the table” The third actor is the DBA (Database Administrator), the person who owns the shape of the tables. Three actors, three agendas, three different change calendars, all holding a key to the same file. When Rosie asks for the rainy-day discount, the risk doesn’t stay confined to her snippet: the change happens inches away from the formatting and the writing, inside the same State , and any slip splashes onto code that belongs to another actor. Now turn the lens on the three copies and the diagnosis closes: the loyalty rule has a single axis (Rosie), but its code is scattered across four files owned by other people. One actor, four addresses. That’s the full anatomy of the drizzle accident, and it’s everything this chapter promises: the exact name of the problem. The fix has a whole part of the book reserved for it. Pitfalls “I’ll just get rid of coupling.” Zero coupling doesn’t exist; the goal is to couple along the axis of change. A system whose parts don’t depend on anything doesn’t do anything. The tab screen
needs the discount calculation; the defect was never the dependency, it was copying as a way of depending. “I already know the fix, just extract the calculation.” If you’re a senior developer, your hand has been itching since the first copy. Hold it back. Extracting now, without the criteria from Part II, tends to just relocate the coupling: the function lands in a utils.dart that three features fight over tomorrow, and the chain in the diagram is still there, with a different name on the middle node. The diagnosis came first in this book precisely because rushing to fix it is the most expensive trap. “Nobody writes code like this.” Reread the origin story: three reasonable pull requests, approved one at a time. Nobody decides to write the tangled screen; it’s the natural state of any screen that takes reasonable shortcuts for six months straight. If your own repository doesn’t have one, look harder. Q&A Isn’t high cohesion just the same as a small class? No. Size is a symptom, not a criterion. An eight-line class that mixes business logic and formatting is less cohesive than a sixty- line one where everything serves the same actor. Measure belonging (do these snippets change for the same reason?), never line count. Wouldn’t a stricter code review have caught the copies? It would help in the individual case and fail the pattern. Human reviewers get tired, lose context, and approve the fourth copy at 6pm on a Friday. A structure that makes copying unnecessary beats discipline that tries to forbid it; which structure that is, Part II answers.
Doesn’t my framework already solve this? No framework decides where your business rules live; that decision is yours in Flutter, in React and in Flask, and the Flutter screen and the Flask route in this chapter show the same tangle in two unrelated ecosystems. The framework changes the frame. The picture is still yours. Quick tip Before you change a rule, search the whole repository for its constant. Search for the variants, not just one spelling: the four copies in this chapter write the same 10% as 0.10 , 0.1 and 0.9 , and a single git grep -n “0.10” only finds one of them. Prefer git grep -nE “0.10?|0.9” (or whatever magic number your rule uses, in every form it can take). Every hit is a potential spot you need to touch, and the ready-made list becomes your change checklist. Thirty seconds of grep save you an afternoon spent hunting the copy you forgot, after the register comes up short. Quick reference Situation Fix Measuring whether the architecture is good Count the files a real change touches One line turns into several files Rigidity: map the chain before you edit Changed here, broke over there Fragility: hunt down the diverging copies
A file mixes jobs that don’t belong together Low cohesion: label each snippet Not sure who owns a snippet The actor who requests changes is its axis Exercises
Powered by TurnKey Linux.