Design Patterns

Design patterns are proven solutions to recurring problems encountered in software design. They provide a structured approach to solving common design issues, making code more maintainable, reusable, and scalable. In this chapter, we'll explore various design patterns and how they can be applied in the C programming language.

Creational Design Patterns

Creational design patterns deal with object creation mechanisms, providing flexibility in object creation while hiding the complexities involved. We’ll discuss three fundamental creational design patterns

Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. Here’s an example:

				
					#include <stdio.h>

typedef struct {
    // Data members
} Singleton;

Singleton* get_instance() {
    static Singleton instance;
    return &instance;
}

int main() {
    Singleton* obj1 = get_instance();
    Singleton* obj2 = get_instance();

    printf("%p\n", obj1);  // Output: Address of the singleton instance
    printf("%p\n", obj2);  // Output: Same address as obj1

    return 0;
}

				
			

Explanation: In this code, get_instance() returns a pointer to the Singleton instance. Both obj1 and obj2 point to the same instance, demonstrating the Singleton pattern.

Factory Pattern

The Factory pattern defines an interface for creating objects, allowing subclasses to alter the type of objects that will be created. Here’s an example:

				
					#include <stdio.h>

// Product interface
typedef struct {
    void (*print)(void);
} Product;

// Concrete product
typedef struct {
    Product product;
    // Data members
} ConcreteProduct;

void print_concrete_product() {
    printf("Concrete product\n");
}

// Factory
Product* create_product() {
    ConcreteProduct* product = malloc(sizeof(ConcreteProduct));
    product->product.print = print_concrete_product;
    return (Product*)product;
}

int main() {
    Product* product = create_product();
    product->print();  // Output: Concrete product
    free(product);

    return 0;
}

				
			

Explanation: Here, create_product() acts as a factory method that creates and returns instances of ConcreteProduct. It provides a unified interface to create different types of products.

Prototype Pattern

The Prototype pattern creates new objects by copying an existing object, known as the prototype. It allows object creation without specifying the exact class of object that will be created. Here’s an example:

				
					#include <stdio.h>

// Prototype
typedef struct {
    int data;
} Prototype;

// Clone function
Prototype* clone(Prototype* prototype) {
    Prototype* new_object = malloc(sizeof(Prototype));
    new_object->data = prototype->data;
    return new_object;
}

int main() {
    Prototype prototype = {10};
    Prototype* clone_obj = clone(&prototype);
    
    printf("%d\n", clone_obj->data);  // Output: 10
    free(clone_obj);

    return 0;
}

				
			

Explanation: In this code, clone() creates a new object by copying the data from the prototype object. It allows dynamic creation of objects at runtime.

Structural Design Patterns

Structural design patterns focus on class and object composition. They help ensure that if one part of a system changes, the entire system doesn’t need to do so. We’ll discuss two important structural design patterns:

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces. Here’s an example:

				
					#include <stdio.h>

// Target interface
typedef struct {
    void (*request)(void);
} Target;

// Adaptee
typedef struct {
    // Adaptee specific interface
} Adaptee;

void adaptee_specific_request() {
    printf("Adaptee specific request\n");
}

// Adapter
typedef struct {
    Target target;
    Adaptee adaptee;
} Adapter;

void adapter_request() {
    Adaptee adaptee;
    adaptee_specific_request();
}

int main() {
    Adapter adapter;
    adapter.target.request = adapter_request;
    adapter.target.request();  // Output: Adaptee specific request

    return 0;
}

				
			

Explanation: Here, adapter_request() acts as a bridge between the Target interface and the Adaptee interface.

Composite Pattern

The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It allows clients to treat individual objects and compositions of objects uniformly. Here’s an example:

				
					#include <stdio.h>

// Component interface
typedef struct {
    void (*operation)(void);
} Component;

// Leaf
typedef struct {
    Component component;
    // Data members
} Leaf;

void leaf_operation() {
    printf("Leaf operation\n");
}

// Composite
typedef struct {
    Component component;
    Leaf* children[10];
    int count;
} Composite;

void composite_operation(Composite* composite) {
    printf("Composite operation\n");
    for (int i = 0; i < composite->count; i++) {
        composite->children[i]->component.operation();
    }
}

int main() {
    Leaf leaf;
    leaf.component.operation = leaf_operation;

    Composite composite;
    composite.component.operation = (void (*)(void))composite_operation;
    composite.children[0] = &leaf;
    composite.count = 1;

    composite.component.operation();  // Output: Composite operation followed by Leaf operation

    return 0;
}

				
			

