☕
Core Java Roadmap Phase 17
0 / 40 (0%)
☰ Syllabus Index Next: P18 →

SOLID Design Principles, GoF Patterns & Architecture

📂 Repository Module: Module 5_ Architecture and Design Patterns \ 01_Solid_Principles_Demos
📖 Textbook Alignment: Volume I & II Best Engineering Practices & Design Patterns
Phase 17

SOLID Design Principles, GoF Patterns & Production Architecture

📂 Module 5_ Architecture and Design Patterns \\ Module 3_ Unit Testing with Junit 5
📖 Core Java Textbook Alignment: Volume I & II Best Engineering Practices • Clean Architecture Standards

15.1 The 5 SOLID Design Principles Explained with Real-World Analogies

📖 Cay S. Horstmann • Core Java Volume I & II — Engineering Best Practices & Design Patterns
“Favor composition over inheritance, and program to an interface, not an implementation. Patterns like Factory Method, Builder, Strategy, and Observer allow code to remain open for extension but closed for modification.”
  • Singleton DCLThe instance field MUST be volatile to prevent CPU instruction reordering.
⚡ Pro-Tip: The 5 SOLID Laws Acronym
S: Single Responsibility | O: Open/Closed | L: Liskov Substitution | I: Interface Segregation | D: Dependency Inversion
Principle Real-World Analogy Code Smell (Violation) Clean Architecture Solution
S - Single Responsibility A Swiss Army Knife with 50 tools is bulky and hard to hold. A chef uses a dedicated kitchen knife for cutting, and a corkscrew for wine. An Invoice class that calculates tax, prints PDF, AND executes SQL database inserts. Split into Invoice (data), InvoiceCalculator, InvoicePdfExporter, and InvoiceRepository.
O - Open/Closed Your phone allows you to install new apps (open for extension) without having to open the phone casing and resolder microchips (closed for modification). Massive if-else or switch statements checking if (paymentType == "PAYPAL") ... else if ("CARD"). Strategy Pattern: Interface PaymentStrategy with pluggable implementations.
L - Liskov Substitution If it looks like a duck and quacks like a duck, but needs batteries, you probably have the wrong abstraction! Square extends Rectangle where setWidth() unexpectedly alters height and breaks existing rectangle logic. Do not force inheritance. Make Shape interface with getArea().
I - Interface Segregation A TV remote control shouldn't force you to carry buttons for controlling an airplane cockpit. A giant "fat" interface with 40 methods forcing classes to implement empty dummy methods with throw new UnsupportedOperationException(). Split into small, focused, role-specific interfaces: Printable, Scanable, Faxable.
D - Dependency Inversion Your lamp plugs into a standard electrical wall socket (abstraction). It doesn't solder its wires directly to the regional electrical power plant (concrete detail). High-level OrderService writing new MySQLDatabase() directly inside its constructor. Inject interface PaymentGateway via constructor (Spring Inversion of Control & Dependency Injection).
📂 Module 5.../SRPDemoSolution.java — Clean SRP Implementation
package com.accenture.ltt.solid;

/**
 * Demonstrates SRP: Single responsibility of tax and total calculation.
 * Persistence and notifications are handled by dedicated services.
 */
public class InvoiceCalculator {
    public double calculateTotal(Order order) {
        return order.getItems().stream()
                    .mapToDouble(Item::getPrice)
                    .sum() * 1.18; // 18% GST tax rate
    }
}
👁 Single Responsibility Principle (SRP) Code Mechanics
  • One Reason to Change Robert C. Martin defines SRP: "A class should have one, and only one, reason to change." InvoiceCalculator changes ONLY if tax laws or discount algorithms change.
  • High Cohesion, Low Coupling By separating database operations (InvoiceRepository) and printing (InvoicePdfExporter) into distinct classes, modifying your SQL schema has zero risk of breaking tax calculations.

15.2 GoF Design Patterns: Singleton, Builder & Strategy in Action

📂 SingletonPatternDemo.java — Thread-Safe Double-Checked Locking Singleton
public class SingletonPatternDemo {

    public static class DatabaseConnectionPool {
        // volatile is MANDATORY: Prevents CPU instruction reordering during initialization!
        private static volatile DatabaseConnectionPool instance;

        // Private constructor prevents direct 'new' instantiation
        private DatabaseConnectionPool() {
            System.out.println("Initializing Database Connection Pool...");
        }

