# SOLID Principles

*The SOLID Principles are five principles of Object-Oriented class design. They are a set of rules and best practices to follow while designing a class structure.*

## History

The **SOLID** principles were introduced by ***Robert J. Martin*** in his paper in 2000. But the **SOLID** acronym was introduced later by ***Michael Feathers***.

**SOLID** principles acronym for

1. The **S**ingle Responsibility Principle.
    
2. The **O**pen-Closed Principle.
    
3. The **L**iskov Substitution Principle.
    
4. The **I**nterface Segregation Principle.
    
5. The **D**ependency Inversion Principle.
    

## Single Responsibility Principle

The idea behind the Single Responsibility Principle is that every class, module, and function should have only one responsibility.

```go
type User struct {
	ID        int
	FirstName string
	LastName  string
	Email     string
}

type UserRegister struct{}

func (ur *UserRegister) RegisterUser(user *User) {
	ur.saveUserToDB(user)
	ur.sendWelcomeEmail(user)
}

func (ur *UserRegister) saveUserToDB(user *User) {
	fmt.Printf("Saving user %s to the database...\n", user.FirstName)
	time.Sleep(1 * time.Second)
	fmt.Printf("User %s saved to the database successfully \n", user.FirstName)
}

func (ur *UserRegister) sendWelcomeEmail(user *User) {
	fmt.Printf("Sending welcome email to %s at %s \n", user.FirstName, user.Email)
	time.Sleep(1 * time.Second)
	fmt.Printf("Welcome email to %s at %s \n", user.FirstName, user.Email)
}
```

The `RegisterUser` has two responsibility

1. `saveUserToDB`
    
2. `sendWelcomeEmail`
    

According to Single Responsibility Principle, every class, module, and function should have only one responsibility.

Code after applying Single Responsibility Principle

```go
type User struct {
	ID        int
	FirstName string
	LastName  string
	Email     string
}

type UserDB struct{}

func (udb *UserDB) SaveUser(user *User) {
	fmt.Printf("Saving user %s to the database...\n", user.FirstName)
	time.Sleep(1 * time.Second)
	fmt.Printf("User %s saved to the database successfully \n", user.FirstName)
}

type EmailSender struct{}

func (es *EmailSender) SendWelcomeEmail(user *User) {
	fmt.Printf("Sending welcome email to %s at %s \n", user.FirstName, user.Email)
	time.Sleep(1 * time.Second)
	fmt.Printf("Welcome email to %s at %s \n", user.FirstName, user.Email)
}
```

## Open/Closed Principle

The open-closed principle states that software entities (classes, modules, functions, and so on) should be open for extension, but closed for modification.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690085117241/16b664a1-3548-48d2-a7db-76b2d15f8bfb.png align="center")

As the diagram suggests, we can not modify the shape but extend it.

```go
type Circle struct {
	Radius float64
}

type Square struct {
	SideLength float64
}

type Rectangle struct {
	Width  float64
	Height float64
}

type Calculator struct{}

func (c *Calculator) CalculateCircleArea(circle Circle) float64 {
	return math.Pi * circle.Radius * circle.Radius
}

func (c *Calculator) CalculateSquareArea(square Square) float64 {
	return square.SideLength * square.SideLength
}

func (c *Calculator) CalculateRectangleArea(rectangle Rectangle) float64 {
	return rectangle.Width * rectangle.Height
}
```

If we want to add a new shape, we have to modify the existing `Calculator` struct and add a new method for the triangle, which violates the principle.

Let's apply the open-closed principle

```go

type Shape interface {
	Area() float64
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

type Square struct {
	SideLength float64
}

func (s Square) Area() float64 {
	return s.SideLength * s.SideLength
}

type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

type Calculator struct{}

func (c *Calculator) CalculateArea(shape Shape) float64 {
	return shape.Area()
}
```

Now, We have defined the Interface name `Shape` with an `Area()` method and each shape implements this interface. The `Calculator` struct can now work with any shape that implements the `Shape` interface without requiring modification. If you want to add a new shape, you can create a new type that implements the `Shape` interface and `Calculator` will work with it seamlessly.

## Liskov Substitution Principle

According to Barbara Liskov and Jeannette Wing, the Liskov substitution principle states that:

`Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.`

This Principle implies that when an instance of a class is passed/extended to another class, the inheriting class should have a use case for all the properties and behavior of the inherited class.

Let's say we have a class called `Amphibian` for animals that can live on both land and water. This class has two methods to show the features of an amphibian – `swim()` and `walk()`.

