Functions — Practice Tasks¶
Twelve refactoring drills that turn the rules from the chapter README into muscle memory. Each task gives you a scenario, a function that violates one clean-function principle, and a precise instruction. Work the refactor yourself first; the collapsible solution shows the cleaned code and the reasoning behind every move. Languages alternate between Go, Java, and Python on purpose — the principles are language-independent; the idioms are not.
Table of Contents¶
- Extract a long function into named steps (Go, easy)
- Remove a flag argument by splitting the function (Java, easy)
- Replace an output argument with a return value (Python, easy)
- Eliminate temporal coupling (Java, medium)
- Lift mixed levels of abstraction (Python, medium)
- Replace a null return with an empty collection (Go, medium)
- Replace a null return with Optional (Java, medium)
- Introduce a parameter object (Python, medium)
- Separate command from query (Java, medium)
- Make a hidden side effect explicit and isolated (Go, hard)
- Replace a boolean-returning failure path with a Result type (Go, hard)
- Full audit: many smells at once (Python, hard)
How to Use¶
- Read the smelly code and the instruction. Name the principle being violated out loud before you touch anything — naming the smell is half the skill.
- Refactor on paper or in an editor first. Do not peek. The point is to train the move, not to read a solution.
- Open the solution. Compare structure, not character-for-character text. If your version reads as cleanly and preserves behaviour, you passed.
- Read the reasoning. Every solution explains why the move helps — that "why" is what transfers to your own code.
- Re-run the behaviour in your head. Clean refactoring never changes observable behaviour. If outputs could differ, it is a rewrite, not a refactor.
The dependency between the smells is roughly:
Task 1 — Extract a long function (Go, easy)¶
Scenario: A signup handler grew over six months. The top-level function is 40 lines and reads as one undifferentiated wall. A new engineer cannot tell where validation ends and persistence begins.
Instruction: Extract the implicit phases into named functions so the top-level function reads as a table of contents. Keep behaviour identical.
func RegisterUser(db *sql.DB, form SignupForm) (*User, error) {
// validate
if form.Email == "" {
return nil, errors.New("email required")
}
if !strings.Contains(form.Email, "@") {
return nil, errors.New("email invalid")
}
if len(form.Password) < 8 {
return nil, errors.New("password too short")
}
// normalize
email := strings.ToLower(strings.TrimSpace(form.Email))
name := strings.TrimSpace(form.Name)
if name == "" {
name = strings.Split(email, "@")[0]
}
// hash password
hash, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("hashing password: %w", err)
}
// persist
user := &User{Email: email, Name: name, PasswordHash: string(hash)}
res, err := db.Exec(
"INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)",
user.Email, user.Name, user.PasswordHash,
)
if err != nil {
return nil, fmt.Errorf("inserting user: %w", err)
}
id, _ := res.LastInsertId()
user.ID = id
return user, nil
}
Solution
func RegisterUser(db *sql.DB, form SignupForm) (*User, error) {
if err := validateSignup(form); err != nil {
return nil, err
}
user, err := buildUser(form)
if err != nil {
return nil, err
}
if err := insertUser(db, user); err != nil {
return nil, err
}
return user, nil
}
func validateSignup(form SignupForm) error {
switch {
case form.Email == "":
return errors.New("email required")
case !strings.Contains(form.Email, "@"):
return errors.New("email invalid")
case len(form.Password) < 8:
return errors.New("password too short")
default:
return nil
}
}
func buildUser(form SignupForm) (*User, error) {
email := strings.ToLower(strings.TrimSpace(form.Email))
name := strings.TrimSpace(form.Name)
if name == "" {
name = strings.Split(email, "@")[0]
}
hash, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("hashing password: %w", err)
}
return &User{Email: email, Name: name, PasswordHash: string(hash)}, nil
}
func insertUser(db *sql.DB, user *User) error {
res, err := db.Exec(
"INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)",
user.Email, user.Name, user.PasswordHash,
)
if err != nil {
return fmt.Errorf("inserting user: %w", err)
}
id, _ := res.LastInsertId()
user.ID = id
return nil
}
Task 2 — Remove a flag argument (Java, easy)¶
Scenario: Reviewers keep stumbling on notify(user, true) and notify(user, false) in pull requests. Nobody can recall what true means without opening the method.
Instruction: A boolean parameter that selects between two behaviours means the function is really two functions wearing a trench coat. Split it.
class Notifier {
void notify(User user, boolean urgent) {
Message message = buildMessage(user);
if (urgent) {
message.setChannel(Channel.SMS);
message.setPriority(Priority.HIGH);
smsGateway.send(message);
} else {
message.setChannel(Channel.EMAIL);
message.setPriority(Priority.NORMAL);
emailGateway.send(message);
}
}
}
// Call sites:
// notifier.notify(user, true);
// notifier.notify(user, false);
Solution
class Notifier {
void notifyUrgent(User user) {
Message message = buildMessage(user);
message.setChannel(Channel.SMS);
message.setPriority(Priority.HIGH);
smsGateway.send(message);
}
void notifyRoutine(User user) {
Message message = buildMessage(user);
message.setChannel(Channel.EMAIL);
message.setPriority(Priority.NORMAL);
emailGateway.send(message);
}
}
// Call sites — now self-documenting:
// notifier.notifyUrgent(user);
// notifier.notifyRoutine(user);
Task 3 — Replace an output argument with a return value (Python, easy)¶
Scenario: A function mutates a list passed in by the caller and returns None. Call sites read enrich(results) and then keep using results, which silently changed. New readers assume enrich is a no-op because they ignore its (nonexistent) return.
Instruction: Output arguments hide the function's real effect. Make the function take an input and return a fresh value instead.
def enrich(records):
for record in records:
record["full_name"] = f"{record['first']} {record['last']}"
record["initials"] = record["first"][0] + record["last"][0]
# mutates the caller's list; returns None
# Call site:
# rows = load_rows()
# enrich(rows) # easy to misread as having no effect
# render(rows)
Solution
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Record:
first: str
last: str
full_name: str = ""
initials: str = ""
def enrich(record: Record) -> Record:
return replace(
record,
full_name=f"{record.first} {record.last}",
initials=record.first[0] + record.last[0],
)
# Call site — the data flow is now visible:
# rows = load_rows()
# enriched = [enrich(r) for r in rows]
# render(enriched)
Task 4 — Eliminate temporal coupling (Java, medium)¶
Scenario: A ReportBuilder requires callers to invoke open(), then addSection() any number of times, then close() — in that exact order. Forgetting open() throws an NPE three calls later; forgetting close() silently writes a truncated file. The ordering rule lives only in a Confluence page.
Instruction: The class lets you call methods in an order that is invalid. Redesign the API so the correct sequence is the only sequence the type system allows.
class ReportBuilder {
private Writer writer; // null until open()
private boolean closed;
void open(String path) throws IOException {
this.writer = new FileWriter(path);
writer.write("<report>");
}
void addSection(String title, String body) throws IOException {
writer.write("<section><h>" + title + "</h>" + body + "</section>");
}
void close() throws IOException {
writer.write("</report>");
writer.close();
closed = true;
}
}
// Required call order — enforced only by hope:
// b.open("out.xml");
// b.addSection("Intro", "...");
// b.close();
Solution
class Report {
private final List<Section> sections;
private Report(List<Section> sections) {
this.sections = sections;
}
// Construction collects everything first; nothing is half-built.
static Report of(Section... sections) {
return new Report(List.of(sections));
}
// A single call does the whole open -> write -> close lifecycle.
void writeTo(Path path) throws IOException {
try (Writer writer = Files.newBufferedWriter(path)) {
writer.write("<report>");
for (Section s : sections) {
writer.write("<section><h>" + s.title() + "</h>" + s.body() + "</section>");
}
writer.write("</report>");
}
}
}
record Section(String title, String body) {}
// Usage — impossible to call out of order, impossible to forget close():
// Report.of(
// new Section("Intro", "..."),
// new Section("Results", "...")
// ).writeTo(Path.of("out.xml"));
Task 5 — Lift mixed levels of abstraction (Python, medium)¶
Scenario: A checkout function alternates between high-level business intent ("apply loyalty discount") and low-level mechanics (string formatting, dictionary key juggling, byte writing). Reading it feels like a plane that keeps changing altitude.
Instruction: Apply the Single Level of Abstraction Principle: every statement in a function should sit at the same conceptual level. Push the mechanics down into helpers so the top function tells the story.
def checkout(cart, customer):
total = 0
for item in cart["items"]:
total += item["price"] * item["qty"]
if customer["tier"] == "gold":
total = total - (total * 0.1)
# write receipt bytes to disk
lines = []
lines.append("RECEIPT".center(32))
lines.append("-" * 32)
for item in cart["items"]:
name = item["name"][:20].ljust(20)
amount = f"${item['price'] * item['qty']:.2f}".rjust(11)
lines.append(name + amount)
lines.append("-" * 32)
lines.append("TOTAL".ljust(20) + f"${total:.2f}".rjust(11))
with open(f"receipts/{cart['id']}.txt", "wb") as f:
f.write("\n".join(lines).encode("utf-8"))
return total
Solution
def checkout(cart, customer):
subtotal = compute_subtotal(cart)
total = apply_loyalty_discount(subtotal, customer)
write_receipt(cart, total)
return total
def compute_subtotal(cart):
return sum(item["price"] * item["qty"] for item in cart["items"])
def apply_loyalty_discount(subtotal, customer):
if customer["tier"] == "gold":
return subtotal * 0.9
return subtotal
def write_receipt(cart, total):
text = render_receipt(cart, total)
path = f"receipts/{cart['id']}.txt"
with open(path, "wb") as f:
f.write(text.encode("utf-8"))
def render_receipt(cart, total):
lines = ["RECEIPT".center(32), "-" * 32]
lines += [render_line(item) for item in cart["items"]]
lines += ["-" * 32, "TOTAL".ljust(20) + money(total).rjust(11)]
return "\n".join(lines)
def render_line(item):
name = item["name"][:20].ljust(20)
return name + money(item["price"] * item["qty"]).rjust(11)
def money(value):
return f"${value:.2f}"
Task 6 — Replace a null return with an empty collection (Go, medium)¶
Scenario: FindOverdueInvoices returns nil when there are no overdue invoices. Half the call sites forget to nil-check before ranging — which happens to be safe in Go for range, but the same pattern returns nil maps elsewhere and panics on write. The team wants one consistent rule: collection-returning functions never return nil.
Instruction: Make the function return an empty (non-nil) slice in the "nothing found" case, so callers can use the result unconditionally.
func FindOverdueInvoices(invoices []Invoice, now time.Time) []Invoice {
var overdue []Invoice
for _, inv := range invoices {
if inv.DueDate.Before(now) && !inv.Paid {
overdue = append(overdue, inv)
}
}
if len(overdue) == 0 {
return nil // forces callers to wonder: nil or empty?
}
return overdue
}
// Call site that breaks the team's rule:
// result := FindOverdueInvoices(all, now)
// if result == nil { // defensive noise repeated everywhere
// return
// }
// for _, inv := range result { ... }
Solution
func FindOverdueInvoices(invoices []Invoice, now time.Time) []Invoice {
overdue := make([]Invoice, 0, len(invoices)) // non-nil, right-sized
for _, inv := range invoices {
if inv.DueDate.Before(now) && !inv.Paid {
overdue = append(overdue, inv)
}
}
return overdue
}
// Call site — no nil check, no special case:
// for _, inv := range FindOverdueInvoices(all, now) {
// remind(inv)
// }
Task 7 — Replace a null return with Optional (Java, medium)¶
Scenario: findByEmail returns null when no user matches. A NullPointerException in production traced back to a caller that chained findByEmail(email).getName() without checking. The author's intent — "this might not find anything" — was invisible in the signature.
Instruction: A reference result that can legitimately be absent should say so in its type. Replace the nullable return with Optional<User> and update a representative call site to use it idiomatically.
class UserRepository {
User findByEmail(String email) {
for (User u : users) {
if (u.getEmail().equals(email)) {
return u;
}
}
return null; // signature lies: it claims to always return a User
}
}
// Call site that exploded:
// String name = repo.findByEmail(email).getName(); // NPE when absent
Solution
import java.util.Optional;
class UserRepository {
Optional<User> findByEmail(String email) {
return users.stream()
.filter(u -> u.getEmail().equals(email))
.findFirst();
}
}
// Call sites must now acknowledge absence — the compiler enforces it:
// Provide a default:
// String name = repo.findByEmail(email)
// .map(User::getName)
// .orElse("Unknown");
// Or branch explicitly:
// repo.findByEmail(email).ifPresentOrElse(
// this::sendWelcomeBack,
// () -> log.warn("No user for {}", email)
// );
// Or escalate to an error when absence is genuinely exceptional:
// User user = repo.findByEmail(email)
// .orElseThrow(() -> new UserNotFoundException(email));
Task 8 — Introduce a parameter object (Python, medium)¶
Scenario: A reporting function takes nine positional parameters. Call sites are unreadable (build_report(d1, d2, True, False, None, "csv", 50, True, tz)), and the same (start, end) date pair travels together through five other functions — a data clump.
Instruction: Group the parameters that belong together into cohesive objects. Identify the data clump first, then bundle the remaining options.
def build_report(
start_date,
end_date,
include_refunds,
include_pending,
customer_id,
output_format,
page_size,
send_email,
timezone,
):
...
# Call site:
# build_report(d1, d2, True, False, None, "csv", 50, True, "UTC")
Solution
from dataclasses import dataclass
from datetime import date
from enum import Enum
from typing import Optional
class OutputFormat(Enum):
CSV = "csv"
PDF = "pdf"
JSON = "json"
@dataclass(frozen=True)
class DateRange:
start: date
end: date
def __post_init__(self):
if self.end < self.start:
raise ValueError("end date precedes start date")
@dataclass(frozen=True)
class ReportFilters:
include_refunds: bool = False
include_pending: bool = False
customer_id: Optional[str] = None
@dataclass(frozen=True)
class ReportOptions:
output_format: OutputFormat = OutputFormat.CSV
page_size: int = 50
send_email: bool = False
timezone: str = "UTC"
def build_report(
period: DateRange,
filters: ReportFilters = ReportFilters(),
options: ReportOptions = ReportOptions(),
):
...
# Call site — self-documenting and order-independent:
# build_report(
# period=DateRange(d1, d2),
# filters=ReportFilters(include_refunds=True),
# options=ReportOptions(output_format=OutputFormat.CSV, send_email=True),
# )
Task 9 — Separate command from query (Java, medium)¶
Scenario: pop() both mutates the stack (removes the top) and returns a value. Worse, tryReserve() returns a boolean and performs the reservation, so a caller that logs if (reserve(seat)) accidentally double-books when the log line is evaluated twice during debugging. The team wants to honour Command-Query Separation: a method either does something or answers something, never both.
Instruction: Split the offending method so that querying has no side effects and commanding returns nothing observable beyond success/failure where unavoidable.
class SeatMap {
private final Set<Seat> reserved = new HashSet<>();
// Command AND query tangled together:
boolean reserve(Seat seat) {
if (reserved.contains(seat)) {
return false; // query: was it available?
}
reserved.add(seat); // command: reserve it
return true;
}
}
// Dangerous call site:
// if (reserve(seat)) { ... } // calling it again to "check" re-reserves
Solution
class SeatMap {
private final Set<Seat> reserved = new HashSet<>();
// Query: pure, idempotent, side-effect free.
boolean isAvailable(Seat seat) {
return !reserved.contains(seat);
}
// Command: performs the mutation, returns nothing.
// Guards its own precondition so it cannot silently corrupt state.
void reserve(Seat seat) {
if (reserved.contains(seat)) {
throw new SeatAlreadyReservedException(seat);
}
reserved.add(seat);
}
}
// Call site — query first, then command, each doing exactly one thing:
// if (seatMap.isAvailable(seat)) {
// seatMap.reserve(seat);
// }
Task 10 — Make a hidden side effect explicit and isolated (Go, hard)¶
Scenario: ValidatePassword looks like a pure predicate — it takes a user and a password and returns whether they match. But buried inside, it also initialises the user's session, mutates a package-level cache, and writes an audit log. A teammate called it inside a read-only health check and accidentally created thousands of sessions.
Instruction: A function's name promises "validate"; its body does four things, three of them surprising. Separate the pure decision from the side effects, and make the effects explicit at the call site.
var sessionCache = map[string]*Session{} // package-level mutable state
func ValidatePassword(u *User, password string) bool {
if bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) != nil {
return false
}
// surprise #1: creates a session as a side effect
session := newSession(u.ID)
// surprise #2: mutates global cache
sessionCache[session.ID] = session
// surprise #3: writes to the audit log
auditLog.Write("login", u.ID, time.Now())
u.LastLogin = time.Now() // surprise #4: mutates the argument
return true
}
Solution
// Pure decision: no mutation, no I/O, trivially testable.
func PasswordMatches(u *User, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil
}
// The effectful operation is now named for what it actually does,
// and its dependencies are explicit parameters rather than globals.
func (s *AuthService) Login(u *User, password string) (*Session, error) {
if !PasswordMatches(u, password) {
return nil, ErrInvalidCredentials
}
session := newSession(u.ID)
s.sessions.Put(session) // injected store, not a global map
s.audit.Record("login", u.ID, s.clock.Now())
u.LastLogin = s.clock.Now()
return session, nil
}
// Call sites can now choose their intent:
// PasswordMatches(u, pw) // check only, zero side effects
// authService.Login(u, pw) // check AND establish a session, explicitly
Task 11 — Replace a boolean failure path with a Result type (Go, hard)¶
Scenario: ParseConfig returns (Config, bool) where the bool means "did it work." When it returns false, the caller has no idea why — was the file missing, the YAML malformed, or a required key absent? Errors are swallowed into a single bit, and call sites print a useless "config failed" message.
Instruction: Replace the success-boolean with an explicit error result so failures carry meaning. Then show the idiomatic call site. (This is the "don't collapse failure into a boolean" sibling of "don't return null.")
func ParseConfig(path string) (Config, bool) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, false // why? lost.
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return Config{}, false // why? lost.
}
if cfg.Port == 0 {
return Config{}, false // why? lost.
}
return cfg, true
}
// Call site:
// cfg, ok := ParseConfig("app.yaml")
// if !ok {
// log.Fatal("config failed") // useless diagnostic
// }
Solution
func ParseConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("reading config %q: %w", path, err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("parsing config %q: %w", path, err)
}
if err := cfg.validate(); err != nil {
return Config{}, fmt.Errorf("invalid config %q: %w", path, err)
}
return cfg, nil
}
func (c Config) validate() error {
if c.Port == 0 {
return errors.New("port is required")
}
if c.Port < 0 || c.Port > 65535 {
return fmt.Errorf("port out of range: %d", c.Port)
}
return nil
}
// Call site — the failure now explains itself, and errors compose:
// cfg, err := ParseConfig("app.yaml")
// if err != nil {
// log.Fatalf("startup: %v", err)
// // -> startup: invalid config "app.yaml": port is required
// }
Task 12 — Full function audit (Python, hard)¶
Scenario: Below is one function carrying nearly every smell in this chapter at once. This is the boss level: you must name each violation and produce a clean rewrite.
Instruction: First, list every clean-function principle this code violates. Then rewrite it. The function is supposed to "process an order": it currently validates, mutates its arguments, talks to two services, mixes abstraction levels, hides side effects, uses a flag argument, and returns None on failure.
def process(order, cart, send_confirmation, results):
# validate
if not order:
return None
if not cart["items"]:
return None
# compute total inline (low-level, mixed with policy)
total = 0
for i in cart["items"]:
line = i["price"] * i["qty"]
if i.get("category") == "book":
line = line * 0.95
total += line
if order["tier"] == "gold":
total = total * 0.9
# hidden side effect: mutates the argument
order["total"] = total
order["status"] = "PROCESSED"
# flag argument controls a whole branch
if send_confirmation:
email_service.send(order["email"], f"Your total is ${total:.2f}")
# output argument: stuffs into caller's list, returns None
results.append({"order_id": order["id"], "total": total})
payment_gateway.charge(order["email"], total) # another hidden effect
return None
Solution
**The violations, named:** | Principle violated | Where | Symptom | |---|---|---| | Returning `null`/`None` for failure | `return None` (×3) | Caller cannot tell success from failure; no reason given. | | Output argument | `results.append(...)` | Function mutates a list it was handed instead of returning. | | Hidden side effects | `order["total"] = ...`, `order["status"] = ...` | Mutates its argument invisibly. | | Hidden side effects (I/O) | `email_service.send`, `payment_gateway.charge` | A "process" function silently sends email and charges money. | | Flag argument | `send_confirmation` | Boolean selects between two behaviours. | | Mixed abstraction levels | total loop next to business policy | Low-level arithmetic interleaved with discount policy. | | Command/query confusion | function both computes (query) and charges (command) | One function decides *and* acts. | | Doing more than one thing | the whole function | Validate + price + persist + notify + charge + collect. | **The clean rewrite:**from dataclasses import dataclass
from decimal import Decimal
from typing import Iterable
class OrderError(Exception):
"""Raised when an order cannot be processed."""
@dataclass(frozen=True)
class LineItem:
price: Decimal
qty: int
category: str = ""
@dataclass(frozen=True)
class Cart:
items: list[LineItem]
@dataclass(frozen=True)
class PricedOrder:
order_id: str
total: Decimal
# --- Pure pricing core: no I/O, no mutation, trivially testable ---
def price_cart(cart: Cart, tier: str) -> Decimal:
subtotal = sum(_line_total(item) for item in cart.items)
return _apply_tier_discount(subtotal, tier)
def _line_total(item: LineItem) -> Decimal:
total = item.price * item.qty
return total * Decimal("0.95") if item.category == "book" else total
def _apply_tier_discount(subtotal: Decimal, tier: str) -> Decimal:
return subtotal * Decimal("0.9") if tier == "gold" else subtotal
def validate(order: dict, cart: Cart) -> None:
if not order:
raise OrderError("order is missing")
if not cart.items:
raise OrderError("cart has no items")
# --- Effectful orchestration: each effect explicit, named, and isolated ---
class OrderProcessor:
def __init__(self, payments, emails):
self.payments = payments
self.emails = emails
def process(self, order: dict, cart: Cart) -> PricedOrder:
validate(order, cart)
total = price_cart(cart, order["tier"])
self.payments.charge(order["email"], total)
return PricedOrder(order_id=order["id"], total=total)
def confirm(self, order: dict, priced: PricedOrder) -> None:
self.emails.send(order["email"], f"Your total is ${priced.total:.2f}")
# --- Call site: flag gone, return value real, effects visible ---
# processor = OrderProcessor(payment_gateway, email_service)
# priced = processor.process(order, cart) # returns a value or raises
# results.append(priced) # caller owns its own list
# if should_confirm:
# processor.confirm(order, priced) # separate, explicit step
Self-Assessment¶
Rate yourself honestly on each. If you cannot do a row from memory, redo the matching task.
| # | Skill | Can you do it without looking? |
|---|---|---|
| 1 | Extract a long function so the top level reads as named steps | ☐ |
| 2 | Spot a flag argument and split it into two named functions | ☐ |
| 3 | Convert an output-argument mutation into a return value | ☐ |
| 4 | Redesign an API so an invalid call order becomes impossible | ☐ |
| 5 | Recognise mixed abstraction levels and lift the mechanics down | ☐ |
| 6 | Return an empty collection instead of nil/null | ☐ |
| 7 | Replace a nullable reference return with Optional and use it idiomatically | ☐ |
| 8 | Identify a data clump and introduce a parameter object | ☐ |
| 9 | Separate a command from a query (CQS) | ☐ |
| 10 | Make a hidden side effect explicit and push it to the edge | ☐ |
| 11 | Replace a boolean failure flag with a Result/error that carries meaning | ☐ |
| 12 | Audit a function for every smell at once and rewrite it clean | ☐ |
Scoring. 10–12 confident: you have internalised clean-function design — apply it in code review. 6–9: solid foundation; drill the rows you missed. Below 6: re-read the chapter README, then redo every task in order.
Related Topics¶
- Functions — chapter README — the positive rules these tasks invert.
- junior.md — beginner-level definitions of each anti-pattern.
- find-bug.md — buggy snippets where function smells hide a real defect.
- optimize.md — performance-flavoured function refactors.
- Meaningful Names — clean functions start with names that reveal intent.
- Error Handling — the home of the "don't return null / collapse errors into a bool" rules (Tasks 6, 7, 11).
- Refactoring — Code Smells — Long Method, Long Parameter List, and friends, catalogued and cross-referenced.
- Functional Programming — pure core / effectful shell (Tasks 10, 12) taken to its logical end.
Next: Apply these moves to your own codebase. Open the last function you wrote, run it against the Self-Assessment checklist, and refactor the first violation you find. The cheapest time to clean a function is the moment you are already reading it.
In this topic