Explanation of SOLID in OOP
Introduction
SOLID is a set of five fundamental principles that support enhancing maintainability and ease of extension for future software development. Introduced by software engineer Robert C. Martin, also known as "Uncle Bob," in the book "Design Principles and Design Patterns," the SOLID principles include:
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
Below, we'll provide detailed explanations and analysis for each principle.
Note that the examples in this article are implemented using TypeScript, but you can rewrite them in other object-oriented programming languages.
1. Single Responsibility Principle (SRP)
A class should have one and only one reason to change, meaning that a class should have only one job.
This is considered the simplest and most crucial principle because it relates to most of the other principles. Simply put, when implementing a class/method, it should serve only one specific task. If it has more than one responsibility, it's advisable to split those responsibilities into multiple classes or methods. This practice benefits future maintenance, as when there's a need to modify the functionality of a class/method, we only need to make changes within that class/method without affecting others in the application.
Below is an example illustrating a violation of this principle.
Here's an example showing a violation of the SRP principle because the Car class has too many unrelated functions like formatting info and storing data. These functions should be split into different classes to make things clearer and reduce the complexity of the source code.
2. Open/Closed Principle (OCP)
Objects or entities should be open for extension but closed for modificationThis principle means that a class should be designed in a way that allows new functionality to be added without altering its existing code. To achieve this, we can utilize inheritance, interfaces, or composition.
In the example above, the Shape interface defines a method called calculateArea used to calculate the area of a shape. Both the Circle and Rectangle classes, when implementing this interface, must define their own way of calculating the area for each shape.
This implementation approach is beneficial for future extension. If we add more shapes in the future (such as triangles, quadrilaterals, etc.), we only need to implement the Shape interface similarly and define the method to calculate the area for each shape, without needing to modify the existing classes. This avoids disrupting the existing logic of the system.
3. Liskov Substitution Principle (LSP)
This principle was proposed by Barbara Liskov in 1987.
Its essence is as follows:
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.In simpler terms for object-oriented programming, the principle is understood as:
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
Below is an example illustrating a violation of LSP. In reality, we know that a square is a type of rectangle with equal width and height. However, when implementing methods in the Square class that violate the behavior of the Rectangle class, it means that LSP is being violated.
In this example, when the Square class implements the setWidth and setHeight methods from the Rectangle class, it violates the LSP because it changes both the width and height to be equal.
To ensure that the program doesn't violate the LSP, it's better to create a parent class, such as the Shape class, and then have both Square and Rectangle inherit from that class.
Additional Note
This principle is highly abstract and prone to violation if you don't fully understand the concept. In object-oriented programming, we often build classes based on real-life concepts and objects, such as "a square is a type of rectangle" or "a penguin is a bird." However, you can't directly translate these relationships into source code. Remember, "In real life, A is B (a square is a rectangle), but it doesn't necessarily mean that class A should inherit from class B. Class A should only inherit from class B if class A can substitute for class B."
4. Interface Segregation Principle (ISP)
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.The ISP encourages breaking down interfaces into smaller parts so that classes don't have to implement unrelated methods. This helps reduce dependency on unnecessary methods and makes the source code more flexible, easier to extend, and maintain.
Here's an example of a violation of the ISP:
Because the Animal interface has many methods, and some methods may not be applicable to certain species of animals. When the Fish and Bird classes implement the Animal interface, they have to implement all methods, including unnecessary ones. This leads to wasted effort and increases the complexity of the program unnecessarily.
The solution is to split the Animal interface into smaller interfaces as follows:
5. Dependency Inversion Principle (DIP)
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions
In simpler terms:
- High-level modules should not rely on low-level modules; both should rely on abstractions.
- Abstractions should not depend on details; details should depend on abstractions.
Here's an example of creating a DataExporter class that allows exporting data based on the provided `Exporter` (either ExcelExporter or PdfExporter). The `export` method is defined in the Exporter interface, making it easy to create additional exporters for use without needing to change the current source code.
It's important to note that the Dependency Inversion Principle differs from Dependency Injection because Dependency Inversion is a principle, while Dependency Injection is a design pattern. Dependency Injection is just one of the ways to implement Dependency Inversion.
Conclusion
The 5 SOLID principles can be implemented in most object-oriented programming languages like Java, C#, TypeScript, JavaScript, Python, etc. SOLID provides a foundational framework that helps developers build source code that is easy to understand, flexible, easily extendable, enhances system maintainability, and minimizes the risk of issues.
If you have any suggestions or questions regarding the content of the article, please don't hesitate to leave a comment below!
Comments
Post a Comment