Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 5

    Code Smells That Beg for Patterns

    Code smells are Fowler's term for surface symptoms of deeper design problems.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Code smells are Fowler's term for surface symptoms of deeper design problems. They are not bugs — the code works. They are warnings that the next change will be expensive. A skilled engineer reads smells the way a doctor reads symptoms: each one points to a likely diagnosis, and each diagnosis suggests a specific treatment.

    This lesson catalogs the twelve smells most likely to point at a pattern refactor — Long Method, Switch Statement, Feature Envy, Shotgun Surgery, and more — and maps each to the pattern that resolves it. The map is not one-to-one; judgment still applies. But smells radically narrow the candidate list.

    The story

    A three-engineer team owned a 4,200-line OrderProcessor. Every new feature touched it; every release had bugs. They ran a smell audit in one afternoon, listed eight distinct smells, and mapped each to a refactor. Six weeks later the file became eleven focused classes — Strategy for pricing, State for order status, Observer for notifications, Command for actions. Defect rate dropped 70%. The code did not fail before the audit. The smells predicted that it would.

    The business problem

    Ignoring code smells does not cause immediate outages — it causes slow, expensive change that compounds:

    • Merge conflict tax: god-files with twelve contributors block every parallel feature branch.
    • Bug recurrence: the same conditional branch breaks every release because change ripples unpredictably.
    • Review paralysis: reviewers cannot reason about 400-line methods — bugs slip through.
    • Refactor fear: teams avoid touching smelly modules, so workarounds multiply around them.
    • Onboarding cliff: new engineers need weeks to understand one file that should be eleven.

    The problem teams faced

    These structural symptoms appear in almost every codebase that outgrew its first design:

    • Long Method — 50+ lines hiding multiple responsibilities reviewers cannot hold in memory.
    • Switch on Type — growing if/else or switch chains signal missing polymorphism (Strategy, State).
    • Shotgun Surgery — one business change requires editing eight files; missing Facade or Mediator.
    • Feature Envy — a method reads another class's data more than its own; behavior is misplaced.
    • Divergent Change — one class changes for unrelated reasons; violates Single Responsibility.
    • Speculative Generality — abstractions built for futures that never arrived (see previous lesson).

    Understanding the topic

    Four-step smell reading. For every smell you spot, walk this chain before touching code:

    • 1. Name the smell — use Fowler's vocabulary so the team shares diagnosis language.
    • 2. Diagnose the force — what design problem does this smell usually mean?
    • 3. Pick the refactor move — Extract Method, Move Method, Replace Conditional with Polymorphism.
    • 4. Reach for the pattern — only when the refactor move points to a recurring variation axis.

    Internal architecture

    Smell → pattern reference map — bookmark this for code review and smell audits:

    text
    Smell → Likely pattern / refactor
    ──────────────────────────────────────────────────────────────
    Switch on type → Strategy / State / Visitor
    Long Method → Extract Method → Command
    Feature Envy → Move Method → Mediator
    Shotgun Surgery → Facade / Mediator
    Data Clumps → Value Object / Builder
    Primitive Obsession → Value Object
    Divergent Change → Single Responsibility / Strategy
    Parallel Inheritance → Bridge
    Inappropriate Intimacy → Mediator / Facade
    Speculative Generality → Delete abstraction (not a pattern!)
    Duplicate Code → Extract Method → Template Method

    Visual explanation

    Three diagrams guide smell-driven refactoring. The first is the diagnostic pipeline. The second maps the most common smells to likely patterns. The third shows how to prioritize — fix high-churn hotspots first, not the prettiest violations.

    Smell → pattern diagnostic pipeline
    Code works
    Tests pass
    Spot smell
    Name it
    Diagnose force
    What varies?
    Apply pattern
    If evidence exists
    Smells predict fragility — they are not bugs. Fix before the next expensive change.
    Top smells → likely patterns
    Switch on type
    Strategy · State
    Shotgun surgery
    Facade · Mediator
    Feature envy
    Move Method
    Divergent change
    SRP · Strategy
    Not one-to-one — always verify the variation axis exists before applying the pattern.
    Prioritize smells by impact
    Git churn
    Change frequency
    Team touch count
    Merge conflicts
    Impact score
    churn × contributors
    Fix top 3
    This quarter
    A 200-line file nobody touches is fine. A 50-line file eight people edit weekly is urgent.

    Informative example

    Smell: Switch on Type — every new carrier adds another case; merge conflicts guaranteed:

    ts
    // ❌ Switch on type — smells like missing Strategy
    function ship(order: Order, carrier: string) {
    switch (carrier) {
    case "fedex": return fedexApi.createShipment(order);
    case "ups": return upsApi.createShipment(order);
    case "dhl": return dhlApi.createShipment(order);
    default: throw new Error(`Unknown carrier: ${carrier}`);
    }
    }
    // ✅ Strategy — new carrier = new class, zero edits to ship()
    interface CarrierStrategy {
    createShipment(order: Order): Promise<ShipmentId>;
    }
    function ship(order: Order, carrier: CarrierStrategy) {
    return carrier.createShipment(order);
    }
    ts
    // ❌ Feature envy — InvoicePrinter knows too much about Order internals
    class InvoicePrinter {
    print(order: Order) {
    return `${order.customer.name} · ${order.lineItems.length} items · ${order.total.cents}`;
    }
    }
    // ✅ Move Method — behavior lives with the data it uses
    class Order {
    invoiceSummary() {
    return `${this.customer.name} · ${this.lineItems.length} items · ${this.total.cents}`;
    }
    }

    Smell: Feature EnvyInvoicePrinter reads five fields from Order that belong on Order itself. Fix with Move Method first; reach for Mediator only if coordination spans three or more classes.

    Execution workflow

    1Smell audit workflow
    1 / 5

    Audit the file

    Read the suspect module top-to-bottom; list smells by Fowler's names.

    Timebox to 90 minutes — perfection is the enemy of starting.

    Real-world use

    SonarQube, CodeClimate, and ESLint flag many smells automatically — but tools catch syntax-level signals, not domain-level ones. A 200-line SQL builder may trigger "Long Method" falsely; a 40-line method with switch-on-type may pass every metric. Mature teams combine tool output with manual smell audits before major feature work. Treat smells as a backlog item ranked by git churn, not aesthetic offense.

    Production case study

    A logistics platform's routing module had 3,800 lines, 42 if-statements, and eighteen contributors:

    • Symptoms: every feature triggered merge conflicts; bug-fix lead time averaged 9 days.
    • Smell audit: Switch on Type (carrier), Shotgun Surgery (notifications), Feature Envy (pricing).
    • Refactor plan: Strategy for carriers, Observer for notifications, Move Method for pricing — four weeks with characterization tests.
    • Outcome: one god-file became eight focused classes; merge conflicts dropped 80%; bug lead time fell to 2 days.
    • Lesson: name smells in the PR that triggered the audit — depersonalize feedback, accelerate consensus.

    Trade-offs

    • Pro — Diagnostic vocabulary: smells turn vague "this is ugly" into actionable backlog items.
    • Pro — Pattern selection: each smell narrows candidates to two or three patterns.
    • Pro — Review consistency: cite smell names in PR comments instead of personal taste.
    • Con — False positives: Long Method is sometimes correct (parsers, SQL builders, flat config).
    • Con — Smell fatigue: auditing everything demoralizes — pick three high-impact smells per quarter.
    • Con — Cosmetic fixes: renaming without fixing the underlying force wastes a sprint.

    Decision framework

    • Audit before major features in modules with high git churn — refactor first, then build.
    • Audit after incidents — postmortems often trace to a smell the team ignored for months.
    • Cite smells in code review — "Switch on Type here" depersonalizes feedback.
    • Do not witch-hunt — chasing every smell paralyzes delivery.
    • Do not refactor without tests — characterization tests on legacy code first, or you are gambling.
    • Do not refactor code being deleted — smell audits on modules with six-month sunset are waste.

    Best practices

    • Smells fixed by refactor moves alone: Duplicate Code → Extract Method; Long Parameter List → Introduce Parameter Object — no pattern needed.
    • Smells that need patterns: Switch on Type with 3+ variants; Shotgun Surgery across 5+ files; Divergent Change with unrelated change reasons.
    • Prioritize: impact score = git churn × number of contributors touching the file.
    • Run `git log --follow -p -- path/to/file` before debating — evidence ends arguments.
    • Document smell → pattern decisions in ADRs so the team inherits the diagnosis.
    • Celebrate smell audits like features — schedule them, timebox them, measure outcomes.

    Anti-patterns to avoid

    • Splitting a readable 200-line flat sequence into fifteen one-liner methods "to fix Long Method."
    • Introducing Strategy when two variants exist and the second is a one-line edge case.
    • Running SonarQube cleanup sprints without prioritizing by business impact.
    • Calling something a smell because you dislike the author's style — smells are structural, not personal.

    Common mistakes

    • Refactoring during unrelated feature work without a dedicated time budget.
    • Skipping characterization tests — smells become incidents mid-refactor.
    • Fixing smells in modules about to be replaced by a vendor SDK or microservice extraction.
    • Assuming every smell needs a GoF pattern — many need a simple Extract Method.

    Debugging tips

    • If refactor did not reduce churn, the seam was wrong — re-run git log and find the real change axis.
    • When tests fail after Extract Class, the class boundary split behavior from data incorrectly.
    • High cyclomatic complexity in one method often means Switch on Type — count the branches.
    • Use `git shortlog -sn -- path` to see who touches a smelly file — prioritize shared pain.

    Optimization strategies

    • Batch smell fixes in one module per sprint — context switching kills refactor momentum.
    • Pair smell audits — one person reads, one person names smells aloud; catches blind spots.
    • Keep a team smell backlog in your issue tracker, tagged separately from bugs.
    • For interviews, prepare one story: smell you spotted → diagnosis → pattern → measurable outcome.

    Common misconceptions

    • Many developers believe smells mean the code is broken. In reality, smells mean the next change will be expensive — tests still pass.
    • Many developers believe every Long Method must be split. In reality, cyclomatic complexity and change frequency matter more than line count.
    • Many developers believe tools replace manual smell reading. In reality, domain smells (Feature Envy, Divergent Change) need human judgment.
    • Related: Fowler's Refactoring catalog — the refactor moves; patterns are the destinations.

    Advanced interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    5 questions
    1IntermediateQuestionName three code smells and the patterns they typically lead to.+

    Answer

    Switch on Type → Strategy or State; Shotgun Surgery → Facade or Mediator; Feature Envy → Move Method (then possibly Mediator). Frame as smell → diagnosis → destination.

    Follow-up

    Which smell did you fix most recently in production?
    2AdvancedQuestionWhen is a long method actually fine?+

    Answer

    When it is a flat sequence with low cyclomatic complexity — SQL builders, parsers, configuration loaders. Long is not always bad; branching and repetition are the real signals.

    Follow-up

    What metric do you watch besides line count?
    3AdvancedQuestionHow do you prioritize which smells to fix?+

    Answer

    Impact score = change frequency × team size touching the file. Use git churn and shortlog. A untouched 200-line method is low priority; a 50-line method eight people edit weekly is urgent.

    Follow-up

    Show the git commands you would run.
    4AdvancedQuestionHave you refactored toward a pattern and made things worse?+

    Answer

    Honest answer: introduced Strategy with only two variants where the second was a one-line edge case. Added three files of indirection for one saved line. Reverted next sprint when churn did not improve.

    Follow-up

    What signal told you to revert?
    5BeginnerQuestionWhat is the difference between a smell and a bug?+

    Answer

    A bug breaks behavior now; a smell warns the next change will be expensive. Smells do not fail tests but predict fragility. Track both in separate backlogs.

    Follow-up

    Where would you store the smell backlog?

    Summary

    You can now read code smells like symptoms: name them, diagnose the force, match to a refactor or pattern, and prioritize by impact. Tell the OrderProcessor story and walk through the Switch-on-Type example — if you can do that without notes, you own this lesson.

    Ready to mark this lesson complete?Track your journey across the entire course.