# A Crash Course in SOLID Principles

The SOLID principles are a set of five design principles in object-oriented programming intended to make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (also known as Uncle Bob) and are considered fundamental guidelines for creating high-quality, robust, and scalable software systems.

SOLID stands for:

S – Single-Responsiblity Principle

O – Open-closed Principle

L – Liskov Substitution Principle

I – Interface Segregation Principle

D – Dependency Inversion Principle

## Single Responsibility Principle (SRP)

To understand this concept in detail, let’s build an Area Calculator application that takes in a list of shapes and calculates their total area.

Let’s start by creating a class to represent our squares, which takes the length of its side as a constructor parameter.

``````class Square {
// Use double for precision with area calculations
private double sideLength;

public Square(double sideLength) {
this.sideLength = sideLength;
}

public double getArea() {
// Calculate area as sideLength squared
return Math.pow(sideLength, 2);
}
}``````

For circles, you will need to know the radius:

``````class Circle {
// Use double for precision with area calculations
private double radius;

public Circle(double radius) {
this.radius = radius;
}

public double getArea() {
// Calculate area using Pi and radius squared
return Math.PI * Math.pow(radius, 2);
}
}
``````

To find the total area covered by all our shapes, we’ll build an `AreaCalculator` class, which will know how to calculate the area of each shape, whether it’s a `Square `or a `Circle`.

``````class AreaCalculator {

private List<Object> shapes;

public AreaCalculator(List<Object> shapes) {
this.shapes = shapes;
}

public double calculateTotalArea() {
double totalArea = 0;
for (Object shape : shapes) {
if (shape instanceof Square) {
Square square = (Square) shape;
totalArea += square.getArea();
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
totalArea += circle.getArea();
} else {
throw new IllegalArgumentException("Unsupported shape type");
}
}
return totalArea;
}

public String printAreaSummary() {
return String.format("Sum of the areas of provided shapes: %.2f\n", calculateTotalArea());
}
}``````

If you look carefully, you’ll see that the `AreaCalculator `class does two things:

• it calculates the area of shapes and
• handles printing the total area.

If you want to display the area information differently, you might need to modify the `printAreaSummary `method. This could involve formatting the output string differently, or even returning the areas for each shape individually instead of a single sum.

The purpose of the `AreaCalculator `should be solely to calculate the area (adhering to the Single Responsibility Principle). The only reason it should ever change is when we want to support more types of shapes.

Therefore, let’s move the printing responsibility to a separate class called `AreaPrinter`.

``````class AreaPrinter {

// Use final for immutability
private final AreaCalculator calculator;

public AreaPrinter(AreaCalculator calculator) {
this.calculator = calculator;
}

public String getJSON() {
// Use Map for clearer data structure
Map<String, Double> data = new HashMap<>();
data.put("sum", calculator.calculateTotalArea());
// Use appropriate JSON encoder library
return JsonUtils.encodeToString(data);
}

public String getHTML() {
return String.format(
"Sum of the areas of provided shapes: %.2f\n",
calculator.calculateTotalArea());
}
}``````

Now, our `AreaCalculator `class does one thing and one thing only.

``````class AreaCalculator {

private List<Object> shapes;

public AreaCalculator(List<Object> shapes) {
this.shapes = shapes;
}

public double calculateTotalArea() {
double totalArea = 0;
for (Object shape : shapes) {
if (shape instanceof Square) {
Square square = (Square) shape;
totalArea += square.getArea();
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
totalArea += circle.getArea();
} else {
throw new IllegalArgumentException("Unsupported shape type");
}
}
return totalArea;
}
}``````

## Open/Closed Principle (OCP)

The class `AreaCalculator `we wrote follows the Single responsibilty Principle well but it fails to follow the open/closed principle because:

• Each new shape requires modifying `calculateArea`. (modification is open)
• Conditional logic becomes complex as more shapes are added. (modification is open)

To make our class OCP-compliant, we should first close the possibility of modification of `calculateArea `method which we can do using the below steps:

#### 1. Define a Shape interface

``````interface Shape {
double getArea();
}``````

#### 2. Create Concrete Shape classes

