How would you implement a strategy pattern using interfaces in C?

Question

How would you implement a strategy pattern using interfaces in C?

Brief Answer

The Strategy Pattern in C allows you to interchange algorithms dynamically at runtime by effectively emulating interfaces using structs and function pointers. It’s ideal for scenarios where an object needs to choose between a family of related algorithms.

Here’s how it’s typically implemented:

  1. Abstract Strategy Interface: You define a `typedef struct` that contains one or more function pointers. This struct acts as your “contract,” specifying the methods that different concrete algorithms must implement (e.g., an `ISortingStrategy` struct with a `sort` function pointer).

  2. Concrete Strategies: These are the actual algorithm implementations. You create functions (or groups of functions within a struct) that match the signatures defined by the interface’s function pointers (e.g., `bubble_sort_impl`, `quick_sort_impl`). You’ll typically instantiate global or static instances of your interface struct, assigning these concrete functions to their respective function pointers.

  3. Context Struct: This is the client of the pattern. It holds a pointer to an instance of the abstract strategy interface (the struct of function pointers). The context then delegates the algorithm execution by simply calling the function pointer within its held strategy (e.g., a `SorterContext` holding an `ISortingStrategy*` and calling `context->strategy->sort(…)`).

Key Benefits to Emphasize:

  • Loose Coupling & High Cohesion: The context is completely decoupled from the specific algorithm implementations, interacting only through the generic interface.

  • Dynamic Algorithm Switching: Enables changing the behavior of an object at runtime without modifying its core logic.

  • Extensibility & Maintainability: You can easily add new strategies (algorithms) without modifying existing context or strategy code, which also significantly improves testability by isolating algorithm logic.

This pattern leverages C’s powerful function pointers to achieve a high degree of flexibility and modularity, similar to polymorphism in object-oriented languages, making it excellent for managing variations in behavior like different data processing methods or discount calculations.

Super Brief Answer

The Strategy Pattern in C allows dynamic interchangeability of algorithms. It’s implemented using a struct of function pointers as the abstract “interface,” concrete functions for each algorithm, and a context struct that holds a pointer to the chosen strategy and delegates execution. This achieves loose coupling and runtime flexibility, enabling easy addition of new algorithms without modifying core logic.

Detailed Answer

Related To: C Programming, Strategy Pattern, Design Patterns, Function Pointers, Structs, Polymorphism

Direct Summary:

In C, implementing the Strategy Pattern requires emulating interfaces using structs of function pointers. This struct defines the contract for an algorithm. You then create concrete strategy functions or structs that provide the actual implementations. A context struct holds a reference (a pointer) to the chosen strategy’s interface, allowing it to delegate the algorithm execution and dynamically switch between strategies at runtime, promoting flexible and maintainable code.

The Strategy Design Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. This pattern lets the algorithm vary independently from clients that use it. In C, while there are no native “interfaces” or “classes” in the object-oriented sense, you can effectively implement this pattern using structs and function pointers to achieve similar levels of flexibility and decoupling as in object-oriented languages.

Comprehensive C Code Example: Implementing Sorting Strategies

This example demonstrates how to implement the Strategy Pattern in C using function pointers and structs for different sorting algorithms.


#include <stdio.h> // For printf
#include <stdlib.h> // For general utilities like malloc (though not used in this simple example)

// 1. Define the Abstract Strategy Interface (using a struct of function pointers)
// This struct acts as our "interface contract"
typedef struct ISortingStrategy {
    // A function pointer that concrete strategies must implement
    void (*sort)(int* data, int size);
    // You could add other methods here if needed, e.g., a 'destroy' function
    // void (*destroy)(struct ISortingStrategy* self);
} ISortingStrategy;

// --- Concrete Strategy Implementations ---

