Command Pattern - Part III
Deferred execution requires capturing a command instance that persists beyond the moment it was created. So far, the command objects had a fixed state that didn’t change over time, which made it possible to reuse the same instances simply by referencing them using a pointer.
However, the design changes once we introduce the idea of state mutation. A mutated command is one whose internal fields are updated on each invocation, meaning that the object no longer represents a timeless action but a piece of data that evolves over time.
Suppose for instance that a time stamp needs to be added to the command. This immediately introduces the need of creating a new command instance on each invocation. The invoker now needs to create a fresh command object for every event it captures, instead of just enqueueing an existing pointer.
Deferred execution and state mutation pushes towards dynamic memory allocation
Since each deferred invocation requires its own distinct command instance, the command objects can no longer remain stack‑allocated. The logical conclusion is to use dynamic memory allocation and reserve space on the heap, since heap-allocated objects persist until explicitly released and therefore satisfy the lifetime requirements of deferred execution.
This change also affects the division of responsibilities:
- The invoker becomes responsible for creating a new command instance and enqueueing it.
- The executor becomes responsible for dequeueing a command, executing it and then destroying it.
- The command interface must now expose creation and destruction operations. Instead of placing these responsibilities directly on the command interface, a dedicated command factory interface is introduced.
- The queue must carry not only the command object but also the factory responsible for destroying it. To support this, a bundled interface is introduced, which aggregates references to the command interface and the command factory interface into a single transport object.
Updated interfaces (cmd.h)
Command interface
typedef struct cmd_t{
void (*execute)(struct cmd_t *self);
} cmd_t;
Command factory interface
typedef struct cmd_factory_t {
cmd_t* (*create)(struct cmd_factory_t *self);
void (*destroy)(cmd_t *cmd);
} cmd_factory_t;
Bundled command + factory interface
Each enqueued item must carry references to both the command instance and the factory that created it, so the invoker knows how to create the command and the executor knows how to destroy it after execution.
typedef struct {
cmd_t *cmd;
cmd_factory_t *cmd_factory;
} cmd_bundle_t;
Bundling Helper
static inline cmd_bundle_t CMD_Bundle(cmd_t *cmd, cmd_factory_t *cmd_factory) {
cmd_bundle_t bundle = {
.cmd = cmd,
.cmd_factory = cmd_factory
};
return bundle;
}
The New Architecture
The updated architectural diagram presents a modular and extensible architectural skeleton built around three core concepts: command interface, command factory interface, and deferred execution through a queue.
Command factories introduce controlled dynamic allocation, ensuring that command objects are created on demand and destroyed safely by the executor without exposing allocation details to the rest of the system.
The invoker still interacts exclusively with interfaces rather than concrete implementations, preserving full compliance with the GoF Command Pattern and maintaining strict decoupling between input handling and command execution.
The diagram below depicts the new architecture:
C implementation
Below are the modifications applied to the previous implementation in order to support the new architecture:
executor.h
#ifndef EXECUTOR_H
#define EXECUTOR_H
#include <stdbool.h>
#include "cmd.h"
#include "circular_buffer.h"
bool EXECUTOR_Execute(cmd_bundle_t cmd_bundle);
cmd_bundle_t EXECUTOR_Dequeue(circular_buffer_t* cb);
executor.c
#include "executor.h"
bool EXECUTOR_Execute(cmd_bundle_t cmd_bundle){
cmd_t* cmd = cmd_bundle.cmd;
cmd_factory_t* cmd_factory = cmd_bundle.cmd_factory;
if (cmd != NULL && cmd_factory->destroy != NULL){
cmd->execute(cmd);
cmd_factory->destroy(cmd);
return false;
}
return true;
}
cmd_bundle_t EXECUTOR_Dequeue(circular_buffer_t* cb){
cmd_bundle_t cmd_bundle = {0};
if (cb == NULL) {
return cmd_bundle;
}
if (CIRCULAR_BUFFER_Read(cb, &cmd_bundle) != BUFFER_EMPTY) {
return cmd_bundle;
}
return cmd_bundle;
}
invoker_keypad.h
#include "cmd.h"
#include "circular_buffer.h"
#define KEYPAD_KEYS 4
typedef enum {
KEYPAD_KEY_1 = 0,
KEYPAD_KEY_2,
KEYPAD_KEY_3,
KEYPAD_KEY_4
} keypad_key_t;
void KEYPAD_Init(circular_buffer_t *cb);
void KEYPAD_SetCommand(keypad_key_t key, cmd_factory_t* cmd);
void KEYPAD_KeyPress(keypad_key_t key);
invoker_keypad.c
#include <assert.h>
#include "invoker_keypad.h"
static cmd_factory_t* commands[KEYPAD_KEYS];
static circular_buffer_t *queue = NULL;
void KEYPAD_Init(circular_buffer_t *cb){
assert (cb != NULL);
for (int i = 0; i < KEYPAD_KEYS; i++) {
commands[i] = NULL;
}
queue = cb;
}
void KEYPAD_SetCommand(keypad_key_t key, cmd_factory_t* cmd_factory) {
assert(key < KEYPAD_KEYS);
commands[key] = cmd_factory;
}
void KEYPAD_KeyPress(keypad_key_t key) {
assert(key < KEYPAD_KEYS);
cmd_factory_t *cmd_factory = commands[key];
cmd_t* cmd = cmd_factory->create(cmd_factory);
cmd_bundle_t cmd_bundle = CMD_Bundle(cmd, cmd_factory);
buffer_status_t cb_status = CIRCULAR_BUFFER_Write(queue, &cmd_bundle);
assert(cb_status == BUFFER_OK);
}
cmd_led.h
#ifndef CMD_LED_H
#define CMD_LED_H
#include "cmd.h"
#include "gpio_api.h"
typedef struct {
cmd_factory_t base;
gpio_t led_id;
} cmd_factory_led_t;
cmd_factory_t* CMD_LED_FactoryInit(cmd_factory_led_t* self, gpio_t led_id);
#endif
cmd_led.c
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include "cmd_led.h"
typedef struct {
cmd_t base;
gpio_t led_id;
time_t timestamp;
} cmd_led_t;
static void CMD_LED_Toggle(cmd_t *self) {
cmd_led_t *cmd = (cmd_led_t *)self;
printf("Time Stamp: %ld - ",cmd->timestamp);
if (GPIO_ReadPin(cmd->led_id)){
GPIO_SetPinL(cmd->led_id);
} else {
GPIO_SetPinH(cmd->led_id);
}
}
static cmd_t* CMD_LED_Init(cmd_led_t* self, gpio_t led_id){
assert(self != NULL);
if (self == NULL) return NULL;
self->led_id = led_id;
self->base.execute = CMD_LED_Toggle;
self->timestamp = rand(); // Random number, simulating a different time stamp per call
return (cmd_t*)self;
}
static cmd_t* CMD_LED_FactoryCreate(cmd_factory_t *base) {
cmd_factory_led_t *factory = (cmd_factory_led_t*)base;
cmd_led_t *cmd = malloc(sizeof(cmd_led_t));
return CMD_LED_Init(cmd, factory->led_id);
}
static void CMD_LED_FactoryDestroy(cmd_t *cmd) {
cmd_led_t *led_cmd = (cmd_led_t*)cmd;
free(led_cmd);
}
cmd_factory_t* CMD_LED_FactoryInit(cmd_factory_led_t* self, gpio_t led_id) {
self->led_id = led_id;
self->base.create = CMD_LED_FactoryCreate;
self->base.destroy = CMD_LED_FactoryDestroy;
return (cmd_factory_t*)self;
}
main.c (client)
#include <time.h>
#include <stdlib.h>
#include "cmd_led.h"
#include "executor.h"
#include "invoker_keypad.h"
#include "circular_buffer.h"
int main(void) {
// Stack-allocated command factory objects
cmd_factory_led_t g_cmd_factory_led1;
cmd_factory_led_t g_cmd_factory_led2;
// Array of cmd_bundle_t
cmd_bundle_t cmd_bundle[10] = {NULL};
circular_buffer_t command_buffer;
srand(time(NULL));
CIRCULAR_BUFFER_Init(&command_buffer, cmd_bundle, sizeof(cmd_bundle)/sizeof(cmd_bundle[0]), sizeof(cmd_bundle[0]));
KEYPAD_Init(&command_buffer);
KEYPAD_SetCommand(KEYPAD_KEY_1, CMD_LED_FactoryInit(&g_cmd_factory_led1,GPIO_LED1));
KEYPAD_SetCommand(KEYPAD_KEY_2, CMD_LED_FactoryInit(&g_cmd_factory_led2,GPIO_LED2));
KEYPAD_KeyPress(KEYPAD_KEY_1);
KEYPAD_KeyPress(KEYPAD_KEY_2);
KEYPAD_KeyPress(KEYPAD_KEY_2);
KEYPAD_KeyPress(KEYPAD_KEY_1);
while (!EXECUTOR_Execute(EXECUTOR_Dequeue(&command_buffer)));
return 0;
}
Output:
Time Stamp: 29945 - Setting pin LED1_PIN HIGH
Time Stamp: 21483 - Setting pin LED2_PIN HIGH
Time Stamp: 8456 - Setting pin LED2_PIN LOW
Time Stamp: 3337 - Setting pin LED1_PIN LOW
Epilogue
The final architecture operates correctly and demonstrates a clean separation of responsibilities between invoker, factory, command, queue, and executor, while remaining in full compliance with the GoF Command Pattern.
However, despite the correctness of this implementation, I would generally discourage the use of malloc in embedded systems. Dynamic allocation introduces fragmentation risks, unpredictable latency, and potential failures that are unacceptable in long-running or real-time environments. A more robust alternative to malloc is the object-pool pattern, where command objects are pre-allocated and reused.
The architectural skeleton can easily incorporate an object-pool, preserving all its structural advantages while completely eliminating heap usage.