CH.05 L03 · ISP · DIP · BEHAVIOR

Software Design & Architecture

Finishing SOLID & Designing Behavior

The last two SOLID principles — Interface Segregation and Dependency Inversion — plus how to model a component's internal behavior (not just its structure), and the practical rules & common mistakes that tie the whole detailed-design chapter together.

Chapter 05 · Lecture 03 Builds on Lecture 02 — SRP · OCP · LSP Covers ISP · DIP · Sequence diagrams · Rules of Design Next chapter Creational Design Patterns
00

SOLID So Far

Lecture 02 covered the first three letters. This lecture finishes the acronym.

SSingle Responsibility
✓ done
OOpen-Closed
✓ done
LLiskov Substitution
✓ done
IInterface Segregation
⚡ today
DDependency Inversion
⚡ today
01

The Interface Segregation Principle (ISP)

"Classes/methods should not be forced to depend on methods that they do not use." The ISP implies that many small, client-specific interfaces are better than one general-purpose, bloated interface.

🎮 Worked example — enemy characters

A game has enemy characters that can either roam over land, fly, or swim — plus a SuperCharacter that can do all three. A tempting (but wrong) design puts every ability on one shared interface:

interface EnemyCharacter {
    void fly();
    void run();
    void swim();
}
// AerialCharacter, TerrestrialCharacter, AquaticCharacter, SuperCharacter
// ALL implement EnemyCharacter
WHY THIS IS BLOATED — AerialCharacter only needs fly(), yet is forced to also implement run() and swim() (usually with empty/no-op bodies). Only SuperCharacter actually needs the full interface — everyone else is "polluted" with methods they don't use. This is also an LSP violation: a TerrestrialCharacter can't honestly replace an EnemyCharacter if calling code expects fly() to work.

🛠️ The fix — split into small, specific interfaces

interface SuperCharacter {
    void fly(); void run(); void swim();
}
interface AerialCharacter extends SuperCharacter { void fly(); }
interface TerrestrialCharacter extends SuperCharacter { void run(); }
interface AquaticCharacter extends SuperCharacter { void swim(); }

// Each client implements ONLY the specific interface it needs
class AerialCharacterClient implements AerialCharacter {
    public void fly() { System.out.println("flying"); }
}

Now AerialCharacterClient depends on exactly one method it actually uses. Only a true "does everything" character needs to implement the full SuperCharacter contract.

❌ ISP VIOLATION

Worker — one bloated interface

interface Worker {
    void work();
    void eat();
}
class Robot implements Worker {
    public void work() { System.out.println("Robot working"); }
    public void eat() { }  // ❌ Robot doesn't eat!
}
✅ ISP APPLIED

Split into Workable + Eatable

interface Workable { void work(); }
interface Eatable  { void eat(); }

class Robot implements Workable { // only what it needs
    public void work() { … }
}
class Human implements Workable, Eatable {
    public void work() { … }
    public void eat()  { … }
}
02

The Dependency Inversion Principle (DIP)

The most flexible systems are those where source-code dependencies refer only to abstractions, never to concretions. High-level modules should not depend on low-level modules — both should depend on abstractions. And: abstractions should not depend on details; details should depend on abstractions.

❌ REAL-LIFE BAD IDEA

🔌 A charger that only works with Samsung

Buy a charger that works only with Samsung phones — switch to an iPhone, and the charger is useless.

✅ REAL-LIFE GOOD IDEA

🔌 A USB charger

Buy a charger built for the USB abstraction — any phone that speaks USB can use it: Samsung, iPhone, anything. USB is the interface (abstraction).

❌ VIOLATES DIP

Switch depends on concrete Fan

class Fan {
    void turnOn() { System.out.println("Fan is ON"); }
}
class Switch {
    Fan fan = new Fan();  // directly depends on Fan
    void press() { fan.turnOn(); }
}

Want a Switch that controls a Light instead? You have to modify Switch.

