Introduction to SOLID principles in object-oriented programming
Introduction to SOLID principles in object-oriented programming
SOLID principles lie at the heart of (Object-Oriented Programming (OOP)), which has made them the foundation of clean, maintainable, and scalable code. The five principles proposed by **Robert C. Martin**, otherwise known as Uncle Bob, guide developers toward easier understanding, testing, and modification of the code. In the following blog post, we are going to introduce the SOLID principles, explain each of them and provide examples in object-oriented languages like **Java** and **Python**.
What are SOLID Principles?
SOLID is an acronym that represents five design principles which need to be recalled for better designs in software engineering:
1. **S**: SRP (Single Responsibility Principle)
2. **O**: OCP (Open/Closed Principle)
3. **L**: LSP (Liskov Substitution Principle)
4. **I**: ISP (Interface Segregation Principle)
5. D : DIP (Dependency Inversion Principle)
If you apply these principles you avoid many complex systems, prevent code duplication and you always end up with a changeable codebase for changing requirements.
1. SRP — Single Responsibility Principle
Definition: There should be no more than one reason for a class to change: that is, no more than one job, or responsibility.
The single responsibility principle promotes **modularity**, because each class or module should have a special purpose. It becomes difficult to maintain and modify a class when it performs more than one responsibility. That is because a change in one responsibility probably affects another responsibility or two.
Needless to say, maintenance and modification are easier when a class has only one responsibility.
We use two classes: one to manage user’s data and the other to write file operation.
```java
public class UserManager {
public void addUser(User user) {
// add logic of saving the user here
}
Nullable
public void saveUserToFile(User user) {
`` mtx
//// logic to save the user to the file
}
}
END
This above code shows that class `UserManager` is performing two major responsibilities that are, users management and file management. So, it violates SRP. We can further split this responsibility into two classes as shown below.
```java
public class UserManager { public void addUser(User user) {
//\\\”}
// logic for adding user
}
}
class FileManager {
/**public void saveUserToFile(User user) {
```
end
// logic for saving user to file
}
``
Now each class has just one responsibility, therefore code is much easier in maintenance and extension.
— -
2. Open/Closed Principle (OCP)
. DEFINITION Software entities(classes, modules, functions, etc.) should be open for extension but closed for modification.
This helps you introduce new functionality into your system with no changed existing code. It is realized using polymorphism and abstraction. The underlying idea is that you can extend some behavior with new code yet avoid changing existing code. And that is how you reduce the probability of breaking existing functionality.
example (Python)
Imagine that you had a class that computed the rectangle area:
```
class Rectangle:
PASS
def __init__ (self, width, height):
END
pass
def area(self):
pass
PASS
Imagine now that we want to compute areas for different kinds of shapes, such as circles or triangles. The most natural way to modify this class would be a violation of OCP; instead we could do that using inheritance, extend the behaviour.
```python
class Shape:
def area(self)
```
pass
class Rectangle(Shape):
def __init__(self, width, height ):
self.width = width
#self.height = height
#
def area(self):
“””
return self.width * self.height
#
#
class Circle(Shape):
def __init__(self, radius ):
self.radius = radius
#
#
def area(self):
return 3.1416 * self.radius ** 2
#
We don’t need to inherit from the base class Shape to define new shapes.
#
— -///
3. Liskov Substitution Principle (LSP)
Definition: For subtypes, the correctness of program behaviour must be preserved when replacing their base types.
This is called the Liskov Substitution Principle (LSP): Derived classes shall be substitutable for their base classes. That is, derived classes can always be used in place of their base classes without altering the expected behavior of the program. Design such that the Liskov Substitution Principle is violated leads to rather nasty surprises when the derived types behave otherwise than what the base class expects.
Example (Java)
We have a class called `Bird` and further extend to create a class called `Penguin`.
```java
public class Bird {
public void fly() {}
// flying logic
}
}public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException(“Penguins can’t fly!”);
}
}
END
This breaks the LSP because `Penguin` can’t actually behave as `Bird` is supposed to: fly(). We can either accept it or: Refuse to make `Penguin` extend `Bird`; Rethink classes so that it is actually possible that not all birds can fly.
4. Interface Segregation Principle (ISP)
Clients shouldn’t be forced to implement interfaces they don’t use.
Encapsulation and access control The ISP favors breaking down big complex interfaces into smaller, more specialized ones. A class implementing an interface doesn’t have to declare methods in which it isn’t interested. In this way flexibility is ensured, and the system remains modular.
Example (Python)
We declare an interface for different kinds of printers:
```
pass
#
# def fax_document(self, document):
# pass
#
#
#
# forallgebraPrinter
#
# technological advancement of
by cramping all that the unnecessary junk into an extremely oversimplified printer which only needed to print a few documents, with nothing to scan or fax, we are actually forcing it to come up with ways that it will never employ. We can break up this interface into sub-interfaces:
```python
class Printer:
```
def print_document(self, document):
pass
class Scanner:
def scan_document(self, document):
pass
class FaxMachine:
def fax_document(self, document):pass```’
Now classes can depend only on those interfaces in which they are interested, following the ISP.
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Instead they should depend on abstractions. And abstractions should not depend upon details-they should depend upon abstractions.
A Dependency Inversion Principle is the intention to break a coupling between high and low parts of a program by depending on abstractions, such as using interfaces instead of concrete implementations.
Example (Java):
Here’s an abuse of DIP wherein the high-level `NotificationService` class has a direct dependency on a low-level `EmailService` class:
```java
public class EmailService {
public void sendEmail(String message) {
END
// implementation
}
}
```END
//logic to send email
}
}
public class NotificationService {
private EmailService emailService;
public NotificationService() {
this.emailService = new EmailService();
}
public void sendNotification(String message) {
emailService.sendEmail(message);
}
}
```
For example, to have DIP we can introduce an abstraction (NotificationSender) which `EmailService` implements. Now `NotificationService` depends on abstraction not on concrete class:
(emailnotification-service-java)
```java
public interface NotificationSender {
END
void send(String message);
}
public class EmailService implements NotificationSender {
.
public void send(String message) {
// logic to send email”
END
}
}
class public class NotificationService {
private NotificationSender sender;
public NotificationService(NotificationSender sender) {
this.sender = sender;
}
public void sendNotification(String message) {
sender.send(message);
}
}
END
Now, `NotificationService` can return the response by being able to use a notification sender of any kind, be it an SMS or push notification, which does not depend on the use of emails.
The SOLID principles enable one to write clean, maintainable and scalable object oriented code. Systems that follow the SOLID principles will undoubtedly be flexible, more extensible and less buggy. Whether you are designing a new application or refactoring existing code-thus embracing SOLID principles will drastically improve your quality of code.
Here is a quick recap:
S- Single Responsibility Principle (SRP)
O- Open/Closed Principle (OCP)
Liskov Substitution Principle (LSP)
Interface Segregation Principle (ISP)
Dependency Inversion Principle (DIP)
Try to use them in your OOP designs and find out how the code starts cleaning itself after sometime, even self-maintaining in the process.
END