SOLID Principles
The five SOLID principles for object-oriented design, each with a simple Java example.
SOLID principles are the guidelines we use when we create software using OOP concepts.
If we use these SOLID principles, it will be easy for us to maintain our systems or applications and to add new features to them.
1. Single Responsibility Principle
Any function, class or module should have only one responsibility.
Bad: this class saves invoices and also sends emails. Two reasons to change.
class InvoiceService {
void saveInvoice(Invoice invoice) { /* save to database */ }
void sendEmail(Invoice invoice) { /* send email */ }
}Good: each class does one job.
class InvoiceRepository {
void save(Invoice invoice) { /* save to database */ }
}
class EmailService {
void send(Invoice invoice) { /* send email */ }
}2. Open-Closed Principle
Any class should be open to extension and closed to modification.
Bad: every new payment type means editing this method.
class PaymentProcessor {
void pay(String type) {
if (type.equals("card")) { /* card payment */ }
else if (type.equals("paypal")) { /* PayPal payment */ }
}
}Good: add a new payment type by adding a new class. The existing code stays the same.
interface Payment {
void pay();
}
class CardPayment implements Payment {
public void pay() { /* card payment */ }
}
class PayPalPayment implements Payment {
public void pay() { /* PayPal payment */ }
}
class PaymentProcessor {
void process(Payment payment) {
payment.pay();
}
}3. Liskov Substitution Principle
Objects of a superclass should be replaceable (substitutable) with objects of its subclasses without affecting the correctness of the program.
Bad: Penguin is a Bird, but it breaks the program when fly() is called.
class Bird {
void fly() { System.out.println("Flying"); }
}
class Penguin extends Bird {
@Override
void fly() { throw new UnsupportedOperationException("Penguins can't fly"); }
}Good: only birds that can fly get fly().
class Bird { }
class FlyingBird extends Bird {
void fly() { System.out.println("Flying"); }
}
class Sparrow extends FlyingBird { }
class Penguin extends Bird { }4. Interface Segregation Principle
A client should not be forced to implement an interface it does not use.
Bad: a robot has to implement eat(), which it doesn't need.
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
public void work() { System.out.println("Working"); }
public void eat() { /* robots don't eat */ }
}Good: split into small interfaces and implement only what's needed.
interface Workable { void work(); }
interface Eatable { void eat(); }
class Human implements Workable, Eatable {
public void work() { System.out.println("Working"); }
public void eat() { System.out.println("Eating"); }
}
class Robot implements Workable {
public void work() { System.out.println("Working"); }
}5. Dependency Inversion Principle
Entities must depend on abstractions, not on concrete implementations.
Bad: OrderService is tied to MySQL. Switching databases means changing this class.
class MySQLDatabase {
void save(String data) { /* save to MySQL */ }
}
class OrderService {
private MySQLDatabase db = new MySQLDatabase();
void placeOrder(String order) {
db.save(order);
}
}Good: OrderService depends on an interface, so any database can be passed in.
interface Database {
void save(String data);
}
class MySQLDatabase implements Database {
public void save(String data) { /* save to MySQL */ }
}
class MongoDatabase implements Database {
public void save(String data) { /* save to MongoDB */ }
}
class OrderService {
private final Database db;
OrderService(Database db) {
this.db = db;
}
void placeOrder(String order) {
db.save(order);
}
}Example
public abstract class Animal {
public abstract void makeSound();
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof woof");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow meow");
}
}
class AnimalSoundPlayer {
public void playSound(Animal animal) {
animal.makeSound();
}
}