DEV Community

Cover image for Design Patterns for C
Amin Khozaei
Amin Khozaei

Posted on

Design Patterns for C

In the world of programming languages, C may not have flashy interfaces or trendy web apps. But underneath the surface, C is a key player, powering many of the technologies we rely on every day. It's efficient and has the ability to directly engage with hardware, making it essential in creating the strong foundations for countless technologies, from the computers in our vehicles to the operating systems that manage our devices. Even video games depend on C for seamless gameplay and outstanding performance. While other languages may handle the visuals, C ensures that the engine operates smoothly.

C's power and control come with complexity. Making large, maintainable C projects can be hard. Design patterns help with this. They are proven solutions to common design problems. They help bridge the gap between C's low-level nature and the need for well-structured code. Design patterns help you write cleaner, more readable C code that's easier for you and your team to understand and modify. They make your code flexible and adaptable to future changes.

1. What is a design pattern?

A design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design. It is not a finished design that can be transformed directly into code. It is a description or template for how to solve a problem that can be used in many different situations. Design patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system. These patterns focus on the relationships and interactions between classes or objects, without specifying the final application classes or objects that are involved.

Design patterns are typically classified into three main categories, each addressing a different aspect of software design:

  1. Creational Patterns: These patterns concentrate on creating objects in a controlled and flexible manner, decoupling object creation from specific use to promote reusability and better control over object instantiation.
  2. Structural Patterns: These patterns focus on how classes and objects are organized to create larger structures and functionalities. They provide methods for dynamically altering the code structure or adding new functionalities without significant modifications to the existing code.
  3. Behavioral Patterns: These patterns define how objects communicate with each other, enabling complex interactions and collaboration between different parts of your code. They promote loose coupling and improve the overall organization and maintainability of your software.

1.1. Object-oriented Paradigm

Design patterns are largely influenced by object-oriented programming (OOP) and are categorized using objects, although some patterns can be implemented without them. It is possible to apply design patterns in C by utilizing fundamental concepts such as functions, pointers, and structs. This can enhance code cleanliness and maintainability without relying on object-oriented features.

Even though C lacks built-in object-oriented features like classes and inheritance, it can still achieve object-oriented-like behavior using clever techniques with functions, pointers, and structs. Popular libraries like GLib showcase this by implementing object-oriented features within the C language.

Creational Design Patterns

2. Creational Patterns

Creational design patterns provide various object creation mechanisms, which increase flexibility and reuse of existing code.

2.1. Factory design pattern

It provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. The primary goal of the Factory pattern is to encapsulate the object creation process, making it more modular, scalable, and maintainable.

2.1.1. Key Concepts

  • Encapsulation of Object Creation: The Factory pattern encapsulates the logic of creating objects, which means that the client code does not need to know the specific classes being instantiated.
  • Decoupling: It decouples the code that uses the objects from the code that creates the objects, promoting loose coupling.
  • Flexibility: The pattern allows for adding new types of objects easily without changing the client code.

2.1.2. Example Code

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    void (*draw)();
} Shape;

typedef struct {
    Shape base;
} Circle;

void draw_circle() {
    printf("Drawing a Circle\n");
}

Circle* create_circle() {
    Circle* circle = (Circle*)malloc(sizeof(Circle));
    circle->base.draw = draw_circle;
    return circle;
}

typedef struct {
    Shape base;
} Square;

void draw_square() {
    printf("Drawing a Square\n");
}

Square* create_square() {
    Square* square = (Square*)malloc(sizeof(Square));
    square->base.draw = draw_square;
    return square;
}

typedef enum {
    SHAPE_CIRCLE,
    SHAPE_SQUARE
} ShapeType;

Shape* shape_factory(ShapeType type) {
    switch (type) {
        case SHAPE_CIRCLE:
            return (Shape*)create_circle();
        case SHAPE_SQUARE:
            return (Shape*)create_square();
        default:
            return NULL;
    }
}

