YAGNI: delete speculative options
Delete the settings nobody uses, so each new column is a two-line change.
Watch the narrated video
Every step
Problem
Steps 1–4Refactor
Steps 5–10Payoff
Step 11Swift bonus
Steps 12–13Recap
Steps 14–15The code: before and after
Before
/* Config.plist
<dict>
<key>export</key>
<dict>
<key>format</key><string>csv</string>
<key>delimiter</key><string>,</string>
<key>header</key><true/>
</dict>
</dict>
*/
struct ExportConfig {
let format: String
let delimiter: String
let header: Bool
}
struct OrderExport {
private let format: String
private let delimiter: String
private let header: Bool
init(_ cfg: ExportConfig) {
format = cfg.format
delimiter = cfg.delimiter
header = cfg.header
}
func export(_ orders: [Order]) -> String {
var out = ""
if header && format == "csv" {
out += "id\(delimiter)total\n"
}
for o in orders {
let row = switch format {
case "csv": "\(o.id)\(delimiter)\(o.total)"
case "json": "{\"id\":\"\(o.id)\",\"total\":\(o.total)}"
case "xml": "<order id=\"\(o.id)\" total=\"\(o.total)\"/>"
default: fatalError("unknown format \(format)")
}
out += row + "\n"
}
return out
}
}
struct NightlyJob {
let config: ExportConfig
let orders: OrderRepo
let warehouse: Warehouse
func run() {
let csv = OrderExport(config).export(orders.today())
warehouse.uploadCsv(csv)
}
}After
/* Config.plist
<dict>
</dict>
*/
struct OrderExport {
func export(_ orders: [Order]) -> String {
let rows = orders.map { o in
"\(o.id),\(o.total),\(o.postalCode)\n"
}
return "id,total,postalCode\n" + rows.joined()
}
}
struct NightlyJob {
let orders: OrderRepo
let warehouse: Warehouse
func run() {
let csv = OrderExport().export(orders.today())
warehouse.uploadCsv(csv)
}
}Transcript, step by step
Problem
Step 1: An export with three settings Before
Every night, this job exports today's orders and uploads them to the warehouse. Order Export copies three settings out of the config file at the top, a format, a delimiter and a header flag. It writes a header line, then one row per order in whichever format the config names. It can write CSV, JSON or XML, so it looks ready for whatever the business asks for next.
Step 2: One column, four edits Code smell
The warehouse now wants the postal code as a third column. So the header gets a new name, and every case of the format switch gets a new value. A speculative option is a setting built for a need nobody has yet, and this struct copies three of them. You can see what they cost here in the header, here in the CSV case, here in JSON, and here in XML.
Step 3: Two formats nobody reads Code smell
Yet the config file only ever says CSV, and the only caller uploads CSV to the warehouse. So the JSON and XML cases never run, and nobody would notice if one of them broke. One new column costs four edits, and two of them land in formats nothing reads. A missing quote in the XML line would still compile and sit there until someone finally turns XML on. And every option multiplies the paths a reader has to check before trusting a change.
Step 4: Back it out, then delete Plan
Let's back that change out. YAGNI, you aren't gonna need it, says you build a feature when someone needs it, and these two formats have nobody. Kent Beck put it as make the change easy, then make the easy change. So we delete the options nobody uses, one small step at a time, and the file the warehouse gets stays the same after each one.
Refactor
Step 5: Delete the XML case Remove Dead Code
No config value picks XML and no caller reads it, so the XML case goes first. The switch keeps CSV, JSON and the default, and the file the warehouse gets is the same as before.
Step 6: Delete the JSON case Remove Dead Code
JSON goes for the same reason, so the switch is down to the CSV case and a default. That default only stops the program for a format the config file never sets.
Step 7: Format can only be csv Remove Field
With one case left, format can only ever be CSV, so it stops being a property. The switch collapses into the CSV row, and the header test only checks the header flag. Format leaves the config file too, because nothing reads it anymore.
Step 8: The delimiter is always a comma Remove Field
The delimiter was a comma in the config and nowhere else, so we write the comma in place. The property goes, along with its line in Export Config and its key in the config file.
Step 9: The header is always on Remove Field
Header is always true, so the path that skips the header line has never run. We delete the if, and then the property, because nothing reads it anymore. Export Config is an empty struct now, and the initializer takes one and ignores it. The config file is down to an export dictionary with nothing in it.
Step 10: Nothing left to configure Remove Parameter
Order Export took a config and read nothing from it, so the parameter goes with its initializer, and Export Config goes too. With no stored properties, Swift's memberwise initializer takes no arguments, so the nightly job calls it with empty parentheses and drops its config property. And the config file at the top is empty.
Payoff
Step 11: The postal code, in two edits Payoff
Now the postal code goes back in. It takes one new name in the header and one new value in the row, so that's two edits. The JSON and XML cases that took two of the four edits are gone, and there's one row line where there were three cases. YAGNI fit here because the config always said CSV and the only caller uploads CSV, so those options cost an edit on every change and gave nothing back. The loop, the return and the nightly job didn't change.
Swift bonus
Step 12: A string you keep appending to Swift bonus
The method still carries some bookkeeping around its text. It starts a mutable string, appends to it on every pass through the loop, and then hands the variable back at the end. Turning every order into one line is what map is for, so the var and the loop can go.
Step 13: Map each order to a row map and joined
Map runs the trailing closure once per order and gives back an array of rows, each with its newline. Joined puts those rows together into one string, and the header goes in front of them. The warehouse gets the same bytes as before, and the next column is still two edits.
Recap
Step 14: Where we started, where we ended Before / after
On the left, three properties copied a format, a delimiter and a header flag out of the config. The switch on format had three cases, and only the CSV one ever ran. On the right, there's one short method, and the config file is empty. It already writes the postal code, and the whole method is about the size of the old switch.
Step 15: When to reach for it Takeaways
Delete an option when the config only ever sets one value and no caller reaches the other branches. Keep it when a second user is real and dated, like a signed contract with a JSON partner, or when ops set the value per environment, because deleting it means writing it again against a deadline. You'll spot it by config values that never change, switch cases no caller reaches, and defaults that nobody overrides.