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);
}
}