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 11Ruby bonus
Steps 12–13Recap
Steps 14–15The code: before and after
Before
# config/settings.yml
# export:
# format: csv
# delimiter: ","
# header: true
ExportConfig = Data.define(:format, :delimiter, :header)
class OrderExport
def initialize(config)
@format = config.format
@delimiter = config.delimiter
@header = config.header
end
def export(orders)
lines = []
if @header && @format == "csv"
lines << "id#{@delimiter}total"
end
orders.each do |o|
case @format
when "csv"
lines << "#{o.id}#{@delimiter}#{o.total}"
when "json"
lines << %({"id":"#{o.id}","total":#{o.total}})
when "xml"
lines << %(<order id="#{o.id}" total="#{o.total}"/>)
else
raise ArgumentError, @format
end
end
lines.join("\n") + "\n"
end
end
class NightlyJob
def initialize(config, orders, warehouse)
@config = config
@orders = orders
@warehouse = warehouse
end
def run
csv = OrderExport.new(@config).export(@orders.today)
@warehouse.upload_csv(csv)
end
endAfter
# config/settings.yml
module OrderExport
def self.export(orders)
lines = ["id,total,postal_code"]
orders.each do |o|
lines << "#{o.id},#{o.total},#{o.postal_code}"
end
lines.join("\n") + "\n"
end
end
class NightlyJob
def initialize(orders, warehouse)
@orders = orders
@warehouse = warehouse
end
def run
csv = OrderExport.export(@orders.today)
@warehouse.upload_csv(csv)
end
endTranscript, 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 from the settings 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 settings name. 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 branch of the format case gets a new value. A speculative option is a setting built for a need nobody has yet, and this class copies three of them. You can see what they cost here in the header, here in the CSV branch, here in JSON, and here in XML.
Step 3: Two formats nobody reads Code smell
Yet the settings file only ever says CSV, and the only caller uploads CSV to the warehouse. So the JSON and XML branches 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 typo in the XML quotes would sit there unnoticed 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 branch Remove Dead Code
No setting picks XML and no caller reads it, so the XML branch goes first. The case still has CSV, JSON and the else branch, and the file the warehouse gets is the same as before.
Step 6: Delete the JSON branch Remove Dead Code
JSON goes for the same reason, so the case is down to the CSV branch and an else. That else only raises for a format the settings file never sets.
Step 7: Format can only be csv Remove Field
With one branch left, format can only ever be CSV, so it stops being a field. The case collapses into the CSV row, and the header test only checks the header flag. Format leaves the settings file too, because nothing reads it anymore.
Step 8: The delimiter is always a comma Remove Field
The delimiter was a comma in the settings file and nowhere else, so we write the comma in place. The field goes, along with its line in initialize, its member in Export Config and its line in the settings 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, so the array starts with the header, and then the field goes because nothing reads it anymore. The settings file is down to an export key with nothing under it.
Step 10: Nothing left to configure Remove Parameter
Initialize took a config and read nothing from it, so it goes, and the Export Config class goes with it. The nightly job builds Order Export with no arguments now, and it drops its config parameter. And the settings 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 branches that took two of the four edits are gone, and there's one row line where there were three branches. YAGNI fit here because the settings 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 join and the nightly job didn't change.
Ruby bonus
Step 12: A class with nothing to hold Code smell
With the settings gone, Order Export has no instance variables left, and export never touches the object it runs on. So the job builds an object only to call its one method and throw it away.
Step 13: A module method is enough Module method
In Ruby a module can hold a method with no instance behind it, so the class becomes a module with self dot export. The body is the same five lines, and the job calls Order Export dot export with today's orders. If the export ever needs state again, like a setting that changes per environment, the class can come back with it.
Recap
Step 14: Where we started, where we ended Before / after
On the left, three instance variables copied a format, a delimiter and a header flag out of the settings. The format case had three branches, and only the CSV one ever ran. On the right, there's one short module method, and the settings file is empty. It already writes the postal code, and it's still shorter than the case was.
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, case branches no caller reaches, and defaults that nobody overrides.