SOLID Principles

Description

SOLID is a set of five principles that help developers design object-oriented code that's easier to understand, maintain, and extend. They were introduced by Robert C. Martin to promote better software design.

S - Single Responsibility Principle (SRP)

This principle states that a class should have only one reason to change, meaning that it should have only one responsibility or job. If a class does more than one thing, it becomes harder to understand, maintain, and reuse.

Example 1: Consider a class called Employee that manages both employee information and payroll calculations. According to SRP, this violates the principle because the class has multiple responsibilities. Instead, we could split it into two classes: one for managing employee information (Employee) and another for handling payroll calculations (Payroll).

Example 2: Imagine a class UserManager that's responsible for both creating new users and sending them welcome emails. If the logic for sending emails changes, we'd need to modify the UserManager class even though its core functionality of creating users remains the same. This violates SRP. A better approach would be to have separate classes: UserManager for creating users and EmailService for sending emails.

O - Open/Closed Principle (OCP)

This principle suggests that software entities (such as classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, we should be able to extend the behavior of a module without modifying its source code.

Example: Suppose we have a class Shape with a method calculateArea(), and we want to add support for new shapes without modifying the Shape class. We can achieve this by creating a new subclass for each shape (e.g., Circle, Square) and implementing the calculateArea() method in each subclass. This way, we extend the behavior without modifying existing code.

L - Liskov Substitution Principle (LSP)

Named after Barbara Liskov, this principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In simpler terms, if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.

Example: Consider a Rectangle class with a setWidth and setHeight method. We might have a Square class that inherits from Rectangle. However, if we try to set a different width and height for a Square object, it would violate its square property. LSP ensures that subclasses adhere to the contract established by their superclass. In this case, the Square class should have its own logic to ensure both width and height are always the same.

Incorrect (LSP Violation):Correct (LSP Adherence)

I - Interface Segregation Principle (ISP)

This principle emphasizes that clients should not be forced to depend on interfaces they do not use. It suggests that you should break interfaces that are too large into smaller, more specific ones so that clients only need to know about the methods that are relevant to them.

Example 1: Consider an interface called Worker that has methods for both manual labor (workWithHands()) and office work (workWithComputer()). However, not all classes that implement Worker need to perform both types of work. Instead, we can split the interface into smaller, more specific interfaces like ManualWorker and OfficeWorker, allowing classes to implement only the methods they need.

Example 2: Imagine an interface Animal with methods for makeSound(), fly(), and swim(). A Fish class would only implement swim(), but it would still be forced to implement the empty makeSound() and fly() methods. ISP suggests creating smaller, more specific interfaces like Swimmer with just a swim() method. This reduces unnecessary code for classes like Fish.

D - Dependency Inversion Principle (DIP)

This principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. It also suggests that abstractions should not depend on details; rather, details should depend on abstractions. This helps in achieving decoupling and allows for easier changes and substitutions.

Example 1: Suppose we have a FileLogger class that logs messages to a file. Instead of directly creating an instance of FileLogger within another class, we can use dependency injection to pass an instance of Logger (an abstraction) to the dependent class. This way, the dependent class doesn't depend on the concrete implementation (FileLogger), but on an abstraction (Logger). This makes it easier to switch to a different logging mechanism in the future without modifying the dependent class

Example 2: Let's say a class PaymentProcessor directly depends on a specific payment gateway like PayPalGateway to process payments. If we want to switch to a different gateway (e.g., StripeGateway), we'd need to modify the PaymentProcessor class. DIP suggests using an abstraction like a PaymentGateway interface that both PayPalGateway and StripeGateway implement. The PaymentProcessor would then depend on the PaymentGateway interface, allowing us to switch between gateways easily without modifying the core logic.

Last updated