# SOLID Design Principles

### Introduction

Robert C. Martin first established the five design concepts that make up SOLID in the early 2000s. These standards are regarded as the cornerstones for developing scalable and maintainable software systems. They are extensively utilised in object-oriented programming and can be leveraged by TypeScript developers.

**The SOLID principles include:**

* Single Responsibility Principle (SRP)
    
* Open/Closed Principle (OCP)
    
* Liskov Substitution Principle (LSP)
    
* Interface Segregation Principle (ISP)
    
* Dependency Inversion Principle (DIP)
    

Adhering to these principles can enhance software quality by encouraging loose coupling, simplicity, and cohesiveness. Code becomes more tested and manageable as a result, which makes it simpler to incorporate new features and address bugs.

It's crucial to remember that SOLID principles are not a one-size-fits-all approach and should be used sparingly while taking the unique use case into account. The guiding principles should be applied when creating a system so that it is simple to comprehend, expand upon, and maintain throughout time.

In order to improve the design of software systems, we'll examine instances of how each of the SOLID principles can be used in TypeScript in this blog article.

> The example code snipptes in the following sections do not contain the type and interface definitions for the function parameters. However, if you are familiar with typescript. The example snippets should be fairly simple to follow. If not, you can only focus on the Class definitions and the OOP principles used to demonstrate the scenarios.
> 
> The examples included under every principle here on will be of a simple usecase where we will define classes for mocking the functionality of an e-commerce order and shipping module. The examples are only to understand the concepts and does not include implementation logic for all the classes.

### Principles

#### **Single Responsibility Principle (SRP)**

The Single Responsibility Principle (SRP) states that a class should have one and only one reason to change, meaning that a class should have only one responsibility.

A responsibility is defined as a reason for change, which means that if you can think of more than one reason for a class to change, then it likely has more than one responsibility.

```typescript
class Order {
    constructor(private items: Item[], private shipping: Shipping) {}
    calculateTotal(): number {
        return this.items.reduce((total, item) => total + item.price, 0) + this.shipping.cost;
    }
}

class OrderPrinter {
    printReceipt(order: Order): void {
        console.log("Order receipt:");
        console.log("Shipping: ", order.shipping.name);
        console.log("Items: ", order.items);
        console.log("Total: ", order.calculateTotal());
    }
}

class Shipping {
    constructor(public name: string, public cost: number) {}
}

class Item {
    constructor(public name: string, public price: number) {}
}
```

In this example, the `Order` class has only one responsibility: calculating the total cost of an order using `calculateTotal`. The class `OrderPrinter` has the responsibility of printing the receipt. By doing this, it would be easier to change the way the total cost is calculated or the way the receipt is printed without affecting the other functionality.

**Benefits of adhering to SRP include:**

* Classes become more focused and easier to understand
    
* Classes become more reusable, as they are only concerned with one responsibility
    
* Classes become more testable, as each class only has one area of concern
    
* Changes to the system are less likely to affect multiple parts of the codebase
    
* Code is more flexible and easier to maintain over time.
    

It's important to remember that following OCP can result in more smaller classes, but it is a useful tool for managing codebase complexity and making it more flexible to changes.

#### **Open/Closed Principle (OCP)**

The Open/Closed Principle (OCP) states that a class should be open for extension but closed for modification. This means that a class should be designed in such a way that new functionality can be added to it without modifying its existing code.

```typescript
abstract class Order {
    abstract calculateTotal(): number;
}

class PhysicalOrder extends Order {
    constructor(private items: Item[], private shipping: Shipping) {}

    calculateTotal(): number {
        return this.items.reduce((total, item) => total + item.price, 0) + this.shipping.cost;
    }
}

class OnlineOrder extends Order {
    constructor(private items: Item[], private shipping: Shipping, private tax: number) {}

    calculateTotal(): number {
        return this.items.reduce((total, item) => total + item.price, 0) + this.shipping.cost + this.tax;
    }
}

class OrderPrinter {
    printReceipt(order: Order): void {
        console.log("Order receipt:");
        console.log("Shipping: ", order.shipping.name);
        console.log("Items: ", order.items);
        console.log("Total: ", order.calculateTotal());
    }
}

class Shipping {
    constructor(public name: string, public cost: number) {}
}

class Item {
    constructor(public name: string, public price: number) {}
}
```

