Solid refers to five design principles in Object-Oriented Programming. It is designed to reduce code rot and improve software's value, functionality and maintainability. The Solid principles help developers create code with less coupling. When code is tightly coupled, a group of classes depends on one another. It is important to avoided coupling for better maintainability and readability.
Types of Solid Principles:
Single Responsibility
Open/Closed
Liskov Substitution
Interface Segregation
Dependency Inversion
This blog will go through each and every principle one by one.
1. Single Responsibility(SRP).
A class should have only one reason to change. This means that every class, or similar structure, in your code should have only one job to do.
Why is it important? • Helps make code easier to understand and maintain. • Reduces the risk of introducing bugs when making changes.
Let's say we're building a simple online shopping system, and we have a Order class that represents an order in the system. The Order class might have two responsibilities: managing the order details and sending email notifications to the customer. However, this violates the SRP because it has more than one reason to change (e.g., if the order logic or email sending logic needs to change).
Let’s explore the concept with a quick code example:
Original Code:
public class Order {
private int orderId;
private List<Item> items;
private Customer customer;
public Order(int orderId, List<Item> items, Customer customer) {
this.orderId = orderId;
this.items = items;
this.customer = customer;
}
public double calculateTotalPrice() {
// Calculate the total price of items in the order
double totalPrice = 0;
for (Item item : items) {
totalPrice += item.getPrice();
}
return totalPrice;
}
public void sendEmailNotification() {
// Send an email notification to the customer about the order
String emailMessage = "Dear " + customer.getName() + ", your order #" + orderId + " has been placed.";
EmailService.sendEmail(customer.getEmail(), "Order Confirmation", emailMessage);
}
}
In this example, the Order class has two responsibilities:
Managing the order details (e.g., calculating the total price).
Sending email notifications to the customer.
To adhere to the Single Responsibility Principle, we can split the class into two separate classes, each with its own single responsibility:
Improved SRP Code:
public class Order {
private int orderId;
private List<Item> items;
private Customer customer;
public Order(int orderId, List<Item> items, Customer customer) {
this.orderId = orderId;
this.items = items;
this.customer = customer;
}
public double calculateTotalPrice() {
// Calculate the total price of items in the order
double totalPrice = 0;
for (Item item : items) {
totalPrice += item.getPrice();
}
return totalPrice;
}
}
public class EmailService {
public void sendEmailNotification(Customer customer, int orderId) {
// Send an email notification to the customer about the order
String emailMessage = "Dear " + customer.getName() + ", your order #" + orderId + " has been placed.";
sendEmail(customer.getEmail(), "Order Confirmation", emailMessage);
}
private void sendEmail(String to, String subject, String message) {
// Logic to send the email
}
}
Now, we have separated the responsibilities: Order class is responsible for managing order details, and EmailService class is responsible for sending email notifications.
2. Open/Closed Principles
It is now time for the Open/Closed Principle, Classes should be open for extension but closed for modification.
“Open for extension” means that you should design your classes so that new functionality can be added as new requirements are generated.
“Closed for modification” means that once you have developed a class you should never modify it, except to correct bugs.
Here's a simple example in java:
Suppose you have a program that sorts different types of shapes (e.g., circles, squares, triangles). You want to follow the Open/Closed Principle to add new shapes for sorting without changing the existing sorting code.
Original Code:
class Circle {
// Circle-specific properties and methods
}
class Square {
// Square-specific properties and methods
}
class Triangle {
// Triangle-specific properties and methods
}
class ShapeSorter {
public void sortShapes(Shape[] shapes) {
// Sorting logic for shapes
// Code to sort circles, squares, triangles, etc.
}
}
Issue with the Original Code: If you want to add a new shape (e.g., a pentagon), you would need to modify the ShapeSorter class to accommodate the new shape. This violates the OCP because you're modifying existing code.
Improved OCP Code:
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
}
}
public void draw() {
// Drawing logic for a square
}
}
class Triangle implements Shape {
public void draw() {
// Drawing logic for a triangle
}
}
class ShapeSorter {
public void sortShapes(Shape[] shapes) {
// Code to sort circles, squares, triangles, pentagons, etc.
}
}
Advantages of the Improved OCP Code:
We introduced a common Shape interface that all shapes must implement, defining a draw() method.
Now, if you want to add a new shape (e.g., a pentagon), you simply create a new class that implements the Shape interface. You don't need to modify the existing ShapeSorter class.
The existing code is "closed" for modification but "open" for extension, adhering to the Open/Closed Principle.
3. Liskov Substitution Principle(LSP)
Next on our list is Liskov Substitution Principle, It's a rule in programming that says if you have a base class (Parent Class) and a derived class(Child Class), you should be able to use objects of the derived class wherever you use objects of the base class.
In other words, the derived class should be just like the base class and should work smoothly without causing any unexpected issues or surprises.
When you create a new class that inherits from another class, it should follow the same rules and behaviors as the class it inherits from.
Here's a simple example in java:
public class Student
{
private double height;
private double weight;
public void setHeight(double h)
{
height = h;
}
public void setweight(double w)
{
weight= w;
}
...
}
//second class
public class StudentBMI extends Student
{
public void setHeight(double h)
{
super.setHeight(h);
}
public void setWeight(double w)
{
super.setWeight(w);
}
}
This is the explanation of above program:
Student Class
This class is like a blueprint for creating students. Each student has a height and weight.
It provides two methods: setHeight to set a student's height and setWeight to set their weight.
2. StudentBMI Class
This class is a special kind of student, maybe one that's interested in calculating BMI.
It's like a student but with some extra features.
It uses the same setHeight and setWeight methods as regular students, but it doesn't change how they work. It just inherits these methods from the regular student class.
So, the StudentBMI class is like a student with a focus on BMI, and it shares some behaviors with regular students. It's a way to organize and reuse code for students with and without BMI calculations.
4. Interface Segregation Principle(ISP)
Next is on our list is Interface Segregation Principle, ISP states that clients should not be forced to depend upon interface members they do not use. Any unexpected behavior or exceptions in a subclass violate Liskov Substitution Principle(LSP) and can lead to issues when substituting objects.
Derived classes should never provide less functionality than their base classes.
Why is it important?
Make interfaces in a software program smaller and more specific, like having separate tools for different tasks.
Don't add extra stuff to these interfaces that you don't really need, to keep them from becoming too complicated.
Here's simple example in java:
Original Code:
public interface Shape {
void draw();
void resize();
void move();
}
Problem with the Example: The Shape interface has three methods: draw, resize, and move.
If a class only needs to draw shapes but doesn't resize or move them, it's forced to implement unnecessary methods, violating ISP.
Improved ISP Code:
public interface Drawable {
void draw();
}
public interface Resizable {
void resize();
}
public interface Movable {
void move();
}
Solution:
Split the large interface into smaller, more specific interfaces based on related functionality.
In summary, the Interface Segregation Principle (ISP) suggests breaking down large interfaces into smaller, more specialized ones so that classes can choose to implement only the interfaces that are relevant to their functionality. This leads to cleaner and more focused code.
5. Dependency Inversion Principle(DIP)
This is the last principle of the SOLID Principles, The Dependency Inversion Principle (DIP) states that high-level modules should not depend upon low-level modules; they should depend on abstractions.
Abstractions (interfaces or abstract classes) should be used to define contracts and should not rely on implementation details.
Benefits of DIP:
Enhanced Code Flexibility
Improved Testability
Code Maintainability
Here's simple example in java:
Original Code:
public class LightBulb {
public void turnOn() {
}
}
public class LightSwitch {
private LightBulb bulb = new LightBulb();
public void operate() {
bulb.turnOn();
}
}
Problem with the Example: The LightSwitch class depends directly on the concrete implementation of LightBulb. This creates a tight coupling.
Improved DIP Code:
public interface Switchable {
void turnOn();
void turnOff();
}
public class LightBulb implements Switchable {
public void turnOn() {
}
public void turnOff() {
}
}
public class LightSwitch {
private Switchable device;
public LightSwitch(Switchable device) {
this.device = device;
}
public void operate() {
device.turnOn();
}
}
Solution:
Use Interface or abstractions to invert the dependency.
In summary, the Dependency Inversion Principle (DIP) suggests that you should depend on abstractions or interfaces rather than concrete implementations. This helps decouple different parts of your code and makes it more adaptable and easier to maintain.
תגובות