``````class Square implements Shape {
// Use double for precision with area calculations
private double sideLength;

public Square(double sideLength) {
this.sideLength = sideLength;
}

public double getArea() {
// Calculate area as sideLength squared
return Math.pow(sideLength, 2);
}
}

class Circle implements Shape {

private double radius; // Use double for precision with area calculations

public Circle(double radius) {
this.radius = radius;
}

public double getArea() {
return Math.PI * Math.pow(radius, 2); // Calculate area using Pi and radius squared
}
}
``````

#### 3. Modify the AreaCalculator class

``````class AreaCalculator {

private List<Shape> shapes;

public AreaCalculator(List<Shape> shapes) {
this.shapes = shapes;
}

public double calculateTotalArea() {
double totalArea = 0;
for (Shape shape : shapes) {
totalArea += shape.getArea();
}
return totalArea;
}
}``````
• `AreaCalculator `is now closed for modification, open for extension. We can extend this class further and add new methods or override the existing method.
• Adding new shapes doesn’t require changing `AreaCalculator `class, it will be automatically supported.
• Code is more maintainable, testable, and extensible.

## Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) plays a crucial role in ensuring the flexibility of software systems. Named after Barbara Liskov, who introduced the principle in 1987, LSP is one of the five SOLID principles that aim to improve the design and maintainability of software.

At its core, the Liskov Substitution Principle articulates the idea of substitutability between objects of different types within an inheritance hierarchy. The principle asserts that if a class (let’s call it S) is a subtype of another class (let’s call it T), instances of class T should be replaceable with instances of class S without altering the correctness of the program. In simpler terms, a child class should seamlessly replace its parent class without introducing errors or modifying the expected behavior of the system.

Imagine a scenario where you have a base class representing a general shape and a derived class representing a specific type of shape, say a square. According to LSP, if square (S) is a subtype of shape (T), you should be able to substitute an instance of the square class wherever an instance of the shape class is expected, without causing any issues.

Let’s consider a scenario involving a `Rectangle `and a `Square`, where `Square `is a subclass of `Rectangle`. According to classical geometry, a square is a special case of a rectangle where all sides are of equal length. However, implementing this relationship directly might lead to a violation of LSP.

``````class Rectangle {
protected int width;
protected int height;

public void setWidth(int width) {
this.width = width;
}

public void setHeight(int height) {
this.height = height;
}

public int getArea() {
return this.width * this.height;
}
}

class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}

@Override
public void setHeight(int height) {
this.height = height;
this.width = height;
}
}``````

In this scenario, a `Square `is modeled as a subclass of `Rectangle`. However, by overriding the `setWidth() `and `setHeight()` methods in the `Square `class to set both dimensions to the same value, we violate the LSP. This is because a `Square `behaves differently than a `Rectangle `by constraining both width and height to the same value, which is not consistent with the behavior expected from a `Rectangle`.

#### Fixing the Code using LSP

To adhere to the Liskov Substitution Principle, it’s essential to reconsider the relationship between `Square `and `Rectangle`. Instead of making `Square `a subclass of `Rectangle`, let’s refactor the code to remove this inheritance relationship and create separate classes for `Square `and `Rectangle `that do not violate the behavior of each other:

``````class Rectangle {
protected int width;
protected int height;

public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}

public int getArea() {
return this.width * this.height;
}
}

class Square {
private int side;

public Square(int side) {
this.side = side;
}

public int getArea() {
return this.side * this.side;
}
}``````

In this updated code, both `Rectangle `and `Square `are separate classes with distinct behaviors. Each class calculates its area based on its specific properties without inheriting from each other. This design adheres to LSP by ensuring that instances of `Rectangle `and `Square` do not alter each other’s behavior when used interchangeably.

Why is LSP important? The principle contributes to code reusability, extensibility, and maintainability. When adhering to LSP, developers can confidently extend existing classes without fear of introducing unexpected behaviors or errors. This promotes a modular and scalable codebase, allowing for easier updates and modifications.

## Interface Segregation Principle (ISP)

Imagine you’re building a house. Would you rather have a single, oversized toolbox containing every possible tool you might need, or several smaller, specialized toolkits for specific tasks like carpentry, plumbing, and electrical work? The answer is obvious: smaller focused toolkits make your work more efficient and organized.

