Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Applying SOLID principles in your everyday coding

March 29, 2023

Applying SOLID Principles in Your Everyday Coding

As a software developer, you are constantly faced with the challenge of writing maintainable, scalable, and efficient code. The SOLID principles, coined by Robert C. Martin, offer a set of guidelines to achieve these goals. In this blog post, we will explore how you can apply these principles in your everyday coding, with extensive examples in C and Python.

What are the SOLID principles?

The SOLID principles are a set of five design principles that help developers create software that is easy to understand, maintain, and extend. The acronym SOLID stands for:

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

Let’s delve into each of these principles and understand how they can be applied in code.

1. Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should only have one job or responsibility. Let’s consider a simple example in C:

1
2
3
4
5
6
7
8
9
// Bad example violating SRP
#include <stdio.h>

void calculateAreaAndPerimeter(float length, float width) {
  float area = length * width;
  float perimeter = 2 * (length + width);
  printf("Area: %f\n", area);
  printf("Perimeter: %f\n", perimeter);
}

This function violates SRP as it is responsible for both calculating the area and perimeter as well as printing the result. We can refactor it to adhere to SRP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Good example adhering to SRP
#include <stdio.h>

float calculateArea(float length, float width) {
  return length * width;
}

float calculatePerimeter(float length, float width) {
  return 2 * (length + width);
}

void printResults(float area, float perimeter) {
  printf("Area: %f\n", area);
  printf("Perimeter: %f\n", perimeter);
}

2. Open/Closed Principle (OCP)

The Open/Closed Principle states that a class should be open for extension but closed for modification. In other words, you should be able to extend the behavior of a class without modifying its source code. Let’s consider a Python example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Bad example violating OCP
class Shape:
  def draw(self):
    pass

class Circle(Shape):
  def draw(self):
    print("Circle drawn")

class Square(Shape):
  def draw(self):
    print("Square drawn")

In the above example, if we want to add a new shape (e.g., Triangle), we would have to modify the Shape class violating OCP. We can refactor it to adhere to OCP using Python’s abstract base classes (abc module):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Good example adhering to OCP
from abc import ABC, abstractmethod

class Shape(ABC):
  @abstractmethod
  def draw(self):
    pass

class Circle(Shape):
  def draw(self):
    print("Circle drawn")

class Square(Shape):
  def draw(self):
    print("Square drawn")

By using abstract base classes, we ensure that the Shape class is closed for modification but open for extension.

3. Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclass without affecting the correctness of the program. Let’s consider an example in C:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Bad example violating LSP
#include <stdio.h>

typedef struct {
  int width;
  int height;
} Rectangle;

void calculateArea(Rectangle *rect) {
  int area = rect->width * rect->height;
  printf("Area: %d\n", area);
}

int main() {
  Rectangle rect = {5, 10};
  calculateArea(&rect);
  
  return 0;
}

In the above example, if we introduce a Square subclass of Rectangle and pass it to calculateArea, it would violate LSP as a square is not substitutable for a rectangle. We can refactor it to adhere to LSP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Good example adhering to LSP
#include <stdio.h>

typedef struct {
  int width;
  int height;
} Shape;

void calculateArea(Shape *shape) {
  int area = shape->width * shape->height;
  printf("Area: %d\n", area);
}

int main() {
  Shape rect = {5, 10};
  calculateArea(&rect);
  
  return 0;
}

By treating Rectangle and Square as variants of a more general Shape, we adhere to LSP.

4. Interface Segregation Principle (ISP)

The Interface Segregation Principle states that a client should not be forced to depend on methods it does not use. Let’s consider a Python example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Bad example violating ISP
class Machine:
  def print(self):
    pass

  def fax(self):
    pass

class Printer(Machine):
  def print(self):
    print("Printing")

class FaxMachine(Machine):
  def fax(self):
    print("Faxing")

In the above example, a client using Printer is forced to depend on the fax method, violating ISP. We can refactor it to adhere to ISP:

1
2
3
4
5
6
7
8
# Good example adhering to ISP
class Printer:
  def print(self):
    print("Printing")

class Fax:
  def fax(self):
    print("Faxing")

By splitting the interface into separate, smaller interfaces, we adhere to ISP.

5. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Let’s consider an example in Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Bad example violating DIP
class Connection:
  def connect(self):
    pass

class MySQL(Connection):
  def connect(self):
    print("Connecting to MySQL")

class Oracle(Connection):
  def connect(self):
    print("Connecting to Oracle")

In the above example, a high-level module directly depends on low-level modules violating DIP. We can refactor it to adhere to DIP using dependency injection:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Good example adhering to DIP
class Connection:
  def connect(self):
    pass

class MySQL(Connection):
  def connect(self):
    print("Connecting to MySQL")

class Oracle(Connection):
  def connect(self):
    print("Connecting to Oracle")

class DatabaseClient:
  def __init__(self, connection: Connection):
    self.connection = connection

  def connect_to_database(self):
    self.connection.connect()

By injecting the Connection dependency into the DatabaseClient, we adhere to DIP.

Conclusion

In this blog post, we have explored how the SOLID principles can be applied in everyday coding scenarios using extensive examples in C and Python. By following these principles, you can write code that is more maintainable, scalable, and efficient, leading to better software design and development. Remember, while these examples are in C and Python, the concepts of SOLID principles are applicable across various programming languages and paradigms.

Happy coding!


➡️ Determining the Path of the Currently Running Bash Script


⬅️ Simple command line interface library


Go back to Posts.