SRP

 
/***
 * S - SINGLE RESPONSIBILITY PRINCIPLE (SRP)
 * ---------------------------------------------
 * A class should have only ONE reason to change.
 * 
 * That means:
 *  - A class should have only one responsibility
 *  - It should focus on a single concern
 *  - Business logic, persistence, and presenation should NOT
 *    be mixed together
 * 
 * Example: Order Processing System
 * 
 * Design (Correct SRP):
 *  - Order             -> holds data only
 *  - OrderRepository   -> handle persistence (save to DB, file, etc)
 *  - InvoiceService    -> handle invoice printing (display logic)
 * 
 * Why?
 *  - If database logic changes       -> onlye OrderRepository changes
 *  - If invoice format changes       -> only InvoiceService changes
 *  - If order data structure changes -> only Order changes 
 * 
 * BAD DESIGN (Violates SRP):
 *  - OrderRepository having both save() and printInvoice()
 *  - Mixing persistence logic with presentation logic
 * 
 * That would give the class MULTIPLE reasons to change.
 */

package com.minte9.oop.solid;

public class SRP {
    public static void main(String[] args) {
        Order order = new Order("ORD-1", 150.0);

        Repository<Order> repository = new OrderRepository();
        repository.save(order);
            // Order saved:ORD-1

        InvoiceService<Order> consoleInvoiceService = new ConsoleInvoiceService();
        consoleInvoiceService.print(order);
            // Invoice printed: ORD-1 / 150.0
    }
}

record Order(
    String id,
    double amount
) {}

interface Repository<T> {
    void save(T entity);
}

class OrderRepository implements Repository<Order> {
    @Override
    public void save(Order order) {
        System.out.println("Order saved:" + order.id());
    }
}

interface InvoiceService<T> {
    void print(T entity);
}

class ConsoleInvoiceService implements InvoiceService<Order> {
    @Override
    public void print(Order order) {
        System.out.println("Invoice printed: " + order.id() + " / " + order.amount());
    }
}

SRP - Bad Example

 
/**
 * NOT SRP - BAD DESIGN
 * --------------------
 * OrderRepository having both save() and printInvoice()
 * Mixing persistence logic with presentation logic
 * 
 * That would give the class MULTIPLE reasons to change.
 * 
 * SOLID principle don't shine in 20-line examples.
 * They shine in 20000 line systems.
 * 
 * Imagine the project grows.
 * 
 * Repository now:
 *  - Connects to PostgreSQL
 *  - Use transactions
 *  - Use connection pooling
 *  - Handle retry logic
 *  - Logs SQL errors
 * 
 * Invoice printing now:
 *  - Generated PDF
 *  - Supports multiple currencies
 *  - Sends invoice by email
 * 
 * Now your class becomes:
 *  - 200 lines of DB logic
 *  - 150 lines of invoice formating
 */

package com.minte9.oop.solid.bad;

public class SRP_bad {
    public static void main(String[] args) {
        Order order = new Order("ORD-1", 150.0);

        Repository<Order> repository = new Bad_OrderRepository();
        repository.save(order);
        repository.print(order);
            // Order saved:ORD-1
            // Invoice printed: ORD-1 / 150.0
    }
}

record Order(String id, double amount) {}

interface Repository<T> {
    void save(T entity);
    void print(T entity);
}

class Bad_OrderRepository implements Repository<Order> {
    @Override
    public void save(Order order) {
        System.out.println("Order saved:" + order.id());
    }
    @Override
    public void print(Order order) {
        System.out.println("Invoice printed: " + order.id() + " / " + order.amount());
    }
}

OCP

 
/**
 * O - OPEN/CLOSED PRINCIPLE (OCP)
 * -------------------------------
 * Open for extension.
 * Closed for modification. 
 * 
 * We can add new notification types WITHOUT
 * changing existing classes.
 * 
 * Example: Notification Service
 * 
 * If tommorrow you need a SlackNotification, just add a new class.
 * This DO NOT change:
 *  - NotificationService
 *  - EmailService
 *  - Any existing code
 */

package com.minte9.oop.solid;