In this example, the base `Order` class defines the `calculateTotal()` method, which is implemented by both the `PhysicalOrder` and `OnlineOrder` classes. The `OrderPrinter` class can print a receipt using `OrderPrinter` for any type of order without knowing the specific implementation details. This allows for new types of orders (such as digital orders) to be added to the system without modifying the existing `Order`, `PhysicalOrder` or `OnlineOrder` classes.

**Benefits of adhering to OCP include:**

* Classes become more flexible, as new functionality can be added without modifying existing code
    
* Changes to the system are less likely to introduce bugs Code is more maintainable over time
    
* The system is more adaptable to changing requirements Code becomes more reusable and easier to test.
    

OCP is a strong tool for managing the complexity of the codebase and making it more flexible, but it can result in more smaller classes, as is necessary to keep in mind.

#### **Liskov Substitution Principle (LSP)**

The Liskov Substitution Principle (LSP) states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In other words, a subclass should be a strict subtype of its superclass, meaning that it should not change the behavior of the superclass methods.

```typescript
interface OrderOperations {
    calculateTotal(): number;
}

class Order implements OrderOperations {
    constructor(private items: Item[], private shipping: Shipping) {}

    calculateTotal(): number {
        return this.items.reduce((total, item) => total + item.price, 0) + this.shipping.cost;
    }
}

class SpecialOrder extends Order {
    constructor(private items: Item[], private shipping: Shipping, private discount: number) {
        super(items, shipping);
    }

    calculateTotal(): number {
        return super.calculateTotal() - this.discount;
    }
}

class Shipping {
    constructor(public name: string, public cost: number) {}
}

class Item {
    constructor(public name: string, public price: number) {}
}
```

In this example, the `Order` class implements the `OrderOperations` interface which defines the `calculateTotal()` method. The `SpecialOrder` class extends the `Order` class and overrides the `calculateTotal()` method with its own implementation that includes a `discount`.

However, it still calls the `calculateTotal()` method of the `Order` class and then applies the `discount`, ensuring that the behavior of the `OrderOperations` interface is not changed. This allows objects of the `SpecialOrder` class to be used in place of objects of the `Order` class without affecting the correctness of the program.

**Benefits of adhering to LSP include:**

* Classes become more reusable and interchangeable
    
* Classes become easier to test and maintain
    
* Changes to the system are less likely to introduce bugs
    
* Code is more adaptable to changing requirements
    
* The system becomes more robust and less prone to errors
    

It is important to note that following LSP can be difficult and necessitates a thorough comprehension of the superclass's behaviour and methods. However, it enables a more resilient and adaptive codebase, which is advantageous in the long run.

#### **Interface Segregation Principle (ISP)**

The Interface Segregation Principle (ISP) states that a class should not be forced to implement interfaces it does not use. This principle encourages creating small, specific interfaces that are only implemented by the classes that need them.

```typescript
interface OrderOperations {
    calculateTotal(): number;
}

interface PrintOperations {
    printReceipt(): void;
}

class Order implements OrderOperations {
    constructor(private items: Item[], private shipping: Shipping) {}

    calculateTotal(): number {
        return this.items.reduce((total, item) => total + item.price, 0) + this.shipping.cost;
    }
}

class OrderPrinter implements PrintOperations {
    printReceipt(order: Order): void {
        console.log("Order receipt:");
        console.log("Shipping: ", order.shipping.name);
        console.log("Items: ", order.items);
        console.log("Total: ", order.calculateTotal());
    }
}

class Shipping {
    constructor(public name: string, public cost: number) {}
}

class Item {
    constructor(public name: string, public price: number) {}
}
```

