SOLID Principle Simplified
SOLIDified: Demystifying Software Design Principles with practical examples to write maintainable, extensible, and testable code for complex software systems.
Join the DZone community and get the full member experience.
Join For FreeAs software systems grow in complexity, it becomes increasingly important to write maintainable, extensible, and testable code. The SOLID principles, introduced by Robert C. Martin (Uncle Bob), are a set of guidelines that can help you achieve these goals. These principles are designed to make your code more flexible, modular, and easier to understand and maintain. In this post, we’ll explore each of the SOLID principles with code examples to help you understand them better.
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should have a single responsibility or job. By adhering to this principle, you can create classes that are easier to understand, maintain, and test.
Example
Imagine we have a UserManager
class that handles user registration, password reset, and sending notifications. This class violates the SRP because it has multiple responsibilities.
public class UserManager {
public void registerUser(String email, String password) {
// Register user logic
}
public void resetPassword(String email) {
// Reset password logic
}
public void sendNotification(String email, String message) {
// Send notification logic
}
}
To adhere to the SRP, we can split the responsibilities into separate classes:
public class UserRegistration {
public void registerUser(String email, String password) {
// Register user logic
}
}
public class PasswordReset {
public void resetPassword(String email) {
// Reset password logic
}
}
public class NotificationService {
public void sendNotification(String email, String message) {
// Send notification logic
}
}
2. Open/Closed Principle (OCP)
The Open/Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In simpler terms, you should be able to add new functionality without modifying the existing code.
Example
Let’s consider a ShapeCalculator
class that calculates the area of different shapes. Without adhering to the OCP, we might end up with a lot of conditional statements as we add support for new shapes.
public class ShapeCalculator {
public double calculateArea(Shape shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.getRadius() * circle.getRadius();
} else if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.getWidth() * rectangle.getHeight();
}
// More conditional statements for other shapes
return 0;
}
}
To adhere to the OCP, we can introduce an abstract Shape
class and define the calculateArea
method in each concrete shape class.
public abstract class Shape {
public abstract double calculateArea();
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
Now, we can add new shapes without modifying the ShapeCalculator
class.
3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that subtypes must be substitutable for their base types. In other words, if you have a base class and a derived class, you should be able to use instances of the derived class wherever instances of the base class are expected, without affecting the correctness of the program.
Example
Let’s consider a Vehicle
class and a Car
class that extends Vehicle
. According to the LSP, any code that works with Vehicle
objects should also work correctly with Car
objects.
public class Vehicle {
public void startEngine() {
// Start engine logic
}
}
public class Car extends Vehicle {
@Override
public void startEngine() {
// Additional logic for starting a car engine
super.startEngine();
}
}
In this example, the Car
class overrides the startEngine
method and extends the behavior by adding additional logic. However, if we violate the LSP by changing the behavior in an unexpected way, it could lead to issues.
public class Car extends Vehicle {
@Override
public void startEngine() {
// Different behavior, e.g., throwing an exception
throw new RuntimeException("Engine cannot be started");
}
}
Here, the Car
class violates the LSP by throwing an exception instead of starting the engine. This means that code that expects to work with Vehicle
objects may break when Car
objects are substituted.
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle states that clients should not be forced to depend on interfaces they don’t use. In other words, it’s better to have many smaller, focused interfaces than a large, monolithic interface.
Example
Imagine we have a Worker
interface that defines methods for different types of workers (full-time, part-time, contractor).
public interface Worker {
void calculateFullTimeSalary();
void calculatePartTimeSalary();
void calculateContractorHours();
}
public class FullTimeEmployee implements Worker {
@Override
public void calculateFullTimeSalary() {
// Calculate full-time salary
}
@Override
public void calculatePartTimeSalary() {
// Not applicable, throw exception or leave empty
}
@Override
public void calculateContractorHours() {
// Not applicable, throw exception or leave empty
}
}
In this example, the FullTimeEmployee
class is forced to implement methods it doesn't need, violating the ISP. To address this, we can segregate the interface into smaller, focused interfaces:
public interface FullTimeWorker {
void calculateFullTimeSalary();
}
public interface PartTimeWorker {
void calculatePartTimeSalary();
}
public interface ContractWorker {
void calculateContractorHours();
}
public class FullTimeEmployee implements FullTimeWorker {
@Override
public void calculateFullTimeSalary() {
// Calculate full-time salary
}
}
Now, each class only implements the methods it needs, adhering to the ISP.
5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.
Example
Let’s consider a UserService
class that depends on a DatabaseRepository
class for data access.
public class UserService {
private DatabaseRepository databaseRepository;
public UserService() {
databaseRepository = new DatabaseRepository();
}
public void createUser(String email, String password) {
// Use databaseRepository to create a new user
}
}
In this example, the UserService
class is tightly coupled to the DatabaseRepository
class, violating the DIP. To adhere to the DIP, we can introduce an abstraction (interface) and inject the implementation at runtime.
public interface Repository {
void createUser(String email, String password);
}
public class DatabaseRepository implements Repository {
@Override
public void createUser(String email, String password) {
// Database logic to create a new user
}
}
public class UserService {
private Repository repository;
public UserService(Repository repository) {
this.repository = repository;
}
public void createUser(String email, String password) {
repository.createUser(email, password);
}
}
Now, the UserService
class depends on the Repository
abstraction, and the implementation (DatabaseRepository
) can be injected at runtime. This makes the code more modular, testable, and easier to maintain.
By following the SOLID principles, you can create more maintainable, extensible, and testable code. These principles promote modular design, loose coupling, and separation of concerns, ultimately leading to better software quality and easier maintenance over time.
Published at DZone with permission of Lalithkumar Prakashchand. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments