Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

The role of SOLID principles in agile development

May 21, 2023

The role of SOLID principles in agile development

SOLID principles are a set of design principles that are essential for writing clean, maintainable, and scalable code. When it comes to agile development, adhering to these principles is crucial for delivering high-quality software that can adapt to changing requirements and maintain a rapid development pace. In this blog post, we will explore the role of SOLID principles in agile development and demonstrate their application using examples in C and Python.

What are SOLID principles?

SOLID is an acronym that stands for five important principles of object-oriented programming:

These principles provide guidelines for designing classes and systems that are easy to understand, flexible, and maintainable.

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 responsibility. Let’s consider a simple example in C and Python to illustrate this principle.

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* Bad design */
#include <stdio.h>

void processUserData() {
    // logic for processing user data
}

void handleUserInput() {
    // logic for handling user input
}

int main() {
    processUserData();
    handleUserInput();
    return 0;
}

In the above C code, the processUserData and handleUserInput functions are handling multiple responsibilities, violating the SRP. We can refactor the code to adhere to SRP by separating the responsibilities into different classes or functions.

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Bad design
class User:
    def process_user_data(self):
        # logic for processing user data

    def handle_user_input(self):
        # logic for handling user input

if __name__ == "__main__":
    user = User()
    user.process_user_data()
    user.handle_user_input()

Similarly, in the Python example, the User class is handling multiple responsibilities. We can refactor the code by creating separate classes or functions for each responsibility.

Open/Closed Principle (OCP)

The Open/Closed Principle states that classes should be open for extension but closed for modification. This means that we should be able to extend the behavior of a class without modifying its source code. Let’s illustrate this principle with an example in C and Python.

C Example:

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

void printShape(void* shape) {
    if (shape->type == CIRCLE) {
        printf("Drawing a circle\n");
    } else if (shape->type == SQUARE) {
        printf("Drawing a square\n");
    }
}

int main() {
    Circle circle;
    Square square;

    printShape(&circle);
    printShape(&square);

    return 0;
}

In the C code above, the printShape function violates the OCP as it needs to be modified whenever a new shape is added. We can refactor the code to adhere to the OCP by using polymorphism and inheritance.

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Bad design
class Shape:
    def draw(self):
        pass

class Circle(Shape):
    def draw(self):
        print("Drawing a circle")

class Square(Shape):
    def draw(self):
        print("Drawing a square")

if __name__ == "__main__":
    circle = Circle()
    square = Square()

    circle.draw()
    square.draw()

In the Python example, we use inheritance and polymorphism to adhere to the OCP. We can easily add new shapes by creating new classes that inherit from the Shape class without modifying existing code.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. Let’s examine an example in C and Python to illustrate this principle.

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* Bad design */
class Bird {
public:
    void fly() {
        // Fly logic
    }
};

class Ostrich : public Bird {
public:
    void fly() {
        // Ostriches cannot fly
        throw "Ostrich cannot fly";
    }
};

int main() {
    Bird* bird = new Ostrich();
    bird->fly();  // This will throw an exception
    return 0;
}

In the C code above, the Ostrich class violates the LSP as it overrides the fly method to throw an exception. The LSP ensures that derived classes should not alter the behavior of the superclass in an unexpected way.

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Bad design
class Bird:
    def fly(self):
        pass

class Ostrich(Bird):
    def fly(self):
        raise NotImplementedError("Ostrich cannot fly")

if __name__ == "__main__":
    bird = Ostrich()
    bird.fly()  # This will raise a NotImplementedError

In the Python example, the Ostrich class also violates the LSP by altering the behavior of the fly method. We can adhere to the LSP by ensuring that the behavior of the overridden methods in the subclass comply with the behavior of the superclass.

Interface Segregation Principle (ISP)

The Interface Segregation Principle states that a client should not be forced to depend on interfaces it does not use. Let’s demonstrate this principle with an example in C and Python.

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* Bad design */
class Document {
public:
    virtual void print() = 0;
    virtual void scan() = 0;
    virtual void fax() = 0;
};

class MultifunctionPrinter : public Document {
public:
    void print() override {
        // Print logic
    }

    void scan() override {
        // Scan logic
    }

    void fax() override {
        // Fax logic
    }
};

int main() {
    Document* mfPrinter = new MultifunctionPrinter();
    mfPrinter->scan();  // This violates ISP as the client is forced to depend on the fax functionality
    return 0;
}

In the C code above, the MultifunctionPrinter violates the ISP as the client is forced to depend on all the functionalities of the Document interface. We can adhere to the ISP by segregating the interfaces into smaller, specific interfaces.

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Bad design
class Document:
    def print(self):
        pass

    def scan(self):
        pass

    def fax(self):
        pass

class MultifunctionPrinter(Document):
    def print(self):
        # Print logic

    def scan(self):
        # Scan logic

    def fax(self):
        # Fax logic

if __name__ == "__main__":
    mf_printer = MultifunctionPrinter()
    mf_printer.scan()  # This violates ISP as the client is forced to depend on the fax functionality

Similarly, the Python example violates the ISP, and we can adhere to it by creating separate, specific interfaces for the functionalities.

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. Additionally, abstractions should not depend on details, but details should depend on abstractions. Let’s explore an example in C and Python to demonstrate this principle.

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* Bad design */
class Database {
public:
    virtual void connect() = 0;
    virtual void disconnect() = 0;
};

class MySqlConnection : public Database {
public:
    void connect() override {
        // MySQL connect logic
    }

    void disconnect() override {
        // MySQL disconnect logic
    }
};

class LogManager {
private:
    MySqlConnection* connection;

public:
    LogManager() {
        connection = new MySqlConnection();
    }

    void logMessage(const char* message) {
        connection->connect();
        // Log message logic
        connection->disconnect();
    }
};

int main() {
    LogManager logManager;
    logManager.logMessage("Error occurred");
    return 0;
}

In the C code above, the LogManager violates the DIP as it directly depends on the MySqlConnection concrete implementation. We can adhere to the DIP by depending on abstractions rather than concrete implementations.

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Bad design
class Database:
    def connect(self):
        pass

    def disconnect(self):
        pass

class MySqlConnection(Database):
    def connect(self):
        # MySQL connect logic

    def disconnect(self):
        # MySQL disconnect logic

class LogManager:
    def __init__(self):
        self.connection = MySqlConnection()

    def log_message(self, message):
        self.connection.connect()
        # Log message logic
        self.connection.disconnect()

if __name__ == "__main__":
    log_manager = LogManager()
    log_manager.log_message("Error occurred")

Similarly, the Python example violates the DIP, and we can adhere to it by depending on abstractions through the use of dependency injection and interfaces.

Conclusion

In this blog post, we have explored the role of SOLID principles in agile development and demonstrated their application through examples in C and Python. Adhering to these principles is essential for creating code that is clean, maintainable, and adaptable to changing requirements, which is crucial in the fast-paced environment of agile development. By understanding and applying SOLID principles, developers can significantly improve the quality and agility of their codebases, ultimately leading to more successful and efficient software development projects.


➡️ Bit-bang in embedded development


⬅️ Visitor design pattern


Go back to Posts.