I'll Be Backend
Principles · Go

DRY: one rule, one place

Change a business rule once and have every feature pick it up.

13 steps4 chapters~4 min at 1.25×#duplication#constants#single source of truth
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the Go video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–10

Payoff

Step 11

Recap

Steps 12–13

The 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.