Exam Practice

MCQs, flashcards, UML scenario diagrams, and refactoring examples covering the four pillars, SOLID, GRASP, DRY/YAGNI/KISS, and code refactoring.

Reference text: Object First with Java, Barnes and Kölling, 6th Ed.

Jump to a SOLID principle
S

Single Responsibility Principle

A class should have one main responsibility, one reason to change. When unrelated jobs live in the same class, a change to one job risks breaking the others.

Code Comparison
VIOLATES S
ViolatesS.javajava
class EmployeeManager {
    double calculateSalary(Employee e) {
        // salary logic
        return 0;
    }

    void sendEmail(Employee e, String msg) {
        // email logic
    }

    void printReport(List<Employee> list) {
        // report logic
    }

    void saveToDatabase(Employee e) {
        // database logic
    }
}
FOLLOWS S
FollowsS.javajava
class SalaryCalculator {
    double calculate(Employee e) {
        return 0;
    }
}

class EmailService {
    void send(Employee e, String msg) {
    }
}

class ReportGenerator {
    void print(List<Employee> list) {
    }
}

class EmployeeRepository {
    void save(Employee e) {
    }
}
Architecture Diagram
BEFORE ARCHITECTURE (VIOLATION)
Rendering UML diagram...
AFTER ARCHITECTURE (FOLLOWS S)
Rendering UML diagram...
KEY INSIGHT:

Now changing how salary is calculated cannot accidentally break email sending or report printing, because each concern lives in its own class.

O

Open-Closed Principle

Software entities should be open for extension but closed for modification. You should be able to add new behavior without editing existing, already-tested code.

Code Comparison
VIOLATES O
ViolatesO.javajava
class AreaCalculator {

    double calculateArea(Object shape) {
        if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return Math.PI * c.radius * c.radius;
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.width * r.height;
        }
        // every new shape means editing
        // this method again
        return 0;
    }
}
FOLLOWS O
FollowsO.javajava
interface Shape {
    double area();
}

class Circle implements Shape {
    double radius;

    public double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    double width, height;

    public double area() {
        return width * height;
    }
}

class AreaCalculator {
    double calculateArea(Shape shape) {
        return shape.area();
    }
}
extending later, with zero changes to AreaCalculator
Extension.javajava
class Triangle implements Shape {
    double base, height;

    public double area() {
        return 0.5 * base * height;
    }
}
Architecture Diagram
BEFORE ARCHITECTURE (VIOLATION)
Rendering UML diagram...
AFTER ARCHITECTURE (FOLLOWS O)
Rendering UML diagram...
KEY INSIGHT:

This is the classic use of OCP: AreaCalculator is closed for modification (we never touch it again), yet the system stays open for extension. Adding a new shape, such as Triangle, only means writing a new class that implements Shape.

L

Liskov Substitution Principle

A subtype object should be usable wherever its supertype object is expected, without breaking correctness. If a subclass throws away a promise its superclass made, callers that rely on that promise can break.

Code Comparison
VIOLATES L
ViolatesL.javajava
class Bird {
    void fly() {
        System.out.println("Flying");
    }
}

class Ostrich extends Bird {
    void fly() {
        throw new UnsupportedOperationException(
            "Ostriches cannot fly");
    }
}

// Bird bird = new Ostrich();
// bird.fly(); breaks at runtime
FOLLOWS L
FollowsL.javajava
class Bird {
    void eat() {
        System.out.println("Eating");
    }
}

interface FlyingBird {
    void fly();
}

class Sparrow extends Bird implements FlyingBird {
    public void fly() {
        System.out.println("Flying");
    }
}

class Ostrich extends Bird {
    // no fly() promised, so no
    // broken expectation
}
Architecture Diagram
BEFORE ARCHITECTURE (VIOLATION)
Rendering UML diagram...
AFTER ARCHITECTURE (FOLLOWS L)
Rendering UML diagram...
KEY INSIGHT:

Code that expects any Bird can safely call eat(). Code that expects a FlyingBird can safely call fly(). No subtype is forced to break a promise it cannot keep.

I

Interface Segregation Principle

A class should not be forced to depend on methods it does not need. A large interface with unrelated methods forces every implementer to deal with capabilities it may not have.

Code Comparison
VIOLATES I
ViolatesI.javajava
interface Worker {
    void work();
    void fly();
    void swim();
}

class Robot implements Worker {
    public void work() {
    }

    public void fly() {
        throw new UnsupportedOperationException();
    }

    public void swim() {
        throw new UnsupportedOperationException();
    }
}
FOLLOWS I
FollowsI.javajava
interface Workable {
    void work();
}

interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

class Robot implements Workable {
    public void work() {
    }
}

class Duck implements Workable, Flyable, Swimmable {
    public void work() { }
    public void fly() { }
    public void swim() { }
}
Architecture Diagram
BEFORE ARCHITECTURE (VIOLATION)
Rendering UML diagram...
AFTER ARCHITECTURE (FOLLOWS I)
Rendering UML diagram...
KEY INSIGHT:

Robot only implements Workable, the one capability it actually has. No class is forced to fake or throw exceptions for a method it can never really support.

D

Dependency Inversion Principle

High-level and low-level components should both depend on abstractions, not on each other's concrete details. A high-level class that creates a specific low-level class directly cannot be reconfigured without editing that high-level class.

Code Comparison
VIOLATES D
ViolatesD.javajava
class EmailSender {
    void send(String message) {
        System.out.println("Email: " + message);
    }
}

class NotificationService {
    EmailSender sender = new EmailSender();

    void notify(String message) {
        sender.send(message);
    }
}
FOLLOWS D
FollowsD.javajava
interface MessageSender {
    void send(String message);
}

class EmailSender implements MessageSender {
    public void send(String message) {
        System.out.println("Email: " + message);
    }
}

class SmsSender implements MessageSender {
    public void send(String message) {
        System.out.println("SMS: " + message);
    }
}

class NotificationService {
    MessageSender sender;

    NotificationService(MessageSender sender) {
        this.sender = sender;
    }

    void notify(String message) {
        sender.send(message);
    }
}
Architecture Diagram
BEFORE ARCHITECTURE (VIOLATION)
Rendering UML diagram...
AFTER ARCHITECTURE (FOLLOWS D)
Rendering UML diagram...
KEY INSIGHT:

NotificationService now depends on the MessageSender abstraction, not a concrete class. A new SmsSender can be plugged in through the constructor without changing NotificationService at all.