S.O.L.I.D Principles
The SOLID principles are a set of five design principles in object-oriented programming (OOP) intended to make software designs more understandable, flexible, and maintainable. They were introduced by Robert C. Martin. Each letter in "SOLID" represents one of these principles -
Single Responsibility Principle (SRP)
A class should have only one reason to change, meaning that it should have only one job or responsibility. This principle advocates for keeping classes focused and ensuring that each class is responsible for a single part of the functionality within the system.
Real-world example -
Example of a class violating Single Responsibility Principle
class User {
public void createUser() {
//create a user
}
public void sendEmail() {
//send an email
}
}
In the above example, the User class has two responsibilities: creating a user and sending an email. This violates the SRP. Instead, we should separate these responsibilities into two different classes.
Example of a class following Single Responsibility Principle
class UserCreator {
public void createUser() {
// create a user
}
}
class EmailSender {
public void sendEmail() {
// send an email
}
}
Open Closed Principle (OCP)
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that you should be able to extend the behavior of a system without modifying its existing code. This is often achieved through techniques such as inheritance, composition, and design patterns like the Strategy pattern.
Example of a class violating Open Closed Principle
class Shape {
public double calculateArea() {
return 0;
}
}
class Rectangle extends Shape {
private double width;
private double height;
@Override
public double calculateArea() {
return width * height;
}
}
In the above example, if we want to add a new shape like a circle, we have to modify the Shape class. This violates the OCP. Instead, we should design our classes so that they can be extended without modification.
Real-world example -
Example of a class following Open Closed Principle
interface Shape {
double calculateArea();
}
class Rectangle implements Shape {
private double width;
private double height;
@Override
public double calculateArea() {
return width * height;
}
}
class Circle implements Shape {
private double radius;
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclass without affecting the correctness of the program. In other words, if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.
Real-world example-
Example of a class violating Liskov Substitution Principle
Recommended by LinkedIn
class Bird {
public void fly() {
// Add code for flying
}
}
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostriches cannot fly");
}
}
In the above example, although Ostrich is a subtype of Bird, it does not behave like other birds. This violates the LSP. Instead, we should design our classes so that derived classes can be substituted for their base classes without altering the correctness of the program.
Example of a class following Liskov Substitution Principle
interface Bird {
void fly();
}
class Sparrow implements Bird {
@Override
public void fly() {
// Add code for flying
}
}
class Ostrich implements Bird {
@Override
public void fly() {
// Ostriches don't fly
}
}
Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use. This principle encourages the design of smaller, more focused interfaces rather than large, monolithic ones. It helps to prevent classes from depending on methods they don't need.
Real-world example-
Example of a class violating Interface Segregation Principle
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
@Override
public void work() {
// Add code for working
}
@Override
public void eat() {
// nothing to do here for a robot
}
}
In the above example, the Robot class is forced to implement the eat() method even though it doesn't need it. This violates the ISP. Instead, we should design smaller, more focused interfaces:
Example of a class following Interface Segregation Principle
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Robot implements Workable {
@Override
public void work() {
// Add code for working
}
}
Dependency Inversion Principle (DIP)
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. This principle encourages decoupling between classes by introducing abstractions and relying on these abstractions rather than concrete implementations.
Real-world example -
Example of a class violating Dependency Inversion Principle
class Database {
public void connect() {
// Add code to connect to the database
}
}
class UserService {
private Database database;
public UserService() {
this.database = new Database();
}
}
In the above example, UserService directly depends on the Database class, violating the DIP. Instead, we should depend on abstractions rather than concrete implementations.
Example of a class following Dependency Inversion Principle
interface Connection {
void connect();
}
class Database implements Connection {
@Override
public void connect() {
// Add code to connect to the database
}
}
class UserService {
private Connection connection;
public UserService(Connection connection) {
this.connection = connection;
}
}
In the above example, UserService now depends on the Connection interface rather than the Database class directly. This allows for easier substitution of different types of databases or connection mechanisms.
By adhering to these principles, developers can create more modular, flexible, and maintainable codebases, making it easier to extend and refactor software as requirements change.