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
class Checkout {
fun shippingFee(cart: Cart): Double {
if (cart.subtotal >= 50.0) {
return 0.0
}
return 4.99
}
}
class CartBanner {
fun message(cart: Cart): String {
if (cart.subtotal < 50.0) {
val missing = 50.0 - cart.subtotal
return "Add \$$missing to ship free"
}
return "Your order ships free"
}
}
class OrderEmail {
fun shippingLine(cart: Cart): String {
val free = cart.subtotal >= 50.0
return "Shipping: " + if (free) "free" else "$4.99"
}
}After
object ShippingPolicy {
private const val FREE_FROM = 50.0
private const val MEMBER_FREE_FROM = 25.0
const val FEE = 4.99
fun shipsFree(cart: Cart): Boolean {
return cart.subtotal >= freeFrom(cart)
}
fun amountToFree(cart: Cart): Double {
return freeFrom(cart) - cart.subtotal
}
private fun freeFrom(cart: Cart): Double {
return if (cart.member) MEMBER_FREE_FROM else FREE_FROM
}
}
class Checkout {
fun shippingFee(cart: Cart): Double {
if (ShippingPolicy.shipsFree(cart)) {
return 0.0
}
return ShippingPolicy.FEE
}
}
class CartBanner {
fun message(cart: Cart): String {
if (!ShippingPolicy.shipsFree(cart)) {
val missing = ShippingPolicy.amountToFree(cart)
return "Add \$$missing to ship free"
}
return "Your order ships free"
}
}
class OrderEmail {
fun shippingLine(cart: Cart): String {
val free = ShippingPolicy.shipsFree(cart)
return "Shipping: " + if (free) "free" else "$" + ShippingPolicy.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 From, in a new object called Shipping Policy. 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 lives in the policy, 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 into the policy 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 the policy reads Free From anymore, so we make it private. If someone later tries to copy the limit back into a feature, the compiler stops them, so the rule stays in one place.
Payoff
Step 11: Members, in one object Payoff
Now we add the member limit again. It goes into one private function, free from, and both public 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 in Shipping Policy. 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 classes, and a small change starts with a search.