Design Smells (Java)

Context: FIT2099_MOC · a catalogue of surface indicators (Fowler) that a design has a deeper problem · the diagnosis layer that refactoring then treats · leans on SOLID, polymorphism and composition Problem it solves: given a smelly class/method, name the smell and choose the refactoring step that removes it.

Quick Revision

  • 🎯 Trigger: code that works but is hard to read, change, or extend ➔ identify the smell family, apply the matching refactoring.
  • ⚡ Key Constraint: smells are subjective heuristics (“might be wrong”, vary by language) — a smell is a hint to investigate, not proof of a bug. Branching on type information (instanceof/switch(type)) is the single most exam-relevant smell ➔ its fix is almost always Replace Conditional with Polymorphism.

📝 What a smell is

  • Code smell (Fowler) ➔ a surface symptom in the source that usually corresponds to a deeper design weakness; “if it stinks, change it.”
  • Subjective + contextual ➔ Fowler’s taxonomy is a guide, admits it “might be wrong”; something smelly in Java may be fine in C#. Experienced programmers know what stinks.
  • Smell ≠ failing code ➔ the program runs; the cost is maintainability (understanding, changing, extending), not correctness.

🧭 The smell families (with fix)

FamilySmellSymptomRefactoring step
BloatersLong Methodmethod too long to graspExtract Method; Decompose Conditional; Replace Method with Method Object
BloatersLarge / God Class”catch-all” class, likely breaks SRPExtract Class / Extract Subclass / Extract Interface
BloatersLong Parameter List args to a methodpass the object itself; Introduce Parameter Object
BloatersData Clumpssame group of fields recurs everywhereExtract Class / Introduce Parameter Object
Change PreventersDivergent Changeone class changed for many unrelated reasonsExtract Class to split it
Change PreventersShotgun Surgeryone change forces edits across many classesMove Method / Move Field to consolidate; remove redundant classes
CouplersFeature Envya method uses another object’s data more than its ownMove Method (or Extract Method) to where the data lives
CouplersMessage Chainsa.getB().getC().getD() — client navigates the whole structureHide Delegate; move/extract to chain start
CouplersMiddle Manclass only delegates, does nothing itselfRemove Middle Man (the class shouldn’t exist)
ProceduralPrimitive Obsessionprimitives/Strings instead of classes; hard to validategroup primitives into their own class; Replace Array with Object
ProceduralSwitch/Type Checkingswitch/if cascade on a type field, repeated in many placesReplace Conditional with Polymorphism (or Replace Type Code with State/Strategy)
ProceduralData Classclass with fields + getters/setters, no behaviourencapsulate public fields; Move Method — put logic on the data
DispensablesDuplicated Codesame code in several places (breaks DRY)Extract Method / pull up / Form Template Method
OverengineeringSpeculative Generalitymachinery “for the future” that is never usedCollapse Hierarchy / Inline Class / remove unused params
OverengineeringLazy Classclass that no longer earns its keepInline Class

Divergent Change vs Shotgun Surgery (classic exam trap): they are opposites. Divergent Change = one class, many reasons to change (change is contained but tangled). Shotgun Surgery = one reason, many classes to change (change is scattered). Both are Change Preventers.

🔧 Worked examples (smell ➔ fix)

1. Data Clumps / Data Class ➔ Extract Class

// SMELL: {year, month, day} clump repeats; DateUtil is a bloated procedural util
class DateUtil {
    boolean isAfter(int y1,int m1,int d1, int y2,int m2,int d2) { /*...*/ }
    int differenceInDays(int y1,int m1,int d1, int y2,int m2,int d2) { /*...*/ }
}
// FIX: extract a Date class; the clump becomes ONE parameter.
// Best: give Date the behaviour so it isn't a lifeless Data Class.
class Date {
    int year, month, day;
    boolean isAfter(Date other) { /*...*/ }          // logic lives WITH the data
    int differenceInDays(Date other) { /*...*/ }
}

Expected output: call sites read d1.isAfter(d2) not a 6-int soup; a class with only fields + no methods is a Data Class — push the operating logic onto it.

2. Type Checking ➔ Replace Conditional with Polymorphism

// SMELL: switch on a type field — the same cascade will reappear elsewhere
class Pet {
    private String type;
    String makeSoundInSpanish() {
        switch (type) {
            case "cat": return "miau miau";
            case "dog": return "guau guau";
            default:    throw new IllegalStateException();
        }
    }
}
// FIX: one subclass per type; the language's dynamic dispatch IS the switch
abstract class Pet { abstract String makeSoundInSpanish(); }
class Cat extends Pet { String makeSoundInSpanish() { return "miau miau"; } }
class Dog extends Pet { String makeSoundInSpanish() { return "guau guau"; } }

Expected output: adding a Bird = one new subclass, no existing method edited (OCP); the instanceof/switch(type) disappears. This is why “branching on type information is a code smell” — see LSP.

3. Shotgun Surgery ➔ Extract Method (consolidate the rule)

// SMELL: the same guard appears in withdraw(), transfer(), processFees()...
if (this.balance < MINIMUM_BALANCE) { this.notifyAccountHolder(); return; }
// FIX: one private query owns the rule; every method calls it
private boolean isAccountUnderMinimum() { return this.balance < MINIMUM_BALANCE; }
// if (isAccountUnderMinimum()) { notifyAccountHolder(); return; }

Expected output: the threshold rule now changes in one place, not three — one reason, one edit.

🏛️ Structure (Divergent Change / Primitive Obsession ➔ composition)

classDiagram
    class Hero {
        -Integer stamina
        -Integer health
        -Armour armour
        +defense()
        +attack()
    }
    class Armour {
        -Integer health
        -Integer status
        -String rarity
        +getHealth() Integer
    }
    class SavingsAccount {
        -double balance
        -Address address
        -MedicareInfo medicare
    }
    class Address {
        -String streetName
        -int zipCode
        -String city
    }
    class MedicareInfo {
        -int medicareNumber
        -Date medicareValidTo
    }
    Hero *-- Armour
    SavingsAccount *-- Address
    SavingsAccount *-- MedicareInfo

(Armour fields that always changed together are Extracted out of Hero; the SavingsAccount primitive soup is grouped into Address + MedicareInfo. Effect: ↓ coupling to raw fields, ↑ cohesion per class, ↑ extensibility.)

✍️ Practice

⚠️ Common Mistakes

  • 💡 Smell ≠ certainty ➔ Fowler’s list is subjective and language-dependent; treat a smell as “investigate here”, never as an automatic rewrite trigger — over-reacting causes Speculative Generality.
  • 💡 Divergent Change ⇄ Shotgun Surgery mix-up ➔ remember: Divergent = one class, many reasons; Shotgun = one reason, many classes. Getting the direction wrong loses the mark.
  • 💡 Type-branching is the flagged smellswitch(type) / instanceof cascades ➔ Replace Conditional with Polymorphism; leaving them breaks OCP and duplicates the cascade.
  • 💡 Data Class vs good encapsulation ➔ a pure field-bag with getters/setters is a smell, not automatically fine — the goal is behaviour with the data, not anaemic structs manipulated by others.