int main() {
    // Create a Circle using the factory
    Shape* shape1 = shape_factory(SHAPE_CIRCLE);
    if (shape1 != NULL) {
        shape1->draw();
        free(shape1);
    }

    // Create a Square using the factory
    Shape* shape2 = shape_factory(SHAPE_SQUARE);
    if (shape2 != NULL) {
        shape2->draw();
        free(shape2);
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

2.1.3. Known Uses

libcurl uses factory functions to create and initialize different types of handles for various I/O operations. This is essential for setting up the appropriate environment for the different kinds of operations it supports.

#include <curl/curl.h>

int main(void)
{
    // Initialize CURL globally
    curl_global_init(CURL_GLOBAL_DEFAULT);

    // Create and initialize an easy handle using the factory function
    CURL *easy_handle = curl_easy_init();
    if(easy_handle) {
        // Set options for the easy handle
        curl_easy_setopt(easy_handle, CURLOPT_URL, "http://example.com");
        curl_easy_setopt(easy_handle, CURLOPT_FOLLOWLOCATION, 1L);

        // Perform the transfer
        CURLcode res = curl_easy_perform(easy_handle);
        if(res != CURLE_OK) {
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        }

        // Clean up the easy handle
        curl_easy_cleanup(easy_handle);
    }

    // Clean up CURL globally
    curl_global_cleanup();

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

2.2. Singleton Pattern

This pattern is useful when exactly one object is needed to coordinate actions across a system. Common examples include configuration objects, connection pools, and logging mechanisms.

2.2.1. Key Concepts

  • Single Instance: Ensures that only one instance of the struct is created.

  • Global Access: Provides a global point of access to the instance.

  • Lazy Initialization: The instance is created only when it is needed for the first time.

2.2.2. Example Code

#include <stdio.h>
#include <stdlib.h>

// Singleton structure
typedef struct {
    int data;
    // Other members...
} Singleton;

// Static variable to hold the single instance
static Singleton* instance = NULL;

// Function to get the instance of the singleton
Singleton* get_instance() {
    if (instance == NULL) {
        instance = (Singleton*)malloc(sizeof(Singleton));
        instance->data = 0; // Initialize with default values
    }
    return instance;
}

// Function to free the instance (optional, for cleanup)
void free_instance() {
    if (instance != NULL) {
        free(instance);
        instance = NULL;
    }
}

int main() {
    // Get the singleton instance and use it
    Singleton* s1 = get_instance();
    s1->data = 42;
    printf("Singleton data: %p\n", s1);

    // Get the singleton instance again
    Singleton* s2 = get_instance();

    printf("Singleton data: %p\n", s2);

    free_instance();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

The static keyword in the statement static Singleton* instance = NULL; makes the variable private to the file in C. This means that when included in other files, they can't access the instance variable directly and must access it through corresponding functions. The %p in the statement printf("Singleton data: %p\n", s1); prints the address of the s1 variable.

2.2.3. Known Uses

Here's an example of using GLib's main loop in C to handle a simple timer event:

#include <stdio.h>
#include <glib.h>

gboolean timer_callback(gpointer user_data) {
  printf("Timer fired!\n");
  return TRUE; // Keep the timer running
}

int main() {
  GMainLoop *loop = g_main_loop_new(NULL, FALSE); // Create the main loop

  // Set up a timer to fire every 1 second
  guint timer_id = g_timeout_add_seconds(1, (GSourceFunc)timer_callback, NULL);

  printf("Starting the main loop...\n");
  g_main_loop_run(loop); // Run the main loop

  printf("Stopping the main loop...\n");
  g_source_remove(timer_id); // Remove the timer source
  g_main_loop_unref(loop); // Free the loop resources

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Structural Design Pattern

3. Structural Patterns

Structural patterns are design patterns that ease the design by identifying a simple way to realize relationships between entities. These patterns focus on how classes and objects are composed to form larger structures, providing solutions for creating flexible and efficient structures.

3.1. Adapter Patterns

The Adapter design pattern, also known as Wrapper, is a structural pattern that allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by converting the interface of a class into another interface that the client expects. This pattern is particularly useful when integrating existing components with new systems without modifying the existing components.

3.1.1. Key Concepts

  • Target Interface: The interface that the client expects.

  • Adaptee: The existing interface that needs to be adapted.

  • Adapter: A class that implements the target interface and translates the requests from the client to the adaptee.

3.1.2. Example Code

#include <stdio.h>
#include <stdlib.h>

// Old printer interface (Adaptee)
typedef struct {
    void (*print_old)(const char *message);
} OldPrinter;

void old_print(const char *message) {
    printf("Old Printer: %s\n", message);
}

OldPrinter* create_old_printer() {
    OldPrinter* printer = (OldPrinter*)malloc(sizeof(OldPrinter));
    printer->print_old = old_print;
    return printer;
}

// New printer interface (Target)
typedef struct {
    void (*print)(const char *message);
} NewPrinter;

// Adapter
typedef struct {
    NewPrinter base;
    OldPrinter* old_printer;
} PrinterAdapter;

void adapter_print(const char *message) {
    // Adapt the old print method to the new print method
    printf("Adapter: ");
    old_print(message);
}

NewPrinter* create_printer_adapter(OldPrinter* old_printer) {
    PrinterAdapter* adapter = (PrinterAdapter*)malloc(sizeof(PrinterAdapter));
    adapter->base.print = adapter_print;
    adapter->old_printer = old_printer;
    return (NewPrinter*)adapter;
}

// Client code
int main() {
    // Create the old printer (Adaptee)
    OldPrinter* old_printer = create_old_printer();

    // Create the adapter that adapts the old printer to the new interface
    NewPrinter* new_printer = create_printer_adapter(old_printer);

    // Use the new interface to print a message
    new_printer->print("Hello, world!");

    // Cleanup
    free(new_printer);
    free(old_printer);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

3.1.3. Known Uses

Here is an example that demonstrates an adapter-like approach in GTK+ which provides widgets and functionalities for building graphical user interfaces (GUIs).

#include <gtk/gtk.h>

typedef struct config_data_t {
  char *option_name;
  int option_value;
} config_data_t;

// Adapter function 
void config_data_to_combobox(GtkComboBoxText *combobox, config_data_t *data) {

  // Adaptee
  gtk_combo_box_text_append_text(combobox, data->option_name);
  g_object_set_data(G_OBJECT(combobox), "option-value", GINT_TO_POINTER(data->option_value));
}

static void destroy(GtkWidget *widget, gpointer data) {
  gtk_main_quit();
}

int main(int argc, char *argv[]) {
  gtk_init(&argc, &argv);

  // Create a window
  GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title(GTK_WINDOW(window), "Configuration Options");
  g_signal_connect(window, "destroy", G_CALLBACK(destroy), NULL);

  // Create a vertical box container
  GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
  gtk_container_add(GTK_CONTAINER(window), vbox);

  // Create a label for the combobox
  GtkWidget *label = gtk_label_new("Select an option:");
  gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 5);

  // Create a GtkComboBoxText widget
  GtkWidget *combobox = gtk_combo_box_text_new();
  gtk_box_pack_start(GTK_BOX(vbox), combobox, FALSE, FALSE, 5);

  config_data_t data1 = {"Option 1", 10};
  config_data_t data2 = {"Option 2", 20};

  // Populate the combobox using the adapter function
  config_data_to_combobox(GTK_COMBO_BOX_TEXT(combobox), &data1);
  config_data_to_combobox(GTK_COMBO_BOX_TEXT(combobox), &data2);

  // Show all widgets
  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

3.2. Facade Design Pattern

The Facade Design Pattern is a structural pattern that offers a simplified interface to a complex subsystem. It consists of creating a single class (the Facade) that provides simplified methods, which then delegate calls to the more complex underlying system, making it easier to use. This pattern provides a unified interface to a set of interfaces in a subsystem, defining a higher-level interface that makes the subsystem easier to use.

3.2.1. Key Concepts

  • Simplified Interface: The Facade offers a high-level interface that makes the subsystem easier to use.
  • Encapsulation: It hides the complexities of the subsystem from the client.
  • Delegation: The Facade delegates the client requests to appropriate components within the subsystem.

3.2.2. Example Code

#include <stdio.h>

// TV component
typedef struct {
    int is_on;
} TV;

void tv_on(TV *tv) {
    tv->is_on = 1;
    printf("TV is ON\n");
}

void tv_off(TV *tv) {
    tv->is_on = 0;
    printf("TV is OFF\n");
}

// DVD Player component
typedef struct {
    int is_on;
    char movie[50];
} DVDPlayer;

void dvd_on(DVDPlayer *dvd) {
    dvd->is_on = 1;
    printf("DVD Player is ON\n");
}

void dvd_off(DVDPlayer *dvd) {
    dvd->is_on = 0;
    printf("DVD Player is OFF\n");
}

void dvd_play_movie(DVDPlayer *dvd, const char *movie) {
    if (dvd->is_on) {
        strcpy(dvd->movie, movie);
        printf("Playing movie: %s\n", dvd->movie);
    } else {
        printf("DVD Player is OFF. Cannot play movie.\n");
    }
}

// Sound System component
typedef struct {
    int is_on;
} SoundSystem;

void sound_on(SoundSystem *sound) {
    sound->is_on = 1;
    printf("Sound System is ON\n");
}

void sound_off(SoundSystem *sound) {
    sound->is_on = 0;
    printf("Sound System is OFF\n");
}

typedef struct {
    TV tv;
    DVDPlayer dvd;
    SoundSystem sound;
} HomeTheaterFacade;

HomeTheaterFacade* create_home_theater() {
    HomeTheaterFacade* theater = (HomeTheaterFacade*)malloc(sizeof(HomeTheaterFacade));
    theater->tv.is_on = 0;
    theater->dvd.is_on = 0;
    theater->sound.is_on = 0;
    return theater;
}

void home_theater_on(HomeTheaterFacade *theater) {
    tv_on(&theater->tv);
    dvd_on(&theater->dvd);
    sound_on(&theater->sound);
    printf("Home Theater is ON\n");
}

void home_theater_off(HomeTheaterFacade *theater) {
    tv_off(&theater->tv);
    dvd_off(&theater->dvd);
    sound_off(&theater->sound);
    printf("Home Theater is OFF\n");
}

void home_theater_play_movie(HomeTheaterFacade *theater, const char *movie) {
    home_theater_on(theater);
    dvd_play_movie(&theater->dvd, movie);
}

int main() {
    // Create the home theater system
    HomeTheaterFacade* theater = create_home_theater();
    // Use the Facade to play a movie
    home_theater_play_movie(theater, "The Matrix");
    // Turn off the home theater system
    home_theater_off(theater);
    // Cleanup
    free(theater);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

3.2.3. Known Uses

GStreamer uses the Facade pattern to offer a simple interface for creating and managing multimedia pipelines, hiding the complexity of the underlying media processing components.

#include <gst/gst.h>

int main(int argc, char *argv[]) {
    GstElement *pipeline;
    GstBus *bus;
    GstMessage *msg;

    /* Initialize GStreamer */
    gst_init(&argc, &argv);

    /* Build the pipeline using playbin as Facade pattern */
    pipeline = gst_parse_launch("playbin uri=file:///path/to/video", NULL);

    /* Start playing */
    gst_element_set_state(pipeline, GST_STATE_PLAYING);

    /* Wait until error or EOS */
    bus = gst_element_get_bus(pipeline);
    msg = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

    /* Free resources */
    if (msg != NULL)
        gst_message_unref(msg);
    gst_object_unref(bus);
    gst_element_set_state(pipeline, GST_STATE_NULL);
    gst_object_unref(pipeline);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

3.3. Proxy Pattern

The Proxy Design Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. A proxy can perform additional operations, such as access control, lazy initialization, logging, or even caching, before or after forwarding the request to the real object.

3.3.1. Key Concepts

  • Proxy: The proxy object, which implements the same interface as the real object and controls access to it.
  • Real Subject: The actual object that performs the operations. The proxy forwards the requests to this object.

3.3.2. Example Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Image interface
typedef struct {
    void (*display)();
} Image;

// RealImage implementation
typedef struct {
    Image base;
    char *filename;
} RealImage;

void real_image_display(RealImage *real_image) {
    printf("Displaying image: %s\n", real_image->filename);
}

RealImage* create_real_image(const char *filename) {
    RealImage *real_image = (RealImage*)malloc(sizeof(RealImage));
    real_image->base.display = (void (*)())real_image_display;
    real_image->filename = strdup(filename);
    return real_image;
}

// ProxyImage implementation
typedef struct {
    Image base;
    RealImage *real_image;
    char *filename;
} ProxyImage;

void proxy_image_display(ProxyImage *proxy_image) {
    // Add logging functionality
    printf("Proxy: Logging display request for image: %s\n", proxy_image->filename);

    // Lazy initialization of the RealImage
    if (proxy_image->real_image == NULL) {
        proxy_image->real_image = create_real_image(proxy_image->filename);
    }

    // Forward the request to the RealImage
    proxy_image->real_image->base.display(proxy_image->real_image);
}

ProxyImage* create_proxy_image(const char *filename) {
    ProxyImage *proxy_image = (ProxyImage*)malloc(sizeof(ProxyImage));
    proxy_image->base.display = proxy_image_display;
    proxy_image->real_image = NULL;
    proxy_image->filename = strdup(filename);
    return proxy_image;
}

// Client code
int main() {
    // Create the proxy image
    ProxyImage *proxy_image = create_proxy_image("example.jpg");

    // Use the proxy to display the image
    proxy_image->base.display(proxy_image);

    // Clean up
    if (proxy_image->real_image != NULL) {
        free(proxy_image->real_image->filename);
        free(proxy_image->real_image);
    }
    free(proxy_image->filename);
    free(proxy_image);

    return 0;
}

Enter fullscreen mode Exit fullscreen mode

3.3.3. Known Uses

Here’s an example demonstrating how the STM32 HAL library acts as a proxy for hardware, specifically for configuring and using a GPIO pin:

#include "stm32f4xx_hal.h"

// Initialization function for GPIO
void GPIO_Init(void) {
    __HAL_RCC_GPIOA_CLK_ENABLE();  // Enable the GPIOA clock

    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // Configure GPIO pin : PA5 (typically the onboard LED)
    GPIO_InitStruct.Pin = GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

// Function to toggle the GPIO pin
void Toggle_LED(void) {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}

int main(void) {
    // HAL initialization
    HAL_Init();

    // Configure the system clock
    SystemClock_Config();

    // Initialize GPIO
    GPIO_Init();

    // Main loop
    while (1) {
        Toggle_LED();
        HAL_Delay(1000);  // Delay 1 second
    }
}

void SystemClock_Config(void) {
    // System Clock Configuration code
}
Enter fullscreen mode Exit fullscreen mode

Behavioral Design Pattern

4. Behavioral Patterns

Behavioral design patterns are concerned with algorithms and the assignment of responsibilities between objects. These patterns describe not just patterns of objects or classes but also the patterns of communication between them. They help in defining the flow of control and communication between objects.

4.1. Observer Pattern

The Observer Pattern (Publish-Subscribe) is a behavioral design pattern that defines a one-to-many dependency between objects. When one object (the subject) changes state, all its dependents (observers) are notified and updated automatically. This pattern is particularly useful for implementing distributed event-handling systems.

4.1.1. Key Concepts

  • Subject: The object that holds the state and notifies observers of changes.
  • Observers: The objects that are notified and updated when the subject changes.

4.1.2. Example Code

#include <stdio.h>

// Subject (weather sensor)
typedef struct weather_sensor_t {
  double temperature;
  void (*update_observers)(weather_sensor_t *sensor); // Function pointer for notifications
} weather_sensor_t;

// Observer (thermostat)
void thermostat_update(weather_sensor_t *sensor) {
  printf("Temperature changed to: %.2f degrees Celsius\n", sensor->temperature);
  // Simulate thermostat adjustment based on temperature
}

// Registering observer with sensor
void register_thermostat(weather_sensor_t *sensor) {
  sensor->update_observers = thermostat_update; // Assign observer function
}

// Simulating temperature change and notification
void weather_sensor_set_temperature(weather_sensor_t *sensor, double temp) {
  sensor->temperature = temp;
  if (sensor->update_observers) {
    sensor->update_observers(sensor); // Call observer function if registered
  }
}

int main() {
  weather_sensor_t sensor;
  register_thermostat(&sensor); // Register thermostat

  weather_sensor_set_temperature(&sensor, 22.3); // Simulate temperature change

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

4.1.3. Known Uses

The signal/slot mechanism in GLib is a great way to illustrate the Observer pattern. In GLib, signals are emitted by objects when certain events occur, and slots (callbacks) are functions that are called in response to those signals.

#include <glib.h>
#include <stdio.h>

// Subject (GObject that emits signals)
typedef struct {
    GObject parent_instance;
    int state;
} MySubject;

typedef struct {
    GObjectClass parent_class;
} MySubjectClass;

enum {
    STATE_CHANGED,
    LAST_SIGNAL
};

static guint my_subject_signals[LAST_SIGNAL] = { 0 };

// Signal emission function
void my_subject_set_state(MySubject *self, int new_state) {
    if (self->state != new_state) {
        self->state = new_state;
        g_signal_emit(self, my_subject_signals[STATE_CHANGED], 0, self->state);
    }
}

#define MY_TYPE_SUBJECT (my_subject_get_type())
G_DECLARE_FINAL_TYPE(MySubject, my_subject, MY, SUBJECT, GObject)

G_DEFINE_TYPE(MySubject, my_subject, G_TYPE_OBJECT)

static void my_subject_class_init(MySubjectClass *klass) {
    my_subject_signals[STATE_CHANGED] = g_signal_new(
        "state-changed",
        G_TYPE_FROM_CLASS(klass),
        G_SIGNAL_RUN_FIRST,
        0,
        NULL,
        NULL,
        NULL,
        G_TYPE_NONE,
        1,
        G_TYPE_INT
    );
}

static void my_subject_init(MySubject *self) {
    self->state = 0;
}

// Observer (callback function)
void on_state_changed(MySubject *subject, int new_state, gpointer user_data) {
    printf("Observer: State changed to %d\n", new_state);
}

// Main function
int main(int argc, char *argv[]) {
    // Initialize GType system
    g_type_init();

    // Create subject instance
    MySubject *subject = g_object_new(MY_TYPE_SUBJECT, NULL);

    // Connect observer to the signal
    g_signal_connect(subject, "state-changed", G_CALLBACK(on_state_changed), NULL);

    // Change state and emit signal
    my_subject_set_state(subject, 10);
    my_subject_set_state(subject, 20);

    // Cleanup
    g_object_unref(subject);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

4.2. Strategy Pattern

The Strategy Design Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from the clients that use it. The Strategy pattern is particularly useful when you need to switch between different algorithms or behaviors at runtime.

4.2.1. Key Concepts

  • Strategy Interface: Defines a common interface for all supported algorithms.
  • Concrete Strategies: Implement the algorithm using the Strategy interface.
  • Context: Maintains a reference to a Strategy object and uses it to execute the algorithm.

4.2.2. Example Code

‍‍‍

#include <stdio.h>

// Interface for sorting algorithms
typedef int (*sort_function_t)(int *data, size_t data_size);

// Concrete strategy - Bubble sort implementation
int bubble_sort(int *data, size_t data_size) {
  for (size_t i = 0; i < data_size - 1; i++) {
    for (size_t j = 0; j < data_size - i - 1; j++) {
      if (data[j] > data[j + 1]) {
        int temp = data[j];
        data[j] = data[j + 1];
        data[j + 1] = temp;
      }
    }
  }
  return 0;
}

// Concrete strategy - Selection sort implementation
int selection_sort(int *data, size_t data_size) {
  for (size_t i = 0; i < data_size - 1; i++) {
    int min_index = i;
    for (size_t j = i + 1; j < data_size; j++) {
      if (data[j] < data[min_index]) {
        min_index = j;
      }
    }
    if (i != min_index) {
      int temp = data[i];
      data[i] = data[min_index];
      data[min_index] = temp;
    }
  }
  return 0;
}

// Context (sorting utility) using strategy pattern
void sort(int *data, size_t data_size, sort_function_t sort_function) {
  if (sort_function(data, data_size) == 0) {
    printf("Sorting successful!\n");
  } else {
    printf("Error during sorting!\n");
  }
}

int main() {
  int data[] = {5, 2, 8, 1, 3};
  size_t data_size = sizeof(data) / sizeof(data[0]);

  // Choose sorting strategy (can be dynamic based on criteria)
  sort_function_t sort_strategy = bubble_sort;

  sort(data, data_size, sort_strategy);

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

4.2.3. Known Uses

This example will show how to compress data using different zlib compression strategies: default, best speed, and best compression.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h>


int main() {
    const char* input_data = "This is some data to be compressed.";
    size_t input_size = strlen(input_data);
    char output_data[100];
    size_t output_size = sizeof(output_data);
    int result;

    // Using default compression level
    result = compress2((Bytef*)output_data, (uLongf*)output_size, (const Bytef*)input_data, input_size, Z_DEFAULT_COMPRESSION);
    if (result == Z_OK) {
        printf("Default compression success. Compressed size: %zu\n", output_size);
    } else {
        printf("Default compression failed.\n");
    }

    // Reset output size for the next test
    output_size = sizeof(output_data);

    // Using best speed compression level
    result = compress2((Bytef*)output_data, (uLongf*)output_size, (const Bytef*)input_data, input_size, Z_BEST_SPEED);
    if (result == Z_OK) {
        printf("Best speed compression success. Compressed size: %zu\n", output_size);
    } else {
        printf("Best speed compression failed.\n");
    }

    // Reset output size for the next test
    output_size = sizeof(output_data);

    // Using best compression level
    result = compress2((Bytef*)output_data, (uLongf*)output_size, (const Bytef*)input_data, input_size, Z_BEST_COMPRESSION);
    if (result == Z_OK) {
        printf("Best compression success. Compressed size: %zu\n", output_size);
    } else {
        printf("Best compression failed.\n");
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

4.3. State Pattern

The State Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. The object will appear to change its class. This pattern is particularly useful when an object must change its behavior at runtime depending on its state.

4.3.1. Key Concepts

  • Context: The object whose behavior varies based on its state. It maintains a reference to an instance of a state subclass that defines the current state.
  • State Interface: Declares methods that concrete states must implement.
  • Concrete States: Implement the behavior associated with a state of the Context.

4.3.2. Example Code

"Hunt the Wumpus" is a classic text-based adventure game where the player navigates through a network of interconnected rooms in a cave system to hunt a creature called the Wumpus. The player can move through rooms, shoot arrows to kill the Wumpus, and must avoid various hazards such as bottomless pits and super bats. The game provides sensory hints, like smells and sounds, to help the player deduce the locations of the Wumpus and hazards. The player wins by successfully shooting the Wumpus with an arrow, and loses if they enter a room with the Wumpus, fall into a pit, or get carried away by bats. Here's an example in C that implements a simplified Wumpus game and saves state using the State pattern:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Game constants
#define ROOMS 5

// Forward declaration of Context
typedef struct Player Player;

// State interface
typedef struct PlayerState {
    void (*move)(Player*, int);
} PlayerState;

// Context
struct Player {
    PlayerState* state;
    int current_room;
    void (*set_state)(Player*, PlayerState*);
};

// Function to set the state
void set_player_state(Player* player, PlayerState* state) {
    player->state = state;
}

// Function to create a Player
Player* create_player() {
    Player* player = (Player*)malloc(sizeof(Player));
    player->state = NULL;
    player->current_room = 0;
    player->set_state = set_player_state;
    return player;
}

// Forward declarations of state structs
extern PlayerState alive_state;
extern PlayerState dead_state;
extern PlayerState won_state;

// Concrete State: Alive
void alive_move(Player* player, int room);
PlayerState alive_state = { alive_move };

void alive_move(Player* player, int room) {
    player->current_room = room;
    printf("Player moves to room %d.\n", room);
    // For simplicity, let's assume:
    // Room 2 has the Wumpus, Room 4 has the gold.
    if (room == 2) {
        printf("Player encountered the Wumpus and died!\n");
        player->set_state(player, &dead_state);
    } else if (room == 4) {
        printf("Player found the gold and won!\n");
        player->set_state(player, &won_state);
    }
}

// Concrete State: Dead
void dead_move(Player* player, int room);
PlayerState dead_state = { dead_move };

void dead_move(Player* player, int room) {
    printf("Player is dead and cannot move.\n");
}

// Concrete State: Won
void won_move(Player* player, int room);
PlayerState won_state = { won_move };

void won_move(Player* player, int room) {
    printf("Player has already won and cannot move.\n");
}

// Client code
int main() {
    // Create a player
    Player* player = create_player();

    // Initial state: Alive
    player->set_state(player, &alive_state);

    // Move the player to different rooms
    player->state->move(player, 1); // Move to room 1
    player->state->move(player, 2); // Encounter the Wumpus and die
    player->state->move(player, 3); // Cannot move because player is dead

    // Reset player to alive state for the next test
    player->set_state(player, &alive_state);

    player->state->move(player, 3); // Move to room 3
    player->state->move(player, 4); // Find the gold and win
    player->state->move(player, 5); // Cannot move because player has won

    // Clean up
    free(player);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

4.3.3. Known Uses

In GStreamer, the State pattern is used to handle the state transitions of a media pipeline. The states control the flow of media data through the pipeline, ensuring that elements are properly initialized, negotiated, and ready to process media data.

#include <gst/gst.h>

// Function to change the state of the pipeline and print the new state
void change_pipeline_state(GstElement *pipeline, GstState state) {
    GstStateChangeReturn ret = gst_element_set_state(pipeline, state);

    if (ret == GST_STATE_CHANGE_FAILURE) {
        g_printerr("Unable to set the pipeline to the desired state.\n");
        return;
    }

    GstState current_state;
    gst_element_get_state(pipeline, &current_state, NULL, GST_CLOCK_TIME_NONE);
    g_print("Pipeline state changed to %s\n", gst_element_state_get_name(current_state));
}

int main(int argc, char *argv[]) {
    gst_init(&argc, &argv);

    // Create the pipeline
    GstElement *pipeline = gst_pipeline_new("example-pipeline");

    // Set the pipeline to the NULL state
    change_pipeline_state(pipeline, GST_STATE_NULL);

    // Set the pipeline to the READY state
    change_pipeline_state(pipeline, GST_STATE_READY);

    // Set the pipeline to the PAUSED state
    change_pipeline_state(pipeline, GST_STATE_PAUSED);

    // Set the pipeline to the PLAYING state
    change_pipeline_state(pipeline, GST_STATE_PLAYING);

    // Clean up
    gst_object_unref(pipeline);
    gst_deinit();

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

References

Websites

Books

  • E. Gamma, R. Helm, R. Johnson, and J. Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software.

Design Patterns: Elements of Reusable Object-Oriented Software

  • Douglas, B. (2010). Design Patterns for Embedded Systems in C.

Design Patterns for Embedded Systems in C

Top comments (2)

Collapse
 
nasser_firouzpour_48ea4a6 profile image
Nasser Firouzpour

Great as always 👌

Collapse
 
khozaei profile image
Amin Khozaei

Thank you