public class OCP {
    public static void main(String[] args) {
        NotificationService service = new NotificationService();

        service.send(new EmailNotification(), "Welcome!");
        service.send(new SmsNotification(), "Your code is 1234");
        service.send(new PushNotification(), "New message received");

            // Sending EMAIL: Welcome!
            // Sending SMS: Your code is 1234
            // Sending PUSH: New message received
    }
}

// ==== Contract ====

interface Notification   {
    void send(String message);    
}

// ==== Serice ====

class NotificationService {
    public void send(Notification notification, String message) {
        notification.send(message);
    }
} 

// ==== Extensions =====

class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending EMAIL: " + message);
    }
}

class SmsNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

class PushNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending PUSH: " + message);
    }
}

OCP - Bad Example

 
/**
 * NOT OCP - BAD DESIGN
 * --------------------
 * 
 * Every time you add a new type:
 *  - You modify the service class
 *  - You risk breaking existing code
 *  - You recompile and retest
 * 
 * This violetes OCP.
 * 
 * Why interfaces are perfect for OCP?
 * 
 * Interfaces allow:
 *  - Polymorphism
 *  - Loose coupling
 *  - Independent implementations
 *  - Runtime substitution
 * 
 * Whenever you see if (type == ...)
 *  - That's usually a smell that OCP can improve the design.
 */

package com.minte9.oop.solid.bad;

public class OCP_bad {
    public static void main(String[] args) {
        Bad_NotificationService service = new Bad_NotificationService();

        service.send("email", "Welcome!");
        service.send("sms", "Your code is 1234");
        service.send("push", "New message received");
    }
}

class Bad_NotificationService {
    public void send(String type, String message) {

        if (type.equals("email")) {
            System.out.println("Sending EMAIL: " + message);
        } else
        if (type.equals("sms")) {
            System.out.println("Sending SMS: " + message);
        } else
        if (type.equals("push")) {
            System.out.println("Sending PUSH: " + message);
        }

    }
}

LSB

 
/**
 * L - Liskov Substitution Principle (LSP)
 * ---------------------------------------
 * Subtypes must be substituable for their base types.
 * 
 * Example: Payment System
 * 
 * PaymentMethod defines a contract:
 *  - pay(amount)
 *  - amount must be positive
 * 
 * All implementation:
 *  - validate the amount
 *  - perform payment
 *  - behave consistently
 * 
 * All payment types behave consistently. 
 */

package com.minte9.oop.solid;

public class LSP {
    public static void main(String[] args) {
        PaymentMethod p1 = new CreditCardPayment();
        PaymentMethod p2 = new PayPalPayment();

        p1.pay(200);
        p2.pay(300);
    }
}

interface PaymentMethod {
    void pay(double amount);
}

abstract class BasePayment implements PaymentMethod {
    protected void validate(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Invalid amount");
        }
    }
}

class CreditCardPayment extends BasePayment {
    @Override
    public void pay(double amount) {
        validate(amount);
        System.out.println("Paid $" + amount + " using Credit Card");
    }
}

class PayPalPayment extends BasePayment {
    @Override
    public void pay(double amount) {
        validate(amount);
        System.out.println("Paid $" + amount + " using PayPal");
    }
}

LSP - Bad example

 
/**
 * LSB - BAD EXAMPLE
 * -----------------
 * Here, a subtype changes expected behavior.
 * It throws an exception where the base type contract 
 * says it should process the payment.
 */
package com.minte9.oop.solid.bad;

public class LSP_bad {
    public static void main(String[] args) {
        PaymentMethod p1 = new CreditCardPayment();
        PaymentMethod p2 = new FreeTrialPayment();  // problematic subtype

        p1.pay(200);

        // This will break at runtime
        p2.pay(300);
    }
}

interface PaymentMethod {
    void pay(double amount);
}

class CreditCardPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {

        if (amount <= 0) {
            throw new IllegalArgumentException("Invalid amount");
        }

        System.out.println("Paid $" + amount + " using Credit Card");
    }
}

/**
 * Violates LSP
 * 
 * This class strengthens preconditions:
 * It does NOT allow payments above 100.
 * 
 * The base contract does not define such limitation.
 */
class FreeTrialPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {

        if (amount > 100) {
            throw new UnsupportedOperationException(
                "FreeTrial cannot process payments above 100");
        }

        System.out.println("Free trial payment processed: $" + amount);
    }
}