Solid Principles are a set of software design principles, which must be followed to develop flexible, maintainable and scalable software systems.
Design Principles aren’t concrete like Design Patterns. These are core abstract principles that programmers are supposed to follow while developing applications. SOLID is an acronym for 5 Core Software Design principles that every Programmer should know.
1. Single Responsibility Principle (SRP)
A class should have only one reason to change, meaning it should only have one job or responsibility.
// Violates SRP
class Report {
public void generateReport() {
// Generate report logic
}
public void saveToFile() {
// Save report to file logic
}
}Code Explanation:
Violating SRP: The Report class is responsible for both generating a report and saving it to a file. If the saving mechanism changes (e.g., to save in a different format), you’ll have to modify this class, which violates SRP.
Following SRP: We split the functionality into two classes: Report (which only generates reports) and ReportSaver (which handles saving reports). This way, changes in one class won’t affect the other, adhering to SRP.
// Follows SRP
class Report {
public void generateReport() {
// Generate report logic
}
}
class ReportSaver {
public void saveToFile(Report report) {
// Save report to file logic
}
}2. Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
We should be able to add new functionality without changing existing classes, modules, functions, etc.
// Base class
abstract class Shape {
abstract double area();
}
// New shape class added
class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
// New shape class added
class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
double area() {
return width * height;
}
}Code Explanation:
Base Class: The Shape class is abstract, allowing for different shapes (like Circle and Rectangle) to be added later without modifying the existing code.
New Shape Classes: Adding new shape types (like Circle and Rectangle) involves creating new classes that extend Shape. The area() method is implemented specifically for each shape, ensuring that existing code remains unchanged.
3. Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering the correctness of the program.
This means derived classes should enhance, not replace, the behaviour of the base class.
class Bird {
public void fly() {
// Flying logic
}
}
class Sparrow extends Bird {
@Override
public void fly() {
// Sparrow flying logic
}
}
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich can't fly!");
}
}Code Explanation:
Violating LSP: Consider a class Bird with a method fly(). The Ostrich class inherits from Bird but throws an exception when trying to call fly(), which violates LSP because it doesn’t fulfil the expected behaviour of a Bird as per this design. Instead of forcing all birds to implement fly(), it is better to separate birds into categories.
Following LSP: Let us introduce an interface, Flyable, that will be implemented by flying birds only (like Sparrow, Parrot etc.). Birds like Ostrich, Penguin etc. will no longer inherits from Bird in a way that enforces flying, thus adhering to LSP.
// Better design
abstract class Bird {
// Common behavior
}
interface Flyable {
void fly();
}
class Sparrow extends Bird implements Flyable {
@Override
public void fly() {
// Sparrow flying logic
}
}
class Ostrich extends Bird {
// Ostrich-specific behavior
}
class Penguin extends Bird {
// Penguin-specific behavior
}So now, Each class has its own behaviour without breaking the contract of the base class.
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
This encourages creating smaller, more specific interfaces instead of large ones.
// Violates ISP
interface Worker {
void work();
void eat();
}
class Employee implements Worker {
@Override
public void work() {
// Employee work logic
}
@Override
public void eat() {
// Employee eat logic
}
}
class Robot implements Worker {
@Override
public void work() {
// Robot work logic
}
@Override
public void eat() {
throw new UnsupportedOperationException("Robot doesn't eat!");
}
}Code Explanation:
Violating ISP: The Worker interface combines methods that apply to both Employee and Robot. Since Robot cannot eat, it’s forced to implement a method that doesn’t make sense for it, violating ISP.
Following ISP: We separate the responsibilities into two interfaces: Workable and Eatable. Now, Employee implements both interfaces, while Robot only implements Workable. This way, each class only needs to implement the methods relevant to it.
// Follows ISP
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Employee implements Workable, Eatable {
@Override
public void work() {
// Employee work logic
}
@Override
public void eat() {
// Employee eat logic
}
}
class Robot implements Workable {
@Override
public void work() {
// Robot work logic
}
}
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules.
Both should depend on abstractions (like interfaces).
Consider a class LightBulb that can be turned ON.
// Violates DIP
class LightBulb {
public void turnOn() {
// Logic to turn on the light
}
}
class Switch {
private LightBulb bulb;
public Switch(LightBulb bulb) {
this.bulb = bulb;
}
public void operate() {
bulb.turnOn();
}
}Code Explanation:
Violating DIP: The Switch class directly depends on the LightBulb class. If we want to change the type of the light source (e.g., using a LEDLightBulb), we would have to modify the Switch class.
Following DIP: We create an interface Switchable that defines the turnOn() method. Now, the Switch class depends on this abstraction rather than a concrete class. This makes it easy to change or extend the types of light sources without modifying the Switch class, adhering to DIP.
// Follows DIP
interface Switchable {
void turnOn();
}
class LightBulb implements Switchable {
@Override
public void turnOn() {
// Logic to turn on the light
}
}
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void operate() {
device.turnOn();
}
}By adhering to the SOLID principles, the code becomes more maintainable, flexible, and easier to understand.