        // Double-Checked Locking: Fast, thread-safe, locks ONLY on first initialization
        public static DatabaseConnectionPool getInstance() {
            if (instance == null) { // 1st Check (no lock overhead)
                synchronized (DatabaseConnectionPool.class) {
                    if (instance == null) { // 2nd Check (inside lock)
                        instance = new DatabaseConnectionPool();
                    }
                }
            }
            return instance;
        }
    }

    public static void main(String[] args) {
        DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
        DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();
        System.out.println("Are pool1 and pool2 identical instance? " + (pool1 == pool2)); // true
    }
}
👁 Double-Checked Locking & The volatile Requirement Breakdown
  • Why the 1st Check? Avoids acquiring the heavy synchronized lock once the instance is already initialized. 99.99% of calls skip the synchronized block entirely, ensuring high throughput.
  • Why volatile is Mandatory The instruction instance = new DatabaseConnectionPool() is executed in 3 steps: (1) Allocate memory, (2) Run constructor, (3) Assign reference to instance. Without volatile, the JVM/CPU can reorder steps to (1) -> (3) -> (2). Another thread could observe instance != null before the constructor finishes, leading to a disastrous partially-initialized object crash!
📂 UserBuilderPatternDemo.java — Fluent Construction of Complex Objects
public class UserBuilderPatternDemo {

    public static class UserProfile {
        // Mandatory fields
        private final String username;
        private final String email;
        // Optional fields
        private final String phoneNumber;
        private final int age;

        private UserProfile(Builder b) {
            this.username = b.username;
            this.email = b.email;
            this.phoneNumber = b.phoneNumber;
            this.age = b.age;
        }

        public static class Builder {
            private final String username;
            private final String email;
            private String phoneNumber = "N/A";
            private int age = 18;

            public Builder(String username, String email) {
                this.username = username;
                this.email = email;
            }

            public Builder phoneNumber(String phone) { this.phoneNumber = phone; return this; }
            public Builder age(int age) { this.age = age; return this; }

            public UserProfile build() {
                return new UserProfile(this);
            }
        }
    }

    public static void main(String[] args) {
        UserProfile user = new UserProfile.Builder("john_dev", "john@accenture.com")
            .phoneNumber("+1-555-0199")
            .age(28)
            .build();
        System.out.println("Built user profile for: " + user.username);
    }
}
👁 Builder Pattern Mechanics & The Telescoping Constructor Problem
  • Solves Telescoping Constructors When a class has 10 optional fields, creating constructors for every permutation (User(a), User(a,b), User(a,b,c)) results in an unmaintainable nightmare. The Builder pattern eliminates this completely.
  • Fluent Method Chaining Methods like phoneNumber(...) return this (the builder reference), enabling readable, chained configuration before calling the terminal build() method.
  • Guarantees Immutability The product class (UserProfile) fields remain strictly private final with no setter methods, ensuring constructed objects are immutable and thread-safe.
📂 Module 5.../01_Design_Patterns_Demos/.../FactoryMethodPatternDemo.java
package com.accenture.ltt;

// Common Interface
interface Payment {
    void pay(int amount);
}

// Concrete Implementations
class UpiPayment implements Payment {
    public void pay(int amount) {
        System.out.println("Paid via UPI: $" + amount);
    }
}

class CardPayment implements Payment {
    public void pay(int amount) {
        System.out.println("Paid via Credit/Debit Card: $" + amount);
    }
}

// Factory: Decouples creation logic from caller
class PaymentFactory {
    public static Payment getPaymentMethod(String type) {
        if ("UPI".equalsIgnoreCase(type)) return new UpiPayment();
        if ("CARD".equalsIgnoreCase(type)) return new CardPayment();
        throw new IllegalArgumentException("Unknown payment method: " + type);
    }
}

public class FactoryMethodPatternDemo {
    public static void main(String[] args) {
        // Client does NOT instantiate concrete classes directly (no 'new CardPayment()')
        Payment payment = PaymentFactory.getPaymentMethod("CARD");
        payment.pay(500);
    }
}
👁 Factory Method Pattern Decoupling Mechanics
  • Decoupling Instantiation The client code only depends on the Payment interface and PaymentFactory. Concrete classes (UpiPayment, CardPayment) can be swapped or refactored without altering client business logic.
  • Spring Bean Factory Alignment This is the foundational pattern of Spring's BeanFactory and ApplicationContext, which manufactures beans dynamically without hardcoded new expressions.
📂 Module 5.../01_Design_Patterns_Demos/.../ObserverPatternDemo.java
package com.accenture.ltt;

import java.util.*;

