CH.05 L02 · COMPONENT DESIGN + SOLID

Software Design & Architecture

Component Design & the SOLID Principles

This is where detailed design gets its hands dirty: defining the internal classes of each component, then using five battle-tested principles — SOLID — to stop that internal design from rotting the moment requirements change.

Chapter 05 · Lecture 02 Builds on Lecture 01 — Detailed Design overview Covers SRP · OCP · LSP Next lecture ISP · DIP · Internal Behavior
01

Overview of Component Design

Component design (a.k.a. component-level design) is the detailed-design task of defining the internal structure and behavior of the components that were already identified during architecture.

DURING ARCHITECTURE

📦 Abstract components

Architecture defines high-level components and interfaces — e.g. a ClientSystem talking to a ServerSystem through a ServerInterface. You know the boxes and the arrows, not the code.

«component»
ClientSystem
– – – ○ – – –
«component»
ServerSystem
DURING DETAILED DESIGN

🧩 Concrete classes

Component design refines those same boxes into classes, interfaces, and types — enough detail that a developer could start typing code immediately.

EX — ClientSystem becomes a real Client class; ServerSystem becomes a ConcreteServer class that implements a ServerInterface.

🎯 Why this task matters most

During component design, the internal data structures, algorithms, interface details, and communication mechanisms for every component get defined. That's why this single task provides the most significant mechanism for determining the functional correctness of the whole system — and it lets you evaluate alternative solutions before construction even begins, when changes are still cheap.


Before you can design components in OO, you need four building blocks fluent in your fingers: classes/objects, abstract classes & interfaces, inheritance, and polymorphism. The refresher below is deliberately compressed — you've seen this in programming courses, this is just the "why it matters for design" angle.

02

OOP Refresher — the Four Tools

Four concepts, one mnemonic: C.A.I.P.Classes → Abstract classes/Interfaces → Inheritance → Polymorphism. Each one exists to let you write code against a promise instead of a specific implementation.

A
Abstract ClassesA partial blueprint — some methods done, some left for subclasses
+

A class with the abstract keyword. It must have at least one abstract method (no body) but can also have fully-implemented concrete methods. It cannot be instantiated — you can only inherit from it.

  • Offers default functionality subclasses get for free (concrete methods)
  • Forces subclasses to fill in the blanks (abstract methods)
  • Can hold real state: private/protected/public, final, static, and normal instance fields
abstract class Animal {
    // concrete — has a body, inherited as-is or overridden
    public void eat() { System.out.println(name + " is eating.."); }

    // abstract — no body, subclass MUST implement it
    public abstract void makeSound();
}

class Dog extends Animal {
    @Override
    public void makeSound() { System.out.println(name + " says: Woof!"); }
}
EX — new Animal(...) is illegal (abstract), but new Dog(...) is fine — it inherited eat() for free and was forced to implement makeSound().
I
InterfacesA pure contract — 100% abstract, no implementation at all
+

An interface is a special kind of abstract class: a blueprint with zero concrete methods. It cannot be instantiated, never holds instance variables (only public static final constants), and any class that implements it can be instantiated as long as it fills in every method.

Feature Abstract Class Interface
Methods Abstract + concrete Originally abstract only
Variables Instance variables allowed Only constants
Inheritance Extend only one Implement multiple
Coupling Tighter — carries implementation Looser — signatures only
WHY BOTHER — Interfaces define a contract ("anyone who implements me must provide these methods"), give you multiple inheritance of type, and achieve loose coupling: code depends on the interface, not on any one concrete implementation, so it's easy to extend.
H
Inheritance"is-a" — reuse a parent's shape, specialize its behavior
+

UML calls this a Generalization relationship (hollow triangle arrow pointing to the parent). A subclass automatically gets every field and method of its superclass, and can override any of them.

public class Animal {
    public void sound(){ System.out.println("Animal is making a sound"); }
}
public class Cat extends Animal {
    @Override
    public void sound(){ System.out.println("Miaw"); }
}
P
PolymorphismOne reference type, many possible run-time behaviors
+

You can point a variable of the parent type at a child object, and calling an overridden method runs the child's version — decided at runtime, not compile time.

Animal ah = new Horse();  // declared as Animal, actually a Horse
ah.sound();               // runs Horse's override → "Neigh"
Animal ac = new Cat();
ac.sound();               // runs Cat's override → "Miaw"
WHY IT MATTERS FOR DESIGN — polymorphism is the mechanism that makes OCP (section 4) possible: you can add a brand-new subclass and the code that calls it through the parent/interface type never has to change.
03

SOLID — and the Single Responsibility Principle

When designing at the component level, five principles keep designs from becoming complex, hard to reuse, and painful to change. Together they spell SOLID.

SSingle Responsibility
OOpen-Closed
LLiskov Substitution
IInterface Segregation
(next lecture)
DDependency Inversion
(next lecture)
🧠 Mnemonic — S.O.L.I.D. → "Some Old Libraries Inspire Developers" — Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
SRP

1️⃣ Single Responsibility Principle — "one reason to change"

"A module should be responsible to one, and only one, user or stakeholder." A class should do only one job. If it does more than one, every unrelated reason-to-change forces you to open a class that has nothing to do with that change.

ANALOGY — A restaurant: the Chef only cooks, the Waiter only serves, the Cashier only handles money. Force the Waiter to also cook and handle money, and the whole restaurant becomes chaotic the moment any one of those jobs changes.
❌ VIOLATES SRP

