Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Design patterns and SOLID principles: A comprehensive overview

May 1, 2023

Design patterns and SOLID principles: A comprehensive overview

In the world of software development, writing code is not just about solving a problem at hand; it’s also about structuring the code in a way that makes it maintainable, scalable, and easy to work with. This is where design patterns and SOLID principles come into play.

Design Patterns

Design patterns are reusable solutions to common problems encountered in software design. They provide a template for solving certain issues and can speed up the development process by providing tested, proven development paradigms. Let’s dive into some of the most prominent design patterns and provide examples in C and Python.

Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. This is useful when exactly one object is needed to coordinate actions across the system. Here’s an example in C:

 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
#include <stdio.h>

typedef struct {
    int data;
} Singleton;

Singleton* singleton_instance = NULL;

Singleton* get_singleton_instance() {
    if (singleton_instance == NULL) {
        singleton_instance = malloc(sizeof(Singleton));
        singleton_instance->data = 10;
    }
    return singleton_instance;
}

int main() {
    Singleton* s1 = get_singleton_instance();
    printf("%d\n", s1->data);  // Output: 10

    Singleton* s2 = get_singleton_instance();
    s2->data = 20;
    printf("%d\n", s1->data);  // Output: 20

    return 0;
}

And here’s an example in Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.data = 10
        return cls._instance

s1 = Singleton()
print(s1.data)  # Output: 10

s2 = Singleton()
s2.data = 20
print(s1.data)  # Output: 20

Factory Pattern

The Factory pattern is used to create an object based on certain conditions. This allows the creation of objects without specifying the exact class of object that will be created. Here’s an example in C:

 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
#include <stdio.h>

typedef struct {
    int data;
} Product;

typedef struct {
    Product* (*create_product)();
} Factory;

Product* create_product() {
    Product* product = malloc(sizeof(Product));
    product->data = 100;
    return product;
}

int main() {
    Factory factory;
    factory.create_product = create_product;

    Product* product = factory.create_product();
    printf("%d\n", product->data);  // Output: 100

    return 0;
}

And here’s an example in Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Product:
    def __init__(self):
        self.data = 100

class Factory:
    def create_product(self):
        return Product()

factory = Factory()
product = factory.create_product()
print(product.data)  # Output: 100

These are just a couple of examples of design patterns, and there are many more out there, each serving a different purpose.

SOLID Principles

The SOLID principles are a set of five design principles to make software designs more understandable, flexible, and maintainable. Let’s go through each principle and provide examples in C and Python.

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. It advocates for separating a class into distinct parts where each part addresses a separate concern. Here’s an example in C:

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

typedef struct {
    int data;
} DataStorage;

void save_data(DataStorage* storage, int value) {
    storage->data = value;
}

void print_data(DataStorage* storage) {
    printf("%d\n", storage->data);
}

int main() {
    DataStorage storage;
    save_data(&storage, 100);
    print_data(&storage);  // Output: 100

    return 0;
}

And here’s an example in Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class DataStorage:
    def __init__(self):
        self.data = None
    
    def save_data(self, value):
        self.data = value
    
    def print_data(self):
        print(self.data)

storage = DataStorage()
storage.save_data(100)
storage.print_data()  # Output: 100

Open/Closed Principle (OCP)

The Open/Closed Principle states that a class should be open for extension but closed for modification. It encourages the use of abstraction and polymorphism to achieve this. Here’s an example in C:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

typedef struct {
    int data;
} Shape;

void draw(Shape* shape) {
    // Drawing logic specific to each shape
}

int main() {
    // Drawing logic using the draw function
    return 0;
}

And here’s an example in Python:

1
2
3
4
5
class Shape:
    def draw(self):
        pass  # Drawing logic specific to each shape

# Drawing logic using the draw method

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a superclass shall be replaceable with objects of its subclass without affecting the functionality of the program. Here’s an example in C:

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

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

int calculate_area(Rectangle* rect) {
    return rect->width * rect->height;
}

int main() {
    Rectangle rect;
    rect.width = 5;
    rect.height = 10;
    printf("%d\n", calculate_area(&rect));  // Output: 50

    return 0;
}

And here’s an example in Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def calculate_area(self):
        return self.width * self.height

rect = Rectangle(5, 10)
print(rect.calculate_area())  # Output: 50

Interface Segregation Principle (ISP)

The Interface Segregation Principle states that a client should not be forced to implement interfaces they don’t use. It encourages the use of smaller, specific interfaces rather than one large, general interface. Here’s an example in C:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

typedef struct {
    int data;
} Printer;

void print(Document* doc) {
    // Printing logic
}

int main() {
    // Document creation and printing logic
    return 0;
}

And here’s an example in Python:

1
2
3
4
5
class Printer:
    def print(self, doc):
        pass  # Printing logic

# Document creation and printing logic

By understanding and applying these design patterns and SOLID principles, developers can write better, more maintainable code and improve the overall quality of their software.

I hope this comprehensive overview has given you a good understanding of these concepts and how they can be implemented in both C and Python. Let me know in the comments if you have any questions or want to see more examples!


➡️ Exploring the Builder Pattern in C++ and C


⬅️ Implementing the Null Object Pattern in C++ and C


Go back to Posts.