// Observer (Subscriber Contract)
interface Subscriber {
    void notify(String videoTitle);
}

// Concrete Observer
class User implements Subscriber {
    private final String name;
    public User(String name) { this.name = name; }
    public void notify(String video) {
        System.out.println(name + " notified of new upload: " + video);
    }
}

// Subject (Publisher)
class YouTubeChannel {
    private final List<Subscriber> subscribers = new ArrayList<>();

    public void subscribe(Subscriber s) { subscribers.add(s); }
    public void unsubscribe(Subscriber s) { subscribers.remove(s); }

    public void uploadVideo(String title) {
        System.out.println("Channel uploaded: " + title);
        // Notify all registered observers automatically
        subscribers.forEach(sub -> sub.notify(title));
    }
}

public class ObserverPatternDemo {
    public static void main(String[] args) {
        YouTubeChannel channel = new YouTubeChannel();
        channel.subscribe(new User("Alice"));
        channel.subscribe(new User("Bob"));

        channel.uploadVideo("Spring Boot Microservices Architecture");
    }
}
👁 Observer Pattern Event Broadcasting Breakdown
  • Publish-Subscribe (Pub/Sub) The Subject (YouTubeChannel) maintains an internal list of observers. It does not know or care who the observers are, only that they implement Subscriber.
  • Event-Driven Architecture Foundational to message queues (Kafka, RabbitMQ) and Spring ApplicationEventPublisher: when an event occurs (e.g. OrderPlacedEvent), multiple decoupled listeners react independently without hardcoded couplings.
📂 StrategyPaymentDemo.java — Behavioral Strategy Pattern with Pluggable Algorithms
public class StrategyPaymentDemo {

    // Strategy Interface
    public interface PaymentStrategy {
        void pay(double amount);
    }

    // Concrete Strategy A
    public static class CreditCardStrategy implements PaymentStrategy {
        public void pay(double amount) {
            System.out.println("Processing $" + amount + " via Credit Card Gateway.");
        }
    }

    // Concrete Strategy B
    public static class PayPalStrategy implements PaymentStrategy {
        public void pay(double amount) {
            System.out.println("Processing $" + amount + " via PayPal OAuth Session.");
        }
    }

    // Context class: Open for extension, closed for modification!
    public static class ShoppingCart {
        private PaymentStrategy strategy;

        public void setPaymentStrategy(PaymentStrategy s) { this.strategy = s; }

        public void checkout(double amount) {
            strategy.pay(amount);
        }
    }

    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.setPaymentStrategy(new CreditCardStrategy());
        cart.checkout(250.0);

        // Switch algorithm at runtime without modifying ShoppingCart code!
        cart.setPaymentStrategy(new PayPalStrategy());
        cart.checkout(85.0);
    }
}
👁 Strategy Pattern & Open/Closed Principle Mechanics
  • Family of Algorithms The Strategy pattern defines an interchangeable family of algorithms encapsulated behind a common interface (PaymentStrategy).
  • Eliminates Conditionals Replaces brittle if-else if-else or switch blocks with polymorphism. Adding Bitcoin or Apple Pay requires only creating a new class implementing PaymentStrategy — ShoppingCart remains untouched.

15.3 Unit Testing with JUnit 5: Assertions, Lifecycle & Mocking

📂 Module 3.../EmployeeServiceTest01.java — Modern JUnit 5 Unit Testing
package com.accenture.ltt.testing;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

public class EmployeeServiceTest01 {
    private EmployeeService service;

    @BeforeEach
    void setUp() {
        service = new EmployeeService();
    }

    @Test
    @DisplayName("Should calculate bonus accurately for top performer")
    void testBonusCalculation() {
        double bonus = service.calculateBonus(100000.0, 5);
        assertEquals(20000.0, bonus, 0.001, "Bonus must equal 20% of base salary");
    }
}
👁 Modern JUnit 5 Architecture & Testing Best Practices
  • @BeforeEach Lifecycle Isolation Line 9: @BeforeEach executes before every individual @Test method. Instantiating a fresh EmployeeService guarantees test state isolation and prevents test pollution.
  • @DisplayName Semantic Documentation Line 15: Replaces ugly camelCase method names in CI/CD test reports with clean, readable human descriptions of expected system behavior.
  • Delta Tolerance in assertEquals Line 18: assertEquals(expected, actual, delta, message) — When comparing double values, always provide a delta tolerance (e.g. 0.001) to absorb tiny IEEE 754 floating-point inaccuracies.