![What are the SOLID principles in programming?](https://codenga-com-content.s3.us-west-2.amazonaws.com/articles/solid.jpg)
Autor: 06.12.2023
SOLID Principles in Programming
In programming, you may encounter the popular acronym SOLID. In this article, we'll explain its meaning and the role it plays in object-oriented programming. We'll use examples in the Java language, but the knowledge contained in this article should be universal and applicable to most popular programming languages.
Let's unpack the SOLID acronym
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
Great! Now let's look at these five rules one by one.
Single Responsibility Principle
The first SOLID principle prohibits creating "God" classes. A "God" class does everything. We can't allow a class in our code to have too many responsibilities.
Here's a Java code snippet. Take a look at the Point class:
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void draw() {
System.out.printf("x = %d, y = %d \n", this.x, this.y);
}
}
The Point class stores information about a point and is responsible for drawing it. Notice that at this stage, the class already has too many responsibilities. We'll try to split our class into two, making them flexible and independent.
We split the Point class in line with the SRP:
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
class PointDrawer {
public void draw(Point point) {
System.out.printf("x = %d, y = %d \n", point.getX(), point.getY());
}
}
We've adhered to the first SOLID principle by splitting the Point class into Point and PointDrawer. The Point class handles points, while the PointDrawer class is responsible for displaying points.
Each class is responsible for exactly one thing, avoiding the creation of a "God" class.
Open/Closed Principle
The Open/Closed Principle is the second principle in the SOLID design principles, emphasizing that code should be open for extension but closed for modification. To ensure your code adheres to this principle, ask yourself if it's possible to add new functionality without altering existing code.
For instance:
class Triangle {
public void calculateArea() {
System.out.println("triangle - calculate area");
}
}
class Square {
public void calculateArea() {
System.out.println("square - calculate area");
}
}
class AreaCalculator {
public static void calculate(Object shape) {
if(shape instanceof Triangle) {
((Triangle) shape).calculateArea();
} else if(shape instanceof Square) {
((Square) shape).calculateArea();
}
}
}
The code presented here violates the Open/Closed Principle. Why? Because adding another class (e.g., Rectangle) necessitates enforced changes in the AreaCalculator class. Functionality should be added in a way that does not modify existing classes. In Java, one way to achieve this is by using an abstract class.
Consider the correct implementation:
abstract class Shape {
abstract public void calculateArea();
}
class Triangle extends Shape {
public void calculateArea() {
System.out.println("triangle - calculate area");
}
}
class Square extends Shape {
public void calculateArea() {
System.out.println("square - calculate area");
}
}
class AreaCalculator {
public static void calculate(Shape shape) {
shape.calculateArea();
}
}
Utilizing the abstract class "Shape" makes our code more universal. To avoid issues with modifying the AreaCalculator class, all that's needed is to add another class (e.g., Rectangle) that inherits from the abstract Shape class.
Inheritance is indeed a powerful mechanism. By using this technique, you can create highly versatile code that aligns with the second SOLID principle, OCP (Open/Closed Principle).
Liskov Substitution Principle
The Liskov Substitution Principle (LSP), named after Barbara Liskov, an experienced figure in the IT industry working at MIT, is the third principle in the SOLID design principles. It states that functions using pointers or references to base classes must be able to use objects of derived classes without knowing the specific details of those objects.
In simpler terms: Any derived class should be substitutable for its base class. Inheritance should not cause derived classes to override methods from the base class; they should only extend them if necessary.
Looking at the provided code:
class CoffeeMachine {
public void makeCoffee() {
this.prepareCoffee();
}
public void prepareCoffee() {
System.out.println("Prepare coffee");
}
}
class SugarCoffeeMachine extends CoffeeMachine {
public void makeCoffee() {
System.out.println("Add sugar");
}
}
The above code, as mentioned, violates the Liskov Substitution Principle. The SugarCoffeeMachine class inherits from CoffeeMachine and introduces a new implementation for the makeCoffee() method. This violates the Liskov principle as the method's functionality should extend the functionality of the method in the parent class, not entirely replace it. The SugarCoffeeMachine class is currently only addressing adding sugar, neglecting the coffee preparation, which breaks the intent of the Liskov principle.
A corrected implementation could be:
class CoffeeMachine {
public void makeCoffee() {
this.prepareCoffee();
}
public void prepareCoffee() {
System.out.println("Prepare coffee");
}
}
class SugarCoffeeMachine extends CoffeeMachine {
public void makeCoffee() {
super.makeCoffee();
System.out.println("Add sugar");
}
}
In this corrected version, the SugarCoffeeMachine class contains a makeCoffee() method that extends the identical method in the parent class. It adds the functionality of adding sugar while ensuring that the core functionality of preparing coffee is retained. This adheres to the Liskov Substitution Principle by allowing the derived class to add to the behavior of the base class without completely overriding its essential functionality.
Interface Segregation Principle
The Interface Segregation Principle (ISP) emphasizes that "fat" interfaces—interfaces that try to do too much—can become problematic. It's better to have smaller, more focused interfaces that offer greater flexibility.
interface Printer {
void toPdf();
void toCsv();
void toXls();
}
class GeneralPrinter implements Printer {
@Override
public void toPdf() {
System.out.println("Print pdf");
}
@Override
public void toCsv() {
}
@Override
public void toXls() {
}
}
The example provided initially breaks the fourth SOLID principle. It exhibits a comprehensive interface that's problematic. By implementing such an interface, it forces handling multiple types of print: PDF, CSV, XLS. To achieve flexibility, let's see how such an interface can be implemented in accordance with ISP, the fourth SOLID principle.
In the corrected version applying ISP:
interface PdfPrinter {
void toPdf();
}
interface CsvPrinter {
void toCsv();
}
interface XlsPrinter {
void toXls();
}
class GeneralPrinter implements PdfPrinter {
@Override
public void toPdf() {
System.out.println("Print pdf");
}
}
By breaking down the extensive interface into smaller ones, we don't need to rely on an enormous interface. Instead, we use minimal implementations, which is highly desirable according to ISP. This way, classes only implement the interfaces they need, preventing unnecessary dependencies and providing a more maintainable and flexible design.
Dependency Inversion Principle
The Dependency Inversion Principle (DIP), the final principle in the SOLID acronym, advises against high-level modules depending on low-level modules. It advocates independence from specific implementations. Let's delve into a practical example.
class Runner {
public void training() {
}
}
class Coach {
private Runner runner;
public Coach(Runner runner) {
this.runner = runner;
}
public void manageSportsman() {
this.runner.training();
}
}
In the above implementation, the coach is responsible for training runners. However, this coach is somewhat limited; they can only train runners. What happens if the coach needs to train athletes in long jump or swimming? Handling additional athletes will pose many problems. The Coach class will need modification, leading to dependencies on other classes.
To break free from this specific implementation, abstraction is the key. Let's modify the implementation accordingly:
abstract class Sportsman {
abstract void training();
}
class Runner extends Sportsman {
public void training() {
}
}
class Coach {
private Sportsman sportsman;
public Coach(Sportsman sportsman) {
this.sportsman = sportsman;
}
public void manageSportsman() {
this.sportsman.training();
}
}
Introducing the Sportsman class as an abstraction allows us to detach from the specific type of athlete. Now, the coach can train any athlete based on their own abilities. Isn't that elegant? The Coach class has been implemented in accordance with the Dependency Inversion Principle (DIP), ensuring independence from specific implementations.
Summary
The acronym SOLID represents five software design principles aimed at creating more flexible, readable, and maintainable code:
- S - Single Responsibility Principle (SRP) - Each class should have only one reason to change.
- O - Open/Closed Principle (OCP) - Code should be open for extension but closed for modification.
- L - Liskov Substitution Principle (LSP) - Subtypes must be substitutable for their base types without altering the correctness of the program.
- I - Interface Segregation Principle (ISP) - Prefer smaller, specific interfaces over a large, general one.
- D - Dependency Inversion Principle (DIP) - High-level modules should not depend on low-level modules; both should depend on abstractions.
Understanding and applying these principles can assist programmers in designing more flexible and easier-to-maintain source code.