```Java
public class Amphibian {

    public void swim();
    public void walk();

}
```

The `Amphibian` class can extend to a `Frog` class because frogs are amphibians so that they can inherit the properties of the `Amphibian` class without altering the logic and purpose of the class.

```Java
public class Frog extends Amphibian {
    public void swim() {
        System.out.println("The frog is swimming");
    }
    
    public void walk() {
        System.out.println("The frog is walking on land");
    }
}
```

But we cannot extend the `Amphibian` class to a `Dolphin` class because dolphins only live in water which implies that the walk() method would be irrelevant to the `Dolphin` class.

In summary, if a class inherits another, it should do so in a manner that all the properties of the inherited class would remain relevant to its functionality.

## Interface Segregation Principle

The **interface segregation principle** states that the interface of a program should be split in a way that the user/client would only have access to the necessary methods related to their needs.  

```go
type SPrinter interface {
	PrintDocument()
	ScanDocument()
	FaxDocument()
}

type SimplePrinter struct{}

func (sp *SimplePrinter) PrintDocument() {
	fmt.Println("Printing document......")
}

func (sp *SimplePrinter) ScanDocument() {
	fmt.Println("Scanning document......")
}

func (sp *SimplePrinter) FaxDocument() {
	fmt.Println("Faxing document......")
}

type OfficePrinter struct{}

func (op *OfficePrinter) PrintDocument() {
	fmt.Println("Printing office document......")
}

func (op *OfficePrinter) ScanDocument() {
	fmt.Println("Scanning office document......")
}

type HomePrinter struct{}

func (hp *HomePrinter) PrintDocument() {
	fmt.Println("Printing document at home...")
}

func (hp *HomePrinter) ScanDocument() {
	fmt.Println("Scanning document at home...")
}
```

In the given code, the `SPrinter` interface has three methods: `PrintDocument()`, `ScanDocument()`, and `FaxDocument()`. However, the implementations of the interface in the `SimplePrinter`, `OfficePrinter`, and `HomePrinter` structs suggest that not all of them require or support all three methods.

For example, the `SimplePrinter` only needs to print and scan documents, but it is forced to implement the `FaxDocument()` method from the `SPrinter` interface. Similarly, the `HomePrinter` only need to print and scan documents, but it's also forced to implement the `FaxDocument()` method.

Let's apply Interface Segregation Principle  

```go
type Printer interface {
	PrintDocument()
}

type Scanner interface {
	ScanDocument()
}

type Faxer interface {
	FaxDocument()
}

type Simple_Printer struct{}

func (sp *Simple_Printer) PrintDocument() {
	fmt.Println("Printing document......")
}

func (sp *Simple_Printer) ScanDocument() {
	fmt.Println("Scanning document......")
}

func (sp *Simple_Printer) FaxDocument() {
	fmt.Println("Faxing document......")
}

type Office_Printer struct{}

func (op *Office_Printer) PrintDocument() {
	fmt.Println("Printing document at the office......")
}

func (op *Office_Printer) ScanDocument() {
	fmt.Println("Scanning document at the office......")
}

type Home_Printer struct{}

func (hp *Home_Printer) PrintDocument() {
	fmt.Println("Printing document at the home......")
}

func (hp *Home_Printer) ScanDocument() {
	fmt.Println("Scanning document at the home......")
}
```

## Dependency Inversion Principle

The dependency inversion principle states:

`High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).`

And,

`Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions`

Here is a code example that violates this principle:

```Golang
type User struct {
    Name         string
    Email        string
    PhoneNumber  string
    Notification Notification // Dependency on the Notification interface
}

func (u User) Notify(message string) {
    u.Notification.Send(message)
}
```

By improving

```go
type User struct {
    Name        string
    Email       string
    PhoneNumber string
}

func (u User) Notify(notifier Notification, message string) {
    notifier.Send(message)
}
```

## Conclusion

There are so many ways to solve a problem. But there are also many ways to create problems from a solution.

The more rigid and coupled the classes and methods in our code are, the more difficult it would be to maintain and reuse our code.

Neglecting or violating these principles could pose a serious threat to not just the codebase and the developer, but to the organization that owns the product as well.

A rigid and tightly coupled codebase makes it difficult to add or remove features in a product, test, and reuse blocks of code and introduces possible breaking changes with every code modification made.

The SOLID principles act as a guide to help us create a flexible and dynamic product and we went over each principle in this article to help us understand how the objects we create should interact which each other.