✅ APPLIES DIP

Switch depends on a Device interface

interface Device { void turnOn(); }
class Fan   implements Device { public void turnOn(){…} }
class Light implements Device { public void turnOn(){…} }

class Switch {
    Device device;
    Switch(Device device) { this.device = device; }
    void press() { device.turnOn(); }
}
// usage:
Switch s1 = new Switch(new Fan());
Switch s2 = new Switch(new Light());

👷 Manager / Worker example

❌ Bad — tightly coupled:

class Manager {
    Worker worker;  // fixed to ONE type
    void setWorker(Worker w){ worker = w; }
    void manage(){ worker.work(); }
}

✅ Good — depends on abstraction:

interface IWorker { void work(); }
class Manager {
    IWorker worker;  // any worker type!
    void setWorker(IWorker w){ worker = w; }
    void manage(){ worker.work(); }
}
PAYOFF — Manager can now handle RobotWorker, AIWorker, InternWorker — anything implementing IWorker — without a single change to Manager itself.
03

Modeling the Internal Behavior of a Component

Component design models two different things, with two different diagrams: internal structure → a class diagram. Internal behavior (how objects talk to each other over time) → a sequence diagram.

sm : ScheduleManager
1: timer expires
2: getMsg()
‹‹return›› msgId
4: interpretMsg()
s : Schedule
HeatSensor : Sensor
5: activate()  [if msgId == HEAT_MSG_ID]
VibrationSensor : Sensor
6: activate()  [if msgId == VIBRATE_MSG_ID]

Reading this diagram: a timer expires → ScheduleManager asks the Schedule for the next message → interprets the message id → based on an alt (alternative) fragment, activates either the heat sensor or the vibration sensor, never both. This is exactly the kind of internal timing/collaboration detail a class diagram cannot show on its own.

04

Rules of Design

A checklist of practical habits that make any detailed design — not just OO ones — more likely to succeed.

01Make sure the problem is well-defined

All design criteria, requirements, and constraints should be known before design starts.

02What comes before how

Define the functionalities to be performed at every level of abstraction before deciding which structures implement them.

03Separate orthogonal concerns

Do not connect what is independent.

04Design external functionality before internal

Treat the solution as a black-box first — decide how it interacts with its environment — then design what's inside it, recursively.

05Keep it simple

Fancy designs are buggier than simple ones — harder to implement, verify, and often less efficient. Complex problems are usually just simple problems huddled together.

06Work at multiple levels of abstraction

Good designers move between high-level view and low-level detail quickly and easily.

07Design for extensibility

A good design is "open-ended" — easy to extend. Don't introduce what's immaterial; don't restrict what's irrelevant.

08Use rapid prototyping when applicable

Before fully implementing a design, build a high-level prototype and verify the design criteria are met.

09Details should depend on abstractions

Abstractions should not depend on details (this is DIP, restated as a general design rule).

10Classes in one component change together

Classes within a released component should share common closure — if one needs to change, they all likely need to change too.

11Classes in one component are reused together

It should be impossible to reuse less than the whole component — you reuse the total, not partially.

12Dependencies run toward stability

The dependee must be more stable than the depender. The more stable a component is, the more it should consist purely of abstract classes — a completely stable component is nothing but abstractions.

05

Common Design Mistakes

Depth-first design

Diving deep into one part of the design before the rest is even sketched out — only partially satisfies the requirements. Experience is the best cure for this.

Directly refining the requirements specification

Jumping straight from raw requirements text into design details leads to overly constrained, inefficient designs.

Failure to consider potential changes

Always design for extension and contraction — assume today's requirements are not the final ones.

Making the design too detailed

Over-specifying now over-constrains the implementation later, removing flexibility the developer should have.

06

Quick-Fire Flashcards

Tap a card to flip it. Good for a 5-minute recap right before an exam.

07

Practice Questions

Self-check practice written from this lecture's content — not official past-paper questions.

08

One-Screen Recap