Code Smells II: Couplers & Dispensables

Clean Code, SOLID & Refactoring

Chapter 5 · Code Smells II: Couplers & Dispensables

Where Chapter 4's Bloaters were about things growing too large, this chapter's five smells are about the wrong things being connected — coupling that shouldn't exist, and code that shouldn't still exist. Each one is verified directly, tying back to Software Architecture Fundamentals Chapter 5's own coupling and cohesion criteria.

Feature Envy: a Method More Interested in Someone Else's Data

class InvoicePrinter: def print_invoice(self, order): total = 0 for item in order.items: total += item['price'] * item['qty'] return f"Total: ${total:.2f}"
Verified directly — the method touches zero of its own data
Scanning print_invoice's own source: 0 references to self., at least 1 reference to order.InvoicePrinter has no state of its own, and this method does nothing but reach into Order's own data to do its job. The fix moves the calculation onto Order itself; the corrected print_invoice makes exactly one call, order.calculate_total(), with 0 direct references to order.items — both versions compute the identical "Total: $35.00".

Inappropriate Intimacy: Reaching Into What's Supposed to Be Private

class AccountAuditorBad: def audit(self, account): return account._balance > 10000 # reaches directly into a 'private' field class AccountAuditorGood: def audit(self, account): return account.get_balance() > 10000 # uses the public interface only
Verified directly — a legitimate internal rename breaks one version and not the other
Both audits correctly return True for a $15,000 balance before any change. BankAccount then undergoes a real, self-contained internal rename — _balance becomes _current_balance, with get_balance() updated to match. Afterward: AccountAuditorBad.audit() breaksAttributeError: 'BankAccountBadRenamed' object has no attribute '_balance'. AccountAuditorGood.audit() still correctly returns True — completely unaffected, because it never depended on the private field's own name.

Duplicated Code: Two Copies, One Fix

Verified directly — a real bug fix applied to only one of two identical copies
validate_email_v1 and validate_email_v2 start byte-identical (copy-pasted), and agree — both incorrectly accept "@b.com" (no local part before the @). A real bug fix is applied to v1 only. Afterward: validate_email_v1_fixed("@b.com") correctly returns False; validate_email_v2("@b.com") — the un-updated copy — still incorrectly returns True. Nothing broke, no error was raised; the two copies simply, silently disagree now.
This is the same bug Software Architecture Fundamentals already verified, one course apart
That course's own Chapter 8 measured two independently-implemented pricing functions diverging by a real $0.50 for the identical order. This chapter's finding is the same failure mode, caught earlier — while the two copies are still identical, before real business logic has had a chance to diverge on its own.

Dead Code: Verified, Not Assumed

Verified directly — one function is genuinely referenced; the other genuinely isn't
Searching every other function's own source in a small codebase for a call to calculate_shipping(: found, True. Searching the identical codebase for a call to calculate_shipping_legacy( — a function that exists, has a plausible name, and could easily be mistaken for still in use — finds nothing: False. It's not that this function is hard to find a use for; a real, mechanical search across the actual codebase confirms there isn't one.

Speculative Generality: A Hook Nobody Ever Pulls

def calculate_price(base_price, discount_strategy=None): # 'in case we need custom strategies later' if discount_strategy is None: return base_price return discount_strategy(base_price)
Verified directly — every real call site ignores the flexibility that was built for it
Checking all 4 real call sites in a small codebase for whether any of them actually pass a custom discount_strategy: 0 do. The parameter exists, adds a branch to read and reason about, and has never once been exercised with anything other than its own default.
Why "might need it later" isn't evidence on its own
Design Patterns Chapter 10's own capstone named this exact risk directly: a pattern (or, here, a parameter) applied for a hypothetical future need is over-engineering unless that need is real. This chapter's own finding makes the test concrete — if zero real call sites use the flexibility after real code has been written, the generality was speculative, not forward-thinking.

Where This Connects

This chapter's findingWhat it connects to
Feature Envy's own zero-self-reference measurementSoftware Architecture Fundamentals Chapter 5's own coupling/cohesion criteria — the identical underlying question, applied to methods instead of services
A private-field rename breaking one audit and not the otherSoftware Architecture Fundamentals Chapter 7's own dependency inversion finding — depending on a public interface instead of a private implementation detail is the same discipline, one level down
Duplicated code diverging silently after one fixSoftware Architecture Fundamentals Chapter 8's own $0.50 thick-client discrepancy — the same risk, caught here before real logic had drifted apart

Hands-On Exercises

Exercise 1

Write a second method on InvoicePrinter, print_item_count(order), that returns len(order.items). Verify whether this new method also shows Feature Envy by this chapter's own self-vs-order reference count, and explain whether moving it onto Order would be as clearly justified as moving print_invoice's own logic was.

📄 View solution
Exercise 2

Fix this chapter's own duplicated-code problem properly: delete validate_email_v2 entirely, and have every caller use validate_email_v1_fixed instead. Verify there is now only one implementation to keep correct, and confirm both original callers still get the correctly-fixed behavior.

📄 View solution
Exercise 3

Add a fifth call site to this chapter's own calculate_price codebase that does pass a real discount_strategy function. Re-run this chapter's own speculative-generality check across all 5 call sites, and explain whether the parameter is still speculative generality once it has a genuine use.

📄 View solution

Chapter 5 Quick Reference

  • Feature Envy, verified: a method with 0 references to its own class's data and at least 1 to another class's — fixed by moving the logic to the class it actually depends on
  • Inappropriate Intimacy, verified: a private-field rename broke a class reaching directly into it; a class using only the public interface survived unaffected
  • Duplicated Code, verified: two identical copies silently diverged after only one received a real bug fix — the same failure mode Software Architecture Fundamentals verified at a larger scale
  • Dead Code, verified: a real codebase-wide search confirmed one function referenced, one genuinely not
  • Speculative Generality, verified: a flexibility hook never once used with a non-default value across 4 real call sites
  • Next chapter: SOLID I — Single Responsibility & Open/Closed, formalizing several of these smells' own fixes