DRY: one rule, one place
Change a business rule once and have every feature pick it up.
Watch the narrated video
Every step
Problem
Steps 1–3Refactor
Steps 4–10Payoff
Step 11Recap
Steps 12–13The code: before and after
Before
import "fmt"
type Checkout struct{}
func (Checkout) ShippingFee(cart Cart) float64 {
if cart.Subtotal >= 50.0 {
return 0.0
}
return 4.99
}
type CartBanner struct{}
func (CartBanner) Message(cart Cart) string {
if cart.Subtotal < 50.0 {
gap := 50.0 - cart.Subtotal
return fmt.Sprintf("Add $%.2f to ship free", gap)
}
return "Your order ships free"
}
type OrderEmail struct{}
func (OrderEmail) ShippingLine(cart Cart) string {
free := cart.Subtotal >= 50.0
if free {
return "Shipping: free"
}
return "Shipping: $4.99"
}After
import "fmt"
const (
freeShippingFrom = 50.0
memberFreeShippingFrom = 25.0
Fee = 4.99
)
func ShipsFree(cart Cart) bool {
return cart.Subtotal >= freeFrom(cart)
}
func AmountToFree(cart Cart) float64 {
return freeFrom(cart) - cart.Subtotal
}
func freeFrom(cart Cart) float64 {
if cart.Member {
return memberFreeShippingFrom
}
return freeShippingFrom
}
type Checkout struct{}
func (Checkout) ShippingFee(cart Cart) float64 {
if ShipsFree(cart) {
return 0.0
}
return Fee
}
type CartBanner struct{}
func (CartBanner) Message(cart Cart) string {
if !ShipsFree(cart) {
gap := AmountToFree(cart)
return fmt.Sprintf("Add $%.2f to ship free", gap)
}
return "Your order ships free"
}
type OrderEmail struct{}
func (OrderEmail) ShippingLine(cart Cart) string {
free := ShipsFree(cart)
if free {
return "Shipping: free"
}
return fmt.Sprintf("Shipping: $%.2f", Fee)
}Transcript, step by step
Problem
Step 1: Three features, one rule Before
Orders over fifty dollars ship free in this shop. Checkout uses that rule to charge the fee, and the cart banner uses it to tell you how much more to add. The order email checks it a third time to print the shipping line. All three work, and the tests pass.
Step 2: Members ship free from 25 Code smell
Marketing now wants members to ship free from twenty-five dollars. So you find the rule in checkout and in the banner, and you change it in both. This is duplicated knowledge, where one business rule is written out in several places. The free shipping limit lives here, here, and here, in the email nobody remembered to change. So a member who paid nothing for shipping gets an email that says four ninety-nine. And the next change to the limit starts with a search for every copy, because nothing tells you where they are.
Step 3: Undo, then give it a home Plan
Let's back that change out. DRY, short for don't repeat yourself, says every rule should have exactly one home in the code. So we'll build that home first, and then the member rule should take one edit.
Refactor
Step 4: Name the limit Extract Constant
First, the number fifty gets a name, Free Shipping From, as a constant at the top of the file. Every copy of fifty now reads that constant, so the limit itself lives in one place. The comparison, though, is still written out three times.
Step 5: Name the fee Extract Constant
The fee of four ninety-nine was typed twice too, so it becomes a constant named Fee. Now checkout and the email can't quote two different prices.
Step 6: Name the check Extract Function
Every feature keeps asking whether this cart ships free, so that question gets a function of its own. Ships Free sits next to the constants, and checkout calls it.
Step 7: The banner asks too Replace Inline Code with Function Call
The banner had the same check turned around, so now it calls ships free and negates the answer. A check that's flipped by hand is an easy place for an off-by-one bug to hide, and now there's nothing to flip.
Step 8: The email asks too Replace Inline Code with Function Call
The email gets the same call. Now the comparison is written once, in ships free, and all three features ask it.
Step 9: Name the gap Extract Function
The banner still did its own math with the limit, so that subtraction moves up next to Ships Free as Amount To Free. The banner no longer knows what the limit is, only how far away you are.
Step 10: Hide the limit Encapsulate Variable
Nothing outside these two functions reads the limit anymore, so we rename it with a lowercase f, which Go keeps inside this package. Once the features move to their own packages, the compiler stops anyone reading the limit directly, so the rule stays in one place.
Payoff
Step 11: Members, in one place Payoff
Now we add the member limit again. It goes into one unexported function, free from, and both exported functions use it. Checkout, the banner and the email ask the same questions they asked before, here, here, and here. None of them changed, and the email can't fall behind again, because it gets its answer from the same place checkout does.
Recap
Step 12: Where we started, where we ended Before / after
On the left, each feature carried its own copy of the fifty dollar limit. Changing the rule meant finding every copy, and missing one was easy. On the right, the limit, the fee and the check live together at the top of the file. Checkout asks the policy, and so do the banner and the email.
Step 13: When to reach for it Takeaways
Give a rule one home as soon as a second feature needs the same decision. Leave code alone when it only looks the same, like a shipping fee and a gift wrap fee that both happen to cost four ninety-nine, because they'll change for different reasons. You'll spot the smell when the same number or condition turns up in several types, and a small change starts with a search.