// Bubble Sort Strategy
// The actual implementation function for Bubble Sort
void bubble_sort_impl(int* data, int size) {
    printf("Sorting using Bubble Sort\n");
    for (int i = 0; i < size - 1; i++) {
        for (int j = 0; j < size - i - 1; j++) {
            if (data[j] > data[j + 1]) {
                // Swap elements
                int temp = data[j];
                data[j] = data[j + 1];
                data[j + 1] = temp;
            }
        }
    }
}

// Global instance of the Bubble Sort strategy interface
// This acts as our "concrete strategy object"
ISortingStrategy bubble_sort_strategy = {
    .sort = bubble_sort_impl
};

// Quick Sort Strategy
// Helper functions for Quick Sort
void quick_sort_recursive(int* data, int low, int high);
int partition(int* data, int low, int high);

// The actual implementation function for Quick Sort
void quick_sort_impl(int* data, int size) {
    printf("Sorting using Quick Sort\n");
    quick_sort_recursive(data, 0, size - 1);
}

void quick_sort_recursive(int* data, int low, int high) {
    if (low < high) {
        int pi = partition(data, low, high);
        quick_sort_recursive(data, low, pi - 1);
        quick_sort_recursive(data, pi + 1, high);
    }
}

int partition(int* data, int low, int high) {
    int pivot = data[high];
    int i = (low - 1); // Index of smaller element
    for (int j = low; j < high; j++) {
        // If current element is smaller than or equal to pivot
        if (data[j] <= pivot) {
            i++; // Increment index of smaller element
            int temp = data[i];
            data[i] = data[j];
            data[j] = temp;
        }
    }
    int temp2 = data[i + 1];
    data[i + 1] = data[high];
    data[high] = temp2;
    return i + 1;
}

// Global instance of the Quick Sort strategy interface
ISortingStrategy quick_sort_strategy = {
    .sort = quick_sort_impl
};

// 3. Define the Context Struct
// This struct holds a pointer to the chosen strategy interface
typedef struct SorterContext {
    ISortingStrategy* strategy; // Pointer to the current sorting strategy
} SorterContext;

// Functions to manage the SorterContext
void sorter_context_init(SorterContext* context, ISortingStrategy* initial_strategy) {
    context->strategy = initial_strategy;
}

void sorter_context_set_strategy(SorterContext* context, ISortingStrategy* new_strategy) {
    context->strategy = new_strategy;
}

void sorter_context_execute_strategy(SorterContext* context, int* data, int size) {
    // Check if a strategy is set and if its sort method is valid
    if (context->strategy && context->strategy->sort) {
        context->strategy->sort(data, size);
    } else {
        printf("Error: No sorting strategy set or invalid strategy.\n");
    }
}

// Helper function to print an array (for demonstration)
void print_array(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// Example Usage in a main function (uncomment to run):
/*
int main() {
    int numbers1[] = {5, 2, 8, 1, 9, 4};
    int size1 = sizeof(numbers1) / sizeof(numbers1[0]);

    SorterContext context;

    // --- Using Bubble Sort ---
    printf("--- Using Bubble Sort ---\n");
    sorter_context_init(&context, &bubble_sort_strategy); // Initialize with Bubble Sort
    printf("Original array: ");
    print_array(numbers1, size1);
    sorter_context_execute_strategy(&context, numbers1, size1); // Execute Bubble Sort
    printf("Sorted array: ");
    print_array(numbers1, size1);

    printf("\n");

    // --- Switching to Quick Sort ---
    int numbers2[] = {10, 3, 7, 6, 1, 9, 2, 8, 4, 5}; // Reset list for clarity
    int size2 = sizeof(numbers2) / sizeof(numbers2[0]);

    printf("--- Switching to Quick Sort ---\n");
    sorter_context_set_strategy(&context, &quick_sort_strategy); // Change strategy to Quick Sort
    printf("Original array: ");
    print_array(numbers2, size2);
    sorter_context_execute_strategy(&context, numbers2, size2); // Execute Quick Sort
    printf("Sorted array: ");
    print_array(numbers2, size2);

    return 0;
}
*/