Explanation: In this example, composite_operation() recursively traverses through the composite structure, performing operations on each leaf node.

Behavioral Design Patterns

Behavioral design patterns focus on communication between objects, emphasizing how objects collaborate to accomplish tasks. We’ll discuss two important behavioral design patterns:

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects, where changes in one object trigger updates in dependent objects. Here’s an example:

				
					#include <stdio.h>

// Subject
typedef struct {
    // Data members
    int state;
} Subject;

void set_state(Subject* subject, int state) {
    subject->state = state;
}

// Observer
typedef struct {
    // Data members
} Observer;

void update(Subject* subject) {
    printf("State updated: %d\n", subject->state);
}

int main() {
    Subject subject;
    Observer observer1, observer2;

    set_state(&subject, 10);
    update(&subject);  // Output: State updated: 10

    return 0;
}

				
			

Explanation: Here, update() is called whenever the state of the subject changes, notifying all observers of the change.

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each algorithm, and makes them interchangeable. It allows the algorithm to vary independently from clients using it. Here’s an example:

				
					#include <stdio.h>

// Strategy interface
typedef struct {
    void (*execute)(void);
} Strategy;

// Concrete strategies
void strategy1_execute() {
    printf("Executing strategy 1\n");
}

void strategy2_execute() {
    printf("Executing strategy 2\n");
}

// Context
typedef struct {
    Strategy* strategy;
} Context;

void context_execute(Context* context) {
    context->strategy->execute();
}

int main() {
    Strategy strategy1 = {strategy1_execute};
    Strategy strategy2 = {strategy2_execute};

    Context context1 = {&strategy1};
    Context context2 = {&strategy2};

    context_execute(&context1);  // Output: Executing strategy 1
    context_execute(&context2);  // Output: Executing strategy 2

    return 0;
}

				
			

Explanation: In this code, the Context class holds a reference to a Strategy object, allowing the client to switch between different strategies dynamically.

Additional Points

Design PatternAdvantagesImplementation Considerations
Singleton Pattern- Provides a global access point to a single instance, ensuring only one instance throughout the application.
- Lazy initialization saves memory by creating the instance only when first accessed.
- Thread safety: Implementations should be thread-safe to avoid race conditions.
- Destruction: Ensure proper cleanup and resource destruction when the singleton is no longer needed.
Factory Pattern- Decouples object creation from client code, enhancing extension and maintenance.
- Centralized object creation enhances code modularity and scalability.
- Abstract factory: Consider using abstract factory pattern for creating families of related objects in complex systems.
Prototype Pattern- Allows creation of new objects by cloning existing ones, reducing subclassing.
- Improves performance by avoiding expensive object creation operations.
- Deep vs. shallow cloning: Choose appropriately to avoid unintended side effects based on object structure.
Adapter Pattern- Enables integration of incompatible interfaces, promoting code reuse and flexibility.
- Allows legacy code to work with modern systems without modification.
- Class vs. object adapters: Select based on system requirements and complexity.
Composite Pattern- Represents part-whole hierarchies as tree structures, simplifying client code.
- Facilitates addition of new component types without affecting existing code.
- Clearly define responsibilities of leaf and composite components to maintain clarity and simplicity.
Observer Pattern- Promotes loose coupling between objects through one-to-many dependency relationship.
- Supports broadcast communication efficiently propagating changes to multiple objects.
- Choose between event-driven and polling mechanisms based on system changes frequency and nature.
Strategy Pattern- Encapsulates algorithms, allowing independent variation from client code.
- Facilitates addition of new algorithms without modifying existing code.
- Use appropriate mechanisms (e.g., configuration, dependency injection) for dynamic strategy selection at runtime.

In this chapter, we explored various design patterns and their implementations in the C programming language. Design patterns provide elegant solutions to common design problems, making code more maintainable, reusable, and scalable. By understanding and applying these patterns, developers can improve the quality and structure of their C programs.Remember, design patterns are tools, not solutions for all problems. It's essential to understand the problem context and choose the appropriate pattern accordingly. Experiment with these patterns in your projects to gain a deeper understanding and mastery over them. Happy coding!❤️

Table of Contents

Contact here

Copyright © 2025 Diginode

Made with ❤️ in India