The same principle applies to software design also. The Interface Segregation Principle (ISP) tells you to break down large interfaces into smaller, more focused ones based on functionality. This leads to cleaner and more maintainable code. Take for example, the below Fat interface.

``````interface Machine {
void print();
void scan();
void fax();
}

class MultiFunctionPrinter implements Machine {
@Override
public void print() {
// Printing implementation
}

@Override
public void scan() {
// Scanning implementation
}

// Forced to implement fax(), even though it's not used
@Override
public void fax() {
// Do nothing or throw an exception
}
}``````

The fat `Machine `interface forces clients (classes implementing the interface) to carry around methods they don’t need, adding unnecessary complexity and weight. This can lead to:

• Increased coupling: Clients become dependent on the entire interface, even for unused methods. This makes them less flexible and harder to reuse in different contexts.
• Code bloat: Unnecessary methods clutter up the codebase, making it harder to understand and maintain.
• Confusion and errors: Developers implementing the interface might mistakenly use the wrong method, leading to unexpected behavior and bugs.

Lets split up the Fat interface into a thinner cohesive interface.

``````interface Printable {
void print();
}

interface Scannable {
void scan();
}

interface Faxable {
void fax();
}

class MultiFunctionPrinter implements Printable, Scannable {
@Override
public void print() {
// Printing implementation
}

@Override
public void scan() {
// Scanning implementation
}
}

class SimplePrinter implements Printable {
@Override
public void print() {
// Printing implementation
}
}``````

By splitting up fat interfaces into smaller, cohesive interfaces based on functionality, we achieve several benefits:

• Improved clarity: Each interface clearly defines a specific set of related functionalities, making it easier to understand and use.
• Reduced coupling: Clients only need to implement the interfaces that provide the methods they actually use, leading to looser coupling and increased reusability.
• Enhanced maintainability: Smaller interfaces are easier to modify and evolve without impacting other parts of the codebase.
• Developer satisfaction: Working with focused interfaces is more efficient and enjoyable for developers, leading to better code quality and productivity.

The Interface Segregation Principle helps you break down fat interfaces into smaller, focused ones, improving code clarity, and ultimately create software that is easier to understand, use, and maintain.

## Dependency Inversion Principle (DIP)

Dependency Inversion Principle states that “High-level modules should not depend on low-level modules. Both should depend on abstractions.” In simpler terms, your code shouldn’t be directly tied to specific implementations. Instead, it should rely on abstract interfaces or base classes that define the desired behavior, allowing for flexibility and adaptability.

``````class HighLevelModule {
private LowLevelModule lowLevelModule = new LowLevelModule();

public void doSomething() {
lowLevelModule.specificOperation();
}
}``````

You can see that:

• `HighLevelModule `is tightly coupled to `LowLevelModule`, making it difficult to change or test independently.
• Introducing a new `LowLevelModule `implementation would require code changes in `HighLevelModule`.

To make the above functionality Dependency Inversion Principle compliant, we redesign our class as shown below:

``````interface Abstraction {
void operation();
}

class LowLevelModule implements Abstraction {
@Override
public void operation() {
// Specific implementation
}
}

class HighLevelModule {
private Abstraction abstraction;

public HighLevelModule(Abstraction abstraction) {
this.abstraction = abstraction;
}

public void doSomething() {
abstraction.operation();
}
}``````

Now, you can see that:

• `HighLevelModule `depends on the `Abstraction `interface, not the concrete `LowLevelModule`.
• Concrete implementations can be injected through the constructor, allowing flexibility and testability.
• New implementations can be introduced without modifying `HighLevelModule`.

A key consideration in the development of real-time applications is maintaining a loose coupling between High-level and Low-level modules. When a class possesses knowledge about the design and implementation of another class, it introduces the risk that any changes made to one class might disrupt the functionality of the other. Therefore, it is crucial to ensure loose coupling between high-level and low-level modules/classes. Achieving this involves making both modules dependent on abstractions rather than having direct knowledge or instance of each other.

## Bibliography

Martin, Robert, and Sommerville, Ian. Value Pack: Software Engineering with Agile Software Development, Principles, Patterns and Practices. United Kingdom,Pearson Education, Limited, 2004.