Blog, COLDSURF

SOLID Principles

#CS

#development

Overview of SOLID Principles

In computer programming, SOLID refers to five basic principles of object-oriented programming and design, named by Robert Martin in the early 2000s, and introduced as a mnemonic by Michael Feathers. These principles help programmers create systems that are easy to maintain and extend over time.

SOLID principles serve as guidelines for refactoring software to make the source code easy to read, extend, and maintain. They help eliminate code smells and allow software systems to evolve with minimal impact. These principles are a key part of agile and adaptive software development strategies.

Source:

The principles aim to promote clean code, making it easier to write maintainable and scalable software. Whether a project is large or small, the SOLID principles focus on writing clean code where each class’s responsibilities are clearly defined, minimizing its impact on other components.

Though originally intended for object-oriented design, SOLID principles can also be applied in non-object-oriented contexts, such as libraries like React.

Understanding and applying these principles is extremely helpful when aiming to produce code that people can easily understand.

The Content of the SOLID Principles

The SOLID principles consist of five principles, outlined below:

S: Single Responsibility Principle (SRP)

A class should have only one reason to change, meaning it should only have one responsibility.

According to the Single Responsibility Principle, a class should focus on a single responsibility or task. If a class takes on multiple tasks, modifying one responsibility could interfere with others, leading to unclear code and potential errors. To make code maintainable and easy to extend, responsibilities should be separated as much as possible, so that classes can focus on just one job.

O: Open/Closed Principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

When modifying a module, if changes need to be made across every part of the code that uses it, it can be cumbersome in large projects. To avoid this, when extending a module, the existing code should not be modified. Instead, new functionality should be added without altering the code that is already working.

For example, suppose we want to add more cake flavors to the following code:

let cakeMaker = {
  availableFlavor: ['chocolate', 'milk chocolate', 'strawberry'],
  makeCake: function(flavor) {
    const cake = this.availableFlavor.find((v) => v === flavor)
    if (cake) {
      return cake
    } else {
      throw Error(`We don't have ${flavor} flavor`)
    }
  }
}

Rather than modifying the availableFlavor array directly, we can add an addFlavor method:

let cakeMaker = {
  availableFlavor: ['chocolate', 'milk chocolate', 'strawberry'],
  makeCake: function(flavor) {
    const cake = this.availableFlavor.find((v) => v === flavor)
    if (cake) {
      return cake
    } else {
      throw Error(`We don't have ${flavor} flavor`)
    }
  },
  addFlavor: function(flavor) {
    this.availableFlavor = this.availableFlavor.concat(flavor)
  }
}

L: Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

The Liskov Substitution Principle requires that any derived class can be used in place of its parent class without breaking the functionality of the program. This principle ensures that subclasses correctly extend the functionality of the parent class.

I: Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

Rather than using a single, large interface that groups many functionalities, it's better to break it down into smaller, specific interfaces. This prevents clients from being forced to implement unused methods.

For example:

interface Phone {
  call: {
    photoShoot: () => void
    callTo: (phoneNumber: string) => void
  }
}

The above Phone interface mixes the functionalities of calling and photography, which are not necessarily related. We can separate the photoShoot functionality:

interface Phone {
  call: {
    callTo: (phoneNumber: string) => void
  }
  photoShoot: () => void
}

This ensures that classes implementing Phone don’t need to support methods that are not relevant to their use case.

D: Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Furthermore, abstractions should not depend on details. Details should depend on abstractions.

When expanding the functionality of a system, rather than modifying existing implementations, it's better to add new interfaces that support extensions. This prevents side effects caused by modifying existing functionality while adding new capabilities.

In other words, we should avoid direct dependency on concrete implementations. Instead, we rely on abstract interfaces, which can be extended without altering the existing code base.

← Go home