Emergent Design — Practice Tasks¶
12 hands-on exercises on Kent Beck's four rules of simple design and the YAGNI judgment that surrounds them. Every task: a scenario, code (Go / Java / Python — varied), a precise instruction, and a collapsible
<details>solution with the reasoning, not just the answer. The hard part of emergent design is rarely the mechanics of extraction — it is knowing when not to abstract. Several tasks are deliberately judgment calls where the correct answer is "leave it alone (for now)."
Table of Contents¶
- Task 1 — Remove essential duplication (Go, easy)
- Task 2 — Leave accidental look-alike duplication alone (Python, easy)
- Task 3 — Make intent explicit without adding structure (Java, easy)
- Task 4 — Delete speculative generality:
Manager<T>(Java, medium) - Task 5 — Delete the unused plugin hook (Go, medium)
- Task 6 — Apply the Rule of Three (Python, medium)
- Task 7 — Should you abstract this? (Go, medium — judgment)
- Task 8 — Reduce class count without harming clarity (Java, medium)
- Task 9 — Inline a wrong abstraction back to duplication (Python, hard)
- Task 10 — Premature interface from one implementation (Go, hard)
- Task 11 — Sequence a feature: make it work → right → fast (Python, hard)
- Task 12 — Simple-design audit (Java, hard — open-ended)
How to Use¶
Read the chapter README first so the four rules are fresh:
- Passes the tests — correctness is non-negotiable.
- Reveals intent — the code says what it means.
- No duplication — every piece of knowledge has one home (DRY).
- Fewest elements — no class, method, or interface that does not earn its place.
Rules are ordered. When two conflict, the earlier one wins: clarity (rule 2) beats deduplication (rule 3), and you never delete an element (rule 4) at the cost of intent.
Work each task before opening the solution. For the judgment tasks (7, and parts of 2, 9, 12) write down your decision and your reason before expanding — emergent design is a skill of justification, not of pattern-matching. The mermaid diagram below is the loop every task lives inside.
Task 1 — Remove essential duplication (Go, easy)¶
Scenario: Three handlers compute the same discounted price the same way. This is essential duplication — one business rule ("loyalty members get 10% off, capped at $50") copied into three places. Change the cap and you must hunt down three call sites. Extract it.
package pricing
func checkoutTotal(items []Item, member bool) float64 {
subtotal := sum(items)
if member {
d := subtotal * 0.10
if d > 50 {
d = 50
}
subtotal -= d
}
return subtotal
}
func cartPreview(items []Item, member bool) float64 {
subtotal := sum(items)
if member {
d := subtotal * 0.10
if d > 50 {
d = 50
}
subtotal -= d
}
return subtotal
}
func emailQuote(items []Item, member bool) float64 {
subtotal := sum(items)
if member {
d := subtotal * 0.10
if d > 50 {
d = 50
}
subtotal -= d
}
return subtotal
}
Instruction: Give the discount rule a single home (rule 3). The three callers should keep their distinct names — they describe different intents (rule 2), so do not collapse them into one function.
Solution
package pricing
const (
loyaltyRate = 0.10
loyaltyCap = 50.0
)
// loyaltyDiscount is the one home for the discount rule.
// Change the cap here and every caller is correct.
func loyaltyDiscount(subtotal float64, member bool) float64 {
if !member {
return 0
}
d := subtotal * loyaltyRate
if d > loyaltyCap {
return loyaltyCap
}
return d
}
func checkoutTotal(items []Item, member bool) float64 {
subtotal := sum(items)
return subtotal - loyaltyDiscount(subtotal, member)
}
func cartPreview(items []Item, member bool) float64 {
subtotal := sum(items)
return subtotal - loyaltyDiscount(subtotal, member)
}
func emailQuote(items []Item, member bool) float64 {
subtotal := sum(items)
return subtotal - loyaltyDiscount(subtotal, member)
}
Task 2 — Leave accidental look-alike duplication alone (Python, easy)¶
Scenario: A reviewer flags these two functions as "duplication — please DRY this up." They look almost identical.
def is_valid_username(s: str) -> bool:
if not s:
return False
if len(s) < 3 or len(s) > 20:
return False
return s.isalnum()
def is_valid_room_code(s: str) -> bool:
if not s:
return False
if len(s) < 3 or len(s) > 20:
return False
return s.isalnum()
Instruction: Decide whether to extract a shared _is_alnum_in_range(s, lo, hi) helper. State your decision and justify it against the four rules. (Hint: ask what happens when one rule changes.)
Solution
**Decision: do not extract — for now.** This is *accidental* (coincidental) duplication. The two functions are equal today by coincidence, not because they encode the same fact. A username rule and a room-code rule are independent business concepts that happen to share constraints this week. Run the test that distinguishes essential from accidental duplication: **ask what makes each rule change, and whether they change together.** If product later decides usernames may contain underscores, or room codes must be exactly 6 characters, the two functions diverge — and a shared helper becomes a liability. You would either thread in flags (`_is_alnum_in_range(s, lo, hi, allow_underscore=False)`) until the helper is a configuration soup, or you would tear the abstraction back out. A premature merge here would couple two concepts that have no reason to move together. DRY is about a single source of truth for a piece of *knowledge*, not about deleting visually similar lines. Two facts that are identical by accident are still two facts.# Leave them separate. Optionally make intent explicit with a comment
# at the point a reader might "helpfully" merge them:
def is_valid_username(s: str) -> bool:
# Independent of room-code rules despite the resemblance — see Task 2.
if not s:
return False
if not (3 <= len(s) <= 20):
return False
return s.isalnum()
def is_valid_room_code(s: str) -> bool:
if not s:
return False
if not (3 <= len(s) <= 20):
return False
return s.isalnum()
Task 3 — Make intent explicit without adding structure (Java, easy)¶
Scenario: The code passes its tests (rule 1) but a new reader cannot tell what the magic numbers and bit-twiddling mean. Rule 2 says reveal intent. You can do it with renaming and naming intermediate values alone — no new classes.
class AccessControl {
boolean check(int u, int r) {
return (u & 4) != 0 && (r & 1) != 0 && (u & 8) == 0;
}
}
Instruction: Rewrite so the rule is readable. Do not introduce a class hierarchy or a framework — the smallest change that reveals intent is the right one (rule 4).
Solution
class AccessControl {
private static final int PERM_WRITE = 1 << 2; // 4
private static final int PERM_SUSPENDED = 1 << 3; // 8
private static final int RESOURCE_WRITABLE = 1 << 0; // 1
boolean canWrite(int userFlags, int resourceFlags) {
boolean userHasWrite = (userFlags & PERM_WRITE) != 0;
boolean userSuspended = (userFlags & PERM_SUSPENDED) != 0;
boolean resourceWritable = (resourceFlags & RESOURCE_WRITABLE) != 0;
return userHasWrite && resourceWritable && !userSuspended;
}
}
Task 4 — Delete speculative generality: Manager<T> (Java, medium)¶
Scenario: A previous engineer built a generic, pluggable, strategy-driven registry "so we can manage any entity in the future." There is exactly one entity: User. There is one strategy. There are zero second consumers.
interface EntityStrategy<T> {
String keyOf(T entity);
T merge(T existing, T incoming);
}
class DefaultUserStrategy implements EntityStrategy<User> {
public String keyOf(User u) { return u.id(); }
public User merge(User existing, User incoming) { return incoming; }
}
class EntityManager<T> {
private final Map<String, T> store = new HashMap<>();
private final EntityStrategy<T> strategy;
EntityManager(EntityStrategy<T> strategy) { this.strategy = strategy; }
void put(T entity) {
String k = strategy.keyOf(entity);
T existing = store.get(k);
store.put(k, existing == null ? entity : strategy.merge(existing, entity));
}
T get(String key) { return store.get(key); }
}
// the only usage anywhere in the codebase:
class UserService {
private final EntityManager<User> users =
new EntityManager<>(new DefaultUserStrategy());
void save(User u) { users.put(u); }
User find(String id) { return users.get(id); }
}
Instruction: Apply YAGNI. Collapse the speculative generality into the simplest code that serves the one real requirement (rule 4), keeping behavior identical (rule 1).
Solution
class UserService {
private final Map<String, User> users = new HashMap<>();
void save(User u) { users.put(u.id(), u); } // merge == "incoming wins" == plain put
User find(String id) { return users.get(id); }
}
Task 5 — Delete the unused plugin hook (Go, medium)¶
Scenario: An order pipeline grew a "plugin system" to support hooks "in case we need to extend it." A year later, grep shows the registry is never populated. Every order pays the cost of iterating an always-empty slice and reading the indirection.
package orders
type Hook interface {
BeforeSubmit(o *Order) error
AfterSubmit(o *Order) error
}
type Pipeline struct {
hooks []Hook
}
func (p *Pipeline) Register(h Hook) { p.hooks = append(p.hooks, h) }
func (p *Pipeline) Submit(o *Order) error {
for _, h := range p.hooks {
if err := h.BeforeSubmit(o); err != nil {
return err
}
}
if err := persist(o); err != nil {
return err
}
for _, h := range p.hooks {
if err := h.AfterSubmit(o); err != nil {
return err
}
}
return nil
}
// Across the entire codebase, Register is never called and no Hook is implemented.
Instruction: Confirm the hook machinery is dead, then remove it (rule 4). Show the result and explain how you would re-introduce extensibility only when a real hook appears.
Solution
**Reasoning.** A `grep -r "\.Register(" && grep -r "BeforeSubmit"` across the module returns no implementations and no callers — the extension point is *speculative generality* that never paid off. The `Hook` interface, the `hooks` slice, the `Register` method, and two loops existed for hypothetical extenders. Rule 4 says delete elements that do not earn their place; an unused abstraction earns nothing and costs every reader an "is this used?" investigation (the one we just ran). How to re-introduce it correctly **when a real need arrives** — say, a genuine "send confirmation email after submit" requirement: If a *second* concrete after-submit action then appears, *that* is the moment the Rule of Three justifies extracting a hook list. You let the abstraction be **pulled out of working code by real duplication**, rather than guessing its shape in advance. Designing the extension point before the first extension reliably produces the wrong extension point.Task 6 — Apply the Rule of Three (Python, medium)¶
Scenario: You are adding a third notification channel. The first two were written inline. Now there are three near-identical blocks — and unlike Task 2, they do share a fact: "build a payload, send via transport, log the outcome." This is the third occurrence. Now abstract.
def notify_email(user, message):
payload = {"to": user.email, "body": message, "ts": now()}
resp = email_client.send(payload)
log.info("email sent", user=user.id, ok=resp.ok)
return resp.ok
def notify_sms(user, message):
payload = {"to": user.phone, "body": message, "ts": now()}
resp = sms_client.send(payload)
log.info("sms sent", user=user.id, ok=resp.ok)
return resp.ok
def notify_push(user, message): # the third occurrence — abstract now
payload = {"to": user.device_token, "body": message, "ts": now()}
resp = push_client.send(payload)
log.info("push sent", user=user.id, ok=resp.ok)
return resp.ok
Instruction: Extract the shared shape now that the Rule of Three is satisfied. The only things that vary are which field is the address and which client sends. Keep each channel's identity readable.
Solution
from dataclasses import dataclass
from typing import Callable
@dataclass(frozen=True)
class Channel:
name: str
address_of: Callable[[object], str] # how to find the recipient address on a user
client: object # transport with .send(payload) -> resp
def _notify(channel: Channel, user, message) -> bool:
payload = {"to": channel.address_of(user), "body": message, "ts": now()}
resp = channel.client.send(payload)
log.info(f"{channel.name} sent", user=user.id, ok=resp.ok)
return resp.ok
EMAIL = Channel("email", lambda u: u.email, email_client)
SMS = Channel("sms", lambda u: u.phone, sms_client)
PUSH = Channel("push", lambda u: u.device_token, push_client)
def notify_email(user, message): return _notify(EMAIL, user, message)
def notify_sms(user, message): return _notify(SMS, user, message)
def notify_push(user, message): return _notify(PUSH, user, message)
Task 7 — Should you abstract this? (Go, medium — judgment)¶
Scenario: You are reviewing a PR. The author has factored two configuration loaders into a generic LoadConfig[T any] with reflection-based field mapping, "to avoid duplication." Here is the before (what exists on main) and the after (the PR).
// BEFORE — two small, explicit loaders on main
func loadServerConfig(path string) (ServerConfig, error) {
var c ServerConfig
b, err := os.ReadFile(path)
if err != nil {
return c, err
}
return c, json.Unmarshal(b, &c)
}
func loadDBConfig(path string) (DBConfig, error) {
var c DBConfig
b, err := os.ReadFile(path)
if err != nil {
return c, err
}
return c, json.Unmarshal(b, &c)
}
// AFTER — the PR's "DRY" version
func LoadConfig[T any](path string, validators ...func(T) error) (T, error) {
var c T
b, err := os.ReadFile(path)
if err != nil {
return c, err
}
if err := json.Unmarshal(b, &c); err != nil {
return c, err
}
for _, v := range validators {
if err := v(c); err != nil {
return c, err
}
}
return c, nil
}
Instruction: This is a judgment exercise. Decide whether to accept the abstraction, and justify against the four rules. There is a defensible nuance — state the condition under which your answer flips.
Solution
**Decision: not yet — request changes (with a caveat below).** As submitted, the abstraction is not earning its place. Walk the rules: - **Rule 1 (tests):** both versions pass. Neutral. - **Rule 2 (intent):** the two explicit loaders say exactly what they do at the call site (`loadDBConfig("db.json")`). `LoadConfigDBConfig` is no clearer and adds a generic ceremony a reader must parse. Slight loss. - **Rule 3 (duplication):** here is the crux. The "duplicated" body is `ReadFile` + `Unmarshal` — six lines of *standard-library glue*, not domain knowledge. Two functions calling `json.Unmarshal` is not two copies of a business fact; it is two functions doing the obvious thing. This is closer to accidental duplication (Task 2) than essential (Task 1). - **Rule 4 (fewest elements):** the PR *adds* an element — a generic function with a variadic validator hook — and the validator slice is exactly the speculative plugin point from Task 5. No caller passes a validator today. So today the right call is to keep the two explicit loaders. The duplication is trivial glue, only two occurrences exist (Rule of Three unmet), and the generic version trades a hair of clarity for a flexibility nobody requested. **The condition that flips the answer:** if a *third* config type is being added in the same PR (Rule of Three met) **and** at least one loader genuinely needs post-load validation (the `validators` hook has a real first user), then `LoadConfig[T]` is justified — drop the variadic-as-speculation concern and accept it. The lesson: "should I abstract this?" is answered by *counting real occurrences and real needs*, not by how DRY the diff looks.Task 8 — Reduce class count without harming clarity (Java, medium)¶
Scenario: A teammate, over-applying "small classes," split a trivial value into a tower of one-line types. Rule 4 says use the fewest elements — but rule 2 (intent) still wins if collapsing would hide meaning. Find the balance.
interface Temperature { double celsius(); }
class CelsiusTemperature implements Temperature {
private final double value;
CelsiusTemperature(double value) { this.value = value; }
public double celsius() { return value; }
}
class TemperatureFactory {
static Temperature fromCelsius(double c) { return new CelsiusTemperature(c); }
}
class TemperatureFormatter {
String format(Temperature t) { return t.celsius() + "°C"; }
}
// Only ever used as:
// Temperature t = TemperatureFactory.fromCelsius(21.5);
// String s = new TemperatureFormatter().format(t);
Instruction: Collapse the unnecessary elements while preserving any meaning worth keeping (rule 2). Justify which elements you keep and which you delete.
Solution
**Reasoning.** The four classes/interface collapse to one `record`. We deleted: - The **interface** `Temperature` — an interface with a single implementation and no second one in sight is speculative generality (cf. Task 10). It adds a type to read with no polymorphism behind it. - The **`TemperatureFactory`** — a factory that does `new` and nothing else is pure ceremony. `new Temperature(21.5)` is already clear. - The **`TemperatureFormatter`** — a stateless one-method class wrapping behavior that belongs *on the value itself*. Moving `formatted()` onto the record reveals intent better (the temperature knows how to show itself) while removing an element. What we *kept* and even added: the `celsius` accessor (now the record component) and an absolute-zero invariant. Note rule 4 is "fewest elements," not "fewest possible at any cost" — we did not, for example, replace `Temperature` with a bare `double`, because the type carries real meaning and guards an invariant. Collapsing to `double` would serve rule 4 but sacrifice rule 2, and the ordering forbids that trade. The skill is deleting *ceremony* (factory, formatter, redundant interface) while preserving *meaning* (the value type and its invariant).Task 9 — Inline a wrong abstraction back to duplication (Python, hard)¶
Scenario: Six months ago someone "DRY-ed up" report generation. The abstraction has since grown five boolean flags and a kind switch as the two reports drifted apart. The shared function now serves no one well. This is the classic wrong abstraction — the fix is to inline it back to two honest, separate functions, then re-extract only what is truly common.
def build_report(kind, data, *, include_totals=False, currency=None,
group_by=None, redacted=False, locale="en"):
rows = []
for d in data:
if kind == "invoice":
row = {"id": d.id, "amount": fmt(d.amount, currency)}
if redacted:
row["customer"] = mask(d.customer)
else:
row["customer"] = d.customer
elif kind == "usage":
row = {"id": d.id, "units": d.units, "locale": locale}
if group_by:
row["bucket"] = d.timestamp.strftime(group_by)
rows.append(row)
out = {"rows": rows}
if include_totals and kind == "invoice":
out["total"] = fmt(sum(d.amount for d in data), currency)
if include_totals and kind == "usage":
out["total"] = sum(d.units for d in data)
return out
Instruction: The two report kinds share almost nothing real. Inline the abstraction back into two separate functions (accepting some duplication), then extract only the genuinely shared piece. Explain why duplication is the better state here.
Solution
# The one genuinely shared fact: turning a list of rows into the output envelope.
def _envelope(rows, total=None):
out = {"rows": rows}
if total is not None:
out["total"] = total
return out
def build_invoice_report(data, *, currency, include_totals=False, redacted=False):
rows = [
{
"id": d.id,
"amount": fmt(d.amount, currency),
"customer": mask(d.customer) if redacted else d.customer,
}
for d in data
]
total = fmt(sum(d.amount for d in data), currency) if include_totals else None
return _envelope(rows, total)
def build_usage_report(data, *, group_by=None, include_totals=False, locale="en"):
rows = []
for d in data:
row = {"id": d.id, "units": d.units, "locale": locale}
if group_by:
row["bucket"] = d.timestamp.strftime(group_by)
rows.append(row)
total = sum(d.units for d in data) if include_totals else None
return _envelope(rows, total)
Task 10 — Premature interface from one implementation (Go, hard)¶
Scenario: A package exports a Storage interface "for testability and future backends." There is one implementation (PostgresStorage), one consumer, and tests use the real DB anyway. The interface lives in the same package as its only implementer, forcing both to move together.
package store
type Storage interface {
SaveUser(ctx context.Context, u User) error
GetUser(ctx context.Context, id string) (User, error)
DeleteUser(ctx context.Context, id string) error
ListUsers(ctx context.Context, page, size int) ([]User, error)
}
type PostgresStorage struct{ db *sql.DB }
func NewPostgresStorage(db *sql.DB) *PostgresStorage { return &PostgresStorage{db: db} }
func (s *PostgresStorage) SaveUser(ctx context.Context, u User) error { /* ... */ }
func (s *PostgresStorage) GetUser(ctx context.Context, id string) (User, error) { /* ... */ }
func (s *PostgresStorage) DeleteUser(ctx context.Context, id string) error { /* ... */ }
func (s *PostgresStorage) ListUsers(ctx context.Context, page, size int) ([]User, error) { /* ... */ }
// The only consumer:
type UserService struct{ storage Storage }
func NewUserService(s Storage) *UserService { return &UserService{storage: s} }
Instruction: Apply YAGNI and idiomatic Go. Remove the premature interface (rule 4). Then explain the correct, deferred way to introduce an interface — including where it should live — if a second backend or a unit-test seam genuinely appears.
Solution
package store
type PostgresStorage struct{ db *sql.DB }
func NewPostgresStorage(db *sql.DB) *PostgresStorage { return &PostgresStorage{db: db} }
func (s *PostgresStorage) SaveUser(ctx context.Context, u User) error { /* ... */ }
func (s *PostgresStorage) GetUser(ctx context.Context, id string) (User, error) { /* ... */ }
func (s *PostgresStorage) DeleteUser(ctx context.Context, id string) error { /* ... */ }
func (s *PostgresStorage) ListUsers(ctx context.Context, page, size int) ([]User, error) { /* ... */ }
// UserService depends on the concrete type until a real second user appears.
type UserService struct{ storage *PostgresStorage }
func NewUserService(s *PostgresStorage) *UserService { return &UserService{storage: s} }
Task 11 — Sequence a feature: make it work → right → fast (Python, hard)¶
Scenario: New requirement: "given a list of orders, return the top-N customers by total spend." A teammate's first instinct is a clever single-pass heap with memoized currency conversion. Emergent design says sequence the work: make it work (correct, simplest thing), make it right (clean, intent-revealing, deduplicated), make it fast (only if measurement demands it). Produce all three stages.
# Requirement: top_n_customers(orders, n) -> [(customer_id, total_spend), ...]
# orders: list of objects with .customer_id and .amount (already same currency)
Instruction: Write Stage 1 (make it work), Stage 2 (make it right), and Stage 3 (make it fast) — and crucially, state the condition under which Stage 3 is actually warranted. Do not optimize without that condition.
Solution
**Stage 1 — make it work.** The simplest correct thing. Don't be clever; pass the test.def top_n_customers(orders, n):
totals = {}
for o in orders:
totals[o.customer_id] = totals.get(o.customer_id, 0) + o.amount
ranked = sorted(totals.items(), key=lambda kv: kv[1], reverse=True)
return ranked[:n]
Task 12 — Simple-design audit (Java, hard — open-ended)¶
Scenario: Below is a real-looking module. Audit it against the four rules of simple design and YAGNI. For each finding, name the rule violated and the fix — and for at least one item, decide that the right move is to leave it alone.
// A "flexible" pricing engine for a shop that sells exactly one product type: e-books.
interface PriceRule { BigDecimal apply(BigDecimal base, Context ctx); }
abstract class AbstractPriceRule implements PriceRule {
protected abstract BigDecimal doApply(BigDecimal base, Context ctx);
public final BigDecimal apply(BigDecimal base, Context ctx) {
return doApply(base, ctx); // hook does nothing but delegate
}
}
class PercentDiscountRule extends AbstractPriceRule {
private final BigDecimal pct;
PercentDiscountRule(BigDecimal pct) { this.pct = pct; }
protected BigDecimal doApply(BigDecimal base, Context ctx) {
return base.subtract(base.multiply(pct));
}
}
class PriceEngine {
private final List<PriceRule> rules;
private final RuleFactory factory; // builds rules from config strings
private final boolean enableExperimental; // never read
PriceEngine(RuleFactory f) { this.factory = f; this.rules = new ArrayList<>(); this.enableExperimental = false; }
BigDecimal price(BigDecimal base, Context ctx) {
BigDecimal p = base;
for (PriceRule r : rules) p = r.apply(base, ctx); // BUG: uses base, not p
return p;
}
}
// In production there is exactly ONE rule configured: a flat 10% e-book discount.
Instruction: Produce a findings table (rule violated → fix), call out the correctness bug, and state which element you would keep and why. Then give the collapsed implementation that this one real requirement actually warrants.
Solution
**Findings.** | Finding | Rule | Fix | |---|---|---| | `apply` accumulates onto `base`, not `p`, so chained rules silently drop all but the last | Rule 1 (must work) | Fix first — correctness precedes every other rule. `p = r.apply(p, ctx);` | | `AbstractPriceRule` template hook delegates and adds nothing | Rule 4 (fewest elements) | Delete the abstract class; rules implement `PriceRule` directly — or drop the interface too (below). | | `RuleFactory` builds rules from config strings, but there is one hard-coded rule | Rule 4 / YAGNI | Remove; instantiate the one rule directly until config-driven rules are a real requirement. | | `enableExperimental` field is never read | Rule 4 / YAGNI | Delete dead state. | | `PriceRule` interface + `ListSelf-Assessment¶
You have internalized emergent design when you can answer these without hedging:
- For a given block of repetition, can you classify it as essential (one fact, many copies → extract) or accidental (looks alike by coincidence → leave it)? (Tasks 1, 2)
- Can you state the test that distinguishes them — "do these change for the same reason, at the same time?" (Task 2)
- Do you reach for the smallest rule-2 fix (rename, name a value) before adding a class, interface, or framework? (Tasks 3, 8)
- Can you spot speculative generality —
Manager<T>, unused hooks, single-implementation interfaces, never-read flags — and delete it without anxiety? (Tasks 4, 5, 10, 12) - Do you wait for the third occurrence before abstracting, and can you justify why three? (Task 6)
- When asked "should I abstract this?", is your honest default answer "not yet," and can you name the condition that flips it? (Tasks 7, 12)
- Can you recognize a wrong abstraction (flag-laden god-function) and inline it back to honest duplication before re-extracting only what is shared? (Task 9)
- Do you sequence work as make it work → make it right → make it fast, optimizing only after measurement? (Task 11)
- When two rules conflict, do you apply them in order — correctness, then intent, then no-duplication, then fewest-elements? (every task)
If you abstracted in Task 2 or 7, or optimized in Task 11 without a measured need, re-read the chapter README: the discipline is restraint, not cleverness.
Related Topics¶
- Chapter README — the four rules of simple design, stated positively.
junior.md— emergent-design vocabulary and the YAGNI mindset for beginners.find-bug.md— snippets where speculative generality hides a defect (as in Task 12).optimize.md— the "make it fast" stage done responsibly, with measurement first.- Refactoring — Extract Function, Inline Function, and the mechanics behind these moves.
- Design Patterns — patterns are the abstractions you reach for after the Rule of Three, never before.
In this topic