The bloated Student class

class Student {
    void storeData()      { /* save student data */ }
    void calculateGrades(){ /* calculate grades  */ }
    void printReport()    { /* print report      */ }
}

This class has three reasons to change: storage format, grading logic, and report format. Switch from paper reports to PDF? Open this class. Switch the database from MySQL to MongoDB? Open this same class again.

✅ APPLIES SRP

Three focused classes

class StudentData    { void storeData() {…} }
class GradeCalculator { void calculateGrades() {…} }
class ReportPrinter   { void printReport() {…} }

Each class now has one job = one reason to change → easier to maintain, test, and extend independently.

📧 SRP worked example — Person + email validation

A Person class that validates its own email is doing two jobs: being a person, and knowing the rules of a valid email address (that's not "person behavior").

BEFORE — Person has name, surname, email, and its own validateEmail() regex check baked in.
AFTER — Pull email validation into its own Email class. Person now just holds a name, surname, and an Email object — it no longer needs to know anything about regex rules.

Giving a class a single responsibility (for a single actor/stakeholder) makes it, by default, easier to see what it does and easier to extend or improve later.

04

The Open-Closed Principle (OCP)

"Software designs should be open to extension, but closed for modification." Code that already works should stay untouched. New requirements get satisfied by adding new code, not by editing old code that was already tested and shipped.

🎮 Worked example — a gaming system

A fictional game has several types of terrestrial (land) characters, and new character types are expected in the future.

// v1 — violates OCP
class TerrestrialCharacter {
    virtual void draw() { /* draw the terrestrial character */ }
    virtual void run()  { /* make the character run */ }
}
class GameEngine {
    void add(TerrestrialCharacter* pCharacter) {
        pCharacter->draw();
        pCharacter->run();
    }
}
WHY THIS VIOLATES OCP — GameEngine depends directly on the concrete class TerrestrialCharacter. Add a new AerialCharacter that needs to fly() instead of run(), and you're forced to modify GameEngine.add(...) to branch on the character's type. That's a modification to code that already worked — a straight-up OCP violation.

🛠️ The fix — encapsulate the variation behind an interface

Design principle at play: Encapsulate Variation — find the part of the design that's likely to change (how a character moves) and hide it behind polymorphism.

// v2 — adheres to OCP
public interface Character {
    void draw();
    void move();  // generalized: run, fly, swim, etc.
}
public class TerrestrialCharacter implements Character {
    public void draw() { System.out.println("Drawing terrestrial character..."); }
    public void move() { System.out.println("Running on the ground..."); }
}
public class AerialCharacter implements Character {
    public void draw() { System.out.println("Drawing aerial character..."); }
    public void move() { System.out.println("Flying in the sky..."); }
}
public class GameEngine {
    public void add(Character character) {
        character.draw();
        character.move();  // engine code never branches on type
    }
}
✅ NEW REQUIREMENT ARRIVES — Need an AquaticCharacter that swims? Just write a new class that implements Character. GameEngine is never touched again — extended, not modified.
public class AquaticCharacter implements Character {
    public void draw() { System.out.println("Drawing aquatic character..."); }
    public void move() { System.out.println("Swimming in the water..."); }
}
engine.add(new AquaticCharacter());  // engine.add(...) itself: unchanged
i

One honest caveat about OCP: no design is 100% closed forever — at some point, some code has to be readily editable for any real system. The real goal of OCP is to locate the parts of the software that are likely to vary, and encapsulate + implement those variations through polymorphism, so the rest of the system stays closed.

05

The Liskov Substitution Principle (LSP)

"Objects should be replaceable with instances of their subtypes without altering the correctness of that program." Any class derived from a base class must honor the implied contract between that base class and whatever code uses it.

RULE 1

Signature Rule

If a program is type-correct for the super type, it must remain type-correct for the subtype too — method signatures in the subtype must match the base class (same/compatible parameter and return types).

RULE 2

Methods Rule

Calls to base-class methods must stay valid reasoning even when they actually run overridden subtype code. Subtypes may weaken preconditions (require less) and strengthen postconditions (guarantee more) — never the reverse.

🐦 The classic violation — Bird / Penguin

Base class:

class Bird {
  fly(): void { console.log("Flying"); }
}

❌ Violation:

class Penguin extends Bird {
  fly(): void {
    throw new Error("Penguins can't fly!");
  }
}
WHY IT BREAKS — Any code that does bird.fly() expecting a Bird will crash the instant that Bird is actually a Penguin. Substituting the subtype changed the program's correctness — a direct LSP violation. A Sparrow extends Bird that really flies would be a perfectly valid, LSP-honoring subtype.
EXAMPLE 2

⬛ Square / Rectangle problem

Square is not a proper subtype of Rectangle: a Rectangle's height and width are independently mutable, but a Square's must always change together. A User that thinks it's talking to a Rectangle (call setH() then check the area) gets confused the moment the object is actually a Square.

Lesson: a substitutability violation, even a "simple" one, can pollute an entire architecture with extra defensive-checking mechanisms.

EXAMPLE 3 — CONFORMS

🧾 License / PersonalLicense / BusinessLicense

A Billing app calls license.calcFee() on a License reference. PersonalLicense and BusinessLicense each calculate the fee differently, but Billing never needs to know which one it has.

This example satisfies both OCP (new license types can be added without touching Billing) and LSP (both subtypes are fully substitutable for License).

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