1. Single Responsibility Principle (SRP)
- Definition: A class should have only one reason to change.
- Meaning: Each class should only have one responsibility or functionality. This makes the system easier to maintain and understand.
- Example:
- Bad: A
Report
class that both generates reports and sends them via email. - Good: A
Report
class generates reports, and aEmailSender
class handles email sending.
- Bad: A
// Bad example
public
c class Invoice {
public void calculateTotal() {
// Logic for calculating total
}
public void printInvoice() {
// Logic for printing the invoice
}
public void saveToDatabase() {
// Logic for saving the invoice to the database
}
}
// Good Example
public class Invoice {
public void calculateTotal() {
// Logic for calculating total
}
}
public class InvoicePrinter {
public void print(Invoice invoice) {
// Logic for printing the invoice
}
}
public class InvoiceRepository {
public void save(Invoice invoice) {
// Logic for saving the invoice to the database
}
}
2. Open/Closed Principle (OCP)
The problem here, that when we change old classes it can bring new bugs, so better option is to inherit and make changes in some new classes.
- Definition: Software entities (classes, modules, functions) should be open for extension but closed for modification.
- Meaning: You should be able to add new functionality to a class without changing its existing code. This prevents introducing bugs into existing functionality.
- Example:
- Use interfaces or abstract classes that can be implemented or extended without modifying the original code.
// Bad example ( if i need to add another shape i will need to change this method )
public class AreaCalculator {
public double calculateShapeArea(Object shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.radius * circle.radius;
} else if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.width * rectangle.height;
}
return 0;
}
}
// Good example
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double calculateArea() {
return width * height;
}
}
public class AreaCalculator {
public double calculateShapeArea(Shape shape) {
return shape.calculateArea();
}
}
New shapes can be added without modifying the AreaCalculator
.
3. Liskov Substitution Principle (LSP)
- Definition: Objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program.
- Meaning: Subclasses should behave in a way that does not break the functionality expected of the parent class.
- Example:
- Bad: A
Bird
superclass with afly()
method, but aPenguin
subclass overrides it to throw an exception. - Good: Design the hierarchy to reflect actual behavior or use composition over inheritance.
- Bad: A
public class Bird {
public void fly() {
System.out.println("Flying");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
// Good example
public interface Bird {
void eat();
}
public interface FlyingBird extends Bird {
void fly();
}
public class Sparrow implements FlyingBird {
public void eat() {
System.out.println("Eating");
}
public void fly() {
System.out.println("Flying");
}
}
public class Penguin implements Bird {
public void eat() {
System.out.println("Eating");
}
}
4. Interface Segregation Principle (ISP)
- Definition: A class should not be forced to implement interfaces it does not use.
- Meaning: Break down large, complex interfaces into smaller, more specific ones that are relevant to particular clients.
- Example:
- Bad: A
Printer
interface with methodsprint()
,scan()
, andfax()
. A simple printer shouldn’t implementscan()
orfax()
. - Good: Separate interfaces like
Printable
,Scannable
, andFaxable
.
- Bad: A
// Bad example
public interface Worker {
void work();
void eat();
}
public class Robot implements Worker {
public void work() {
System.out.println("Working");
}
public void eat() {
throw new UnsupportedOperationException("Robots don't eat!");
}
}
// Good example
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class HumanWorker implements Workable, Eatable {
public void work() {
System.out.println("Working");
}
public void eat() {
System.out.println("Eating");
}
}
public class RobotWorker implements Workable {
public void work() {
System.out.println("Working");
}
}
Each class only implements the interfaces it needs.
5. Dependency Inversion Principle (DIP)
- Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
- Meaning: Decouple higher-level code from lower-level code using interfaces or abstractions.
- Example:
- Bad: A
Database
class tightly coupled to aUserService
class. - Good:
UserService
depends on an interfaceDataStorage
, which can have implementations likeDatabase
orInMemoryStorage
.
- Bad: A
// Bad example
public class Database {
public void save(String data) {
System.out.println("Saving to database: " + data);
}
}
public class UserService {
private Database database;
public UserService() {
this.database = new Database();
}
public void saveUser(String user) {
database.save(user);
}
}
// Good example
public interface DataStorage {
void save(String data);
}
public class Database implements DataStorage {
public void save(String data) {
System.out.println("Saving to database: " + data);
}
}
public class InMemoryStorage implements DataStorage {
public void save(String data) {
System.out.println("Saving to in-memory storage: " + data);
}
}
public class UserService {
private DataStorage storage;
public UserService(DataStorage storage) {
this.storage = storage;
}
public void saveUser(String user) {
storage.save(user);
}
}