Phase 17
SOLID Design Principles, GoF Patterns & Production Architecture
📂 Module 5_ Architecture and Design Patterns \\ Module 3_ Unit Testing with Junit 5
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."
InvoiceCalculatorchanges 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 toinstance. Withoutvolatile, the JVM/CPU can reorder steps to (1) -> (3) -> (2). Another thread could observeinstance != nullbefore 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(...)returnthis(the builder reference), enabling readable, chained configuration before calling the terminalbuild()method. -
Guarantees Immutability
The product class (
UserProfile) fields remain strictlyprivate finalwith 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
Paymentinterface andPaymentFactory. 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
BeanFactoryandApplicationContext, which manufactures beans dynamically without hardcodednewexpressions.
📂 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 implementSubscriber. -
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-elseorswitchblocks with polymorphism. Adding Bitcoin or Apple Pay requires only creating a new class implementingPaymentStrategy—ShoppingCartremains 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:
@BeforeEachexecutes before every individual@Testmethod. Instantiating a freshEmployeeServiceguarantees 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 comparingdoublevalues, always provide a delta tolerance (e.g.0.001) to absorb tiny IEEE 754 floating-point inaccuracies.