In this example, the `Order` class implements the `OrderOperations` interface which defines the `calculateTotal()` method. The `OrderPrinter` class implements the `PrintOperations` interface which defines the `printReceipt()` method. This way, the `Order` class and `OrderPrinter` class are not forced to implement methods they don't need, making the code more maintainable and easier to understand.

**Benefits of adhering to ISP include:**

* Classes become more focused and easier to understand
    
* Classes become more reusable, as they only need to implement the methods they need
    
* Classes become more testable, as each class only has methods relevant to its functionality
    
* Changes to the system are less likely to affect multiple parts of the codebase
    
* Code is more flexible and easier to maintain over time.
    

The adoption of ISP can result in more compact interfaces, but in the long term, this is advantageous because it allows for greater structure and control of the codebase.

#### **Dependency Inversion Principle (DIP)**

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules, but both should depend on abstractions. This means that the implementation details of a module should be hidden behind an interface, allowing for a more flexible and maintainable codebase.

```typescript
interface OrderDataAccess {
    getOrders(): Order[];
    saveOrder(order: Order): void;
}

class OrderService {
    constructor(private dataAccess: OrderDataAccess) {}

    getOrders(): Order[] {
        return this.dataAccess.getOrders();
    }
    saveOrder(order: Order): void {
        this.dataAccess.saveOrder(order);
    }
}

class OrderFileDataAccess implements OrderDataAccess {
    getOrders(): Order[] {
        // implementation to get orders from a file
    }
    saveOrder(order: Order): void {
        // implementation to save order to a file
    }
}

class OrderDatabaseDataAccess implements OrderDataAccess {
    getOrders(): Order[] {
        // implementation to get orders from a database
    }
    saveOrder(order: Order): void {
        // implementation to save order to a database
    }
}
```

In this example, the `OrderService` class depends on an `abstraction` (the `OrderDataAccess` interface) rather than a specific implementation (`OrderFileDataAccess` or `OrderDatabaseDataAccess`). This allows the `OrderService` class to work with any implementation of the `OrderDataAccess` interface, making the code more flexible and adaptable to changing requirements.

Here is how the classes can be used during instantiation:

```typescript
//using file data access
const fileDataAccess = new OrderFileDataAccess();
const orderService = new OrderService(fileDataAccess);
console.log(orderService.getOrders()); //outputs the orders from the file
orderService.saveOrder(new Order()); //saves the order to the file

//using database data access
const dbDataAccess = new OrderDatabaseDataAccess();
const orderService = new OrderService(dbDataAccess);
console.log(orderService.getOrders()); //outputs the orders from the database
orderService.saveOrder(new Order()); //saves the order to the database
```

Here, we can see that `OrderService` depends on the abstraction `OrderDataAccess` interface, and we can use different implementations like `OrderFileDataAccess` or `OrderDatabaseDataAccess` without affecting the `OrderService` class. This allows us to switch the data access method without changing the `OrderService` code, making the system more flexible and adaptable to changing requirements.

**Benefits of adhering to DIP include:**

* Classes become more reusable and interchangeable
    
* Classes become easier to test and maintain
    
* Changes to the system are less likely to introduce bugs
    
* Code is more adaptable to changing requirements
    
* The system becomes more robust and less prone to errors.
    

It's important to remember that sticking to DIP can result in more complex code, but this is offset by the longer-term benefits of a more adaptable and manageable codebase.

### Conclusion

The SOLID principles are a set of guidelines for creating maintainable and flexible software.

SOLID principles are essential in software development, they help to create a well-structured and easy-to-maintain codebase which is crucial for long-term success. These principles are not only important for creating new software but also for maintaining and updating existing systems. Implementing SOLID principles will save time, effort and money in the long run.

However, using these principles is subject to the targeted codebase and may or may not be needed depending on the use case and complexity of the system. If these principles are used where they are not needed, they can lead to over-engineered solutions that can result in a very complex and hard-to-follow codebase.
