Hello everyone,
In the world of software development, we always run into the same challenges: How do we write code that is easy to maintain, extend, and understand? How do we prevent our codebase from becoming confusing and rigid over time? The answer to these questions often lies not in a new framework, but in fundamental design principles.
This is exactly where the S.O.L.I.D principles come into play. A collection of five design principles for object-oriented programming, they provide a clear guide to making our code more robust, flexible, and maintainable. Developed by Robert C. Martin (also known as “Uncle Bob”), they form the foundation for the architecture of modern, sustainable software.
In this article, we dive into each of the five principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. We’ll look at what each principle means and how we can incorporate them into our everyday code.
##TL;DR
- S — Single Responsibility Principle (SRP)
- A class has exactly one job.
- O — Open/Closed Principle (OCP)
- Classes should be open to extensions and closed to changes.
- L — Liskov Substitution Principle (LSP)
- Objects of a superclass should be interchangeable with objects of their derived classes without changing the application.
- I — Interface Segregation Principle (ISP)
- Clients should not be forced to depend on methods they do not use.
- D — Dependency Inversion Principle (DIP)
- High-level modules should not depend on low-level modules, but both on abstractions.
SOLID
The principles help developers design systems that are loosely coupled, highly cohesive, and easy to modify without destroying existing functionality.
Below I will explain the individual principles using code examples. And show interview sample questions and answers.
Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) states that a class or module should only have a single reason for changing. In other words, each class should only have a single responsibility that is clearly defined.
Example
❌ Poor design:
class Invoice {
public void calculateTotal() {
// logic for calculating total
}
public void printInvoice() {
// logic for printing invoice
}
public void saveToDatabase() {
// logic for saving invoice
}
}
Here the Invoice class handles multiple tasks, calculating the total, printing the invoice and storing it in the database.
✅ Good design:
class Invoice {
public void calculateTotal() {
// logic for calculating total
}
}
class InvoicePrinter {
public void print(Invoice invoice) {
// logic for printing invoice
}
}
class InvoiceRepository {
public void save(Invoice invoice) {
// logic for saving invoice
}
}
Now each class has a task.
Interview questions
Question: What is the main disadvantage of violating the SRP?
Answer: When a class has multiple assignments, changes to one assignment may affect the others. This increases the risk of bugs and affects maintainability.
Open/Closed Principle (OCP)
The Open Closed Principle (OCP) states that software entities (such as classes, modules or functions) should be open to extension** but closed to change.
This means you can extend the behavior of a module without having to change the existing source code.
❌ Bad design
class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.length * r.width;
}
return 0;
}
}
When we add a shape, we need to adjust the calculateArea method.
✅ Good design
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI * radius * radius; }
}
class Rectangle implements Shape {
double length, width;
Rectangle(double length, double width) { this.length = length; this.width = width; }
public double area() { return length * width; }
}
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area();
}
}
We have applied polymorphism to comply with the OCP and can now add as many shapes as we want without having to make any changes to the existing code.
Interview questions
Question: How do you achieve the OCP in Java?
Answer: Abstraction (interfaces and abstract classes) and polymorphism (inheritance), so functionality can be added without changing existing code.
Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) states that objects of a superclass should be interchangeable with objects of their derived classes without changing the behavior of the application.
This sounds complicated, but the idea is simple: a subclass cannot change the behavior of its superclass in a way that causes unexpected errors for the code that uses the superclass.
Example
❌ Poor design:
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// Beispiel-Anwendung
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(4);
// Erwartet: Flaeche 20, Tatsaechlich: 16 (wegen der ueberschriebenen Methoden)
System.out.println(r.getArea());
Here the Square class violates the LSP because it changes the behavior of the Rectangle methods. A Square is not a Rectangle in the LSP sense because it does not meet the expectations of the calling code.
✅ Good design:
interface Shape {
int getArea();
}
class Rectangle implements Shape {
protected int width;
protected int height;
// ... Konstruktoren und Getter
public int getArea() { return width * height; }
}
class Square implements Shape {
protected int side;
// ... Konstruktoren und Getter
public int getArea() { return side * side; }
}
By using a common interface Shape that only defines the getArea() method, we ensure that Rectangle and Square independently implement their own specific properties and logic without violating each other’s contracts.
Interview questions
Question: How can you detect violations of the LSP?
Answer: A clear indication is when in the subclass you have to override the methods of the superclass in such a way that you throw an exception or implement unexpected logic that changes the basic behavior.
Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) states that clients should not be forced to depend on methods they do not use. It’s better to have many specific interfaces than one big, general interface.
The goal is to reduce coupling and increase maintainability by creating interfaces that reflect exactly what a client needs.
Example
❌ Poor design:
interface Worker {
void work();
void eat();
void sleep();
}
class HumanWorker implements Worker {
public void work() { /* ... */ }
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
class RobotWorker implements Worker {
public void work() { /* ... */ }
// Muss essen und schlafen implementieren, obwohl ein Roboter das nicht tut!
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
The RobotWorker is forced to implement methods that make no sense to it. This leads to empty or meaningless logic and unnecessary dependencies.
✅ Good design:
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class HumanWorker implements Workable, Eatable, Sleepable {
public void work() { /* ... */ }
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
class RobotWorker implements Workable {
public void work() { /* ... */ }
}
Now each class only implements the interfaces that it really needs. This makes the code cleaner and reduces coupling.
Interview questions
Question: What is the “client” side of applying the ISP?
Answer: The client is the code that uses the interface. He should not depend on the “thick” interface, but on an interface that only contains the methods that he really needs.
Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules, but both on abstractions.
Simply put: concrete implementations (like a MySqlDatabase class) should not be used directly in your high-level code (like a UserService). Instead, both should depend on an interface or an abstract class.
Example
❌ Poor design:
class MySqlDatabase {
public void save(String data) {
// Logik, um Daten in einer MySQL-Datenbank zu speichern
}
}
class UserService {
private MySqlDatabase database;
public UserService() {
this.database = new MySqlDatabase();
}
public void saveUser(String user) {
database.save(user);
}
}
The UserService class is a high-level module that contains the core logic (user management). It depends directly on the low-level implementation MySqlDatabase. If we want to change the database (e.g. to PostgreSQL), we would have to adapt the UserService class, which violates the Open/Closed Principle.
✅ Good design:
interface Database {
void save(String data);
}
class MySqlDatabase implements Database {
public void save(String data) {
// Logik, um Daten in einer MySQL-Datenbank zu speichern
}
}
class PostgresDatabase implements Database {
public void save(String data) {
// Logik, um Daten in einer PostgreSQL-Datenbank zu speichern
}
}
class UserService {
private Database database;
// Abhaengigkeit wird ueber den Konstruktor injiziert
public UserService(Database database) {
this.database = database;
}
public void saveUser(String user) {
database.save(user);
}
}
Now both the high-level module (UserService) and low-level modules (MySqlDatabase, PostgresDatabase) depend on the abstraction (Database). We can easily swap the implementation of the database without touching the UserService.
Interview questions
Question: What is the connection between the DIP and the concept of Dependency Injection?
Answer: Dependency Injection (DI) is a technique to implement the Dependency Inversion Principle. Instead of instantiating dependencies in the code, they are “injected” into the class from the outside (e.g. via the constructor), so that the class only depends on abstractions.
Conclusion
The S.O.L.I.D principles are the guide for us developers to turn good code into great code. They are not just theoretical concepts, but practical tools that help us design systems that are easy to maintain, extend, and understand.
By applying these principles to your daily code reviews and design discussions, you’ll find your codebase becomes more robust, team collaboration becomes easier, and you’ll be better able to handle increasing complexity. Start with a principle and work your way slowly. The effort is worth it!
![[EN] S.O.L.I.D Design Prinzipien](/images/SOLID_BlogHeader.png)