System Design
SOLID is a set of five principles for designing software that is easy to understand, maintain, and extend. These principles are often used in object-oriented design and serve as guidelines for creating flexible and robust systems.
SOLID Principles
Single Responsibility Principle (SRP)
Open/Closed Principle (OCP)
Liskov Substitution Principle (LSP)
Interface Segregation Principle (ISP)
Dependency Inversion Principle (DIP)
These principles make systems more modular, maintainable, and scalable, reducing the chances of bugs and simplifying future development.
1. Single Responsibility Principle (SRP)
Principle: A class should have only one reason to change, i.e., it should have only one responsibility.
In a car rental service, we could have a CarService
that manages car-related logic (like availability) and a separate RentalService
that handles rental-related logic.
Code Example:
// CarService.java
@Service
public class CarService {
public boolean isCarAvailable(String carId) {
// Check if the car is available for rent
return true; // Assume car is available
}
}
// RentalService.java
@Service
public class RentalService {
private final CarService carService;
public RentalService(CarService carService) {
this.carService = carService;
}
public void rentCar(String carId, String userId) {
if (carService.isCarAvailable(carId)) {
// Logic to rent the car
System.out.println("Car rented successfully.");
} else {
System.out.println("Car not available.");
}
}
}
Explanation:
CarService
only checks the car's availability, while RentalService
handles the rental logic. Each service has a single responsibility.
2. Open/Closed Principle (OCP)
Principle: Classes should be open for extension but closed for modification.
In a car rental service, suppose we want to calculate different rental prices based on car types. Instead of modifying a single pricing class each time we add a new car type, we can extend it.
Code Example:
// RentalPriceCalculator.java
public interface RentalPriceCalculator {
double calculatePrice(int days);
}
// StandardCarPriceCalculator.java
@Service
public class StandardCarPriceCalculator implements RentalPriceCalculator {
public double calculatePrice(int days) {
return days * 50; // Standard car rental price
}
}
// LuxuryCarPriceCalculator.java
@Service
public class LuxuryCarPriceCalculator implements RentalPriceCalculator {
public double calculatePrice(int days) {
return days * 100; // Luxury car rental price
}
}
Explanation:
If a new car type is added, like SportsCarPriceCalculator
, we create a new class implementing RentalPriceCalculator
without modifying the existing ones.
3. Liskov Substitution Principle (LSP)
Principle: Objects of a superclass should be replaceable with objects of its subclasses without affecting the system.
In our car rental system, if ElectricCar
is a subtype of Car
, it should work in the same way as a Car
when rented, without breaking any functionality.
Code Example:
// Car.java
public abstract class Car {
public abstract void start();
}
// ElectricCar.java
@Service
public class ElectricCar extends Car {
public void start() {
System.out.println("Electric car started silently.");
}
}
// DieselCar.java
@Service
public class DieselCar extends Car {
public void start() {
System.out.println("Diesel car started with engine sound.");
}
}
Explanation:
Both ElectricCar
and DieselCar
can be used wherever a Car
type is expected without breaking the code, thus adhering to Liskov Substitution.
4. Interface Segregation Principle (ISP)
Principle: Clients should not be forced to depend on methods they do not use.
In our car rental example, a Car
interface could have separate interfaces like FuelCar
and ElectricCar
. This way, electric cars don’t need to implement fuel-related methods.
Code Example:
// Car.java
public interface Car {
void start();
}
// FuelCar.java
public interface FuelCar extends Car {
void refuel();
}
// ElectricCar.java
public interface ElectricCar extends Car {
void recharge();
}
// Sedan.java
@Service
public class Sedan implements FuelCar {
public void start() {
System.out.println("Sedan car started.");
}
public void refuel() {
System.out.println("Sedan refueled.");
}
}
// Tesla.java
@Service
public class Tesla implements ElectricCar {
public void start() {
System.out.println("Tesla started.");
}
public void recharge() {
System.out.println("Tesla recharged.");
}
}
Explanation:
FuelCar
has a refuel()
method, while ElectricCar
has a recharge()
method. Tesla, being an electric car, does not need to implement refuel()
.
5. Dependency Inversion Principle (DIP)
Principle: High-level modules should not depend on low-level modules but rather on abstractions.
In our car rental system, the RentalService
should depend on an NotificationService
interface rather than a specific implementation like EmailNotificationService
.
Code Example:
// NotificationService.java
public interface NotificationService {
void notifyUser(String message);
}
// EmailNotificationService.java
@Service
public class EmailNotificationService implements NotificationService {
public void notifyUser(String message) {
System.out.println("Email notification sent: " + message);
}
}
// RentalService.java
@Service
public class RentalService {
private final NotificationService notificationService;
public RentalService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void rentCar(String carId, String userId) {
// Rent the car and notify user
notificationService.notifyUser("Your car rental is confirmed.");
}
}
Explanation:
RentalService
depends on the NotificationService
abstraction, so we can switch to any other notification implementation, like SMSNotificationService
, without changing RentalService
.
Summary Table
Single Responsibility
CarService
checks availability, RentalService
handles rentals.
Open/Closed
RentalPriceCalculator
extended by StandardCarPriceCalculator
, etc.
Liskov Substitution
ElectricCar
and DieselCar
are interchangeable as Car
types.
Interface Segregation
FuelCar
and ElectricCar
interfaces avoid unused methods.
Dependency Inversion
RentalService
depends on NotificationService
abstraction.
This unified car rental example demonstrates each SOLID principle in Spring Boot for a maintainable, scalable system.
Last updated