c_state_pattern
A simple example of how to implement the state pattern in c. check main.c for the implementation.
the state pattern is a pattern that aims to get rid of the mess that a state machine causes within your code. So i propose a problem for you, you are trying to write an state machine for a phone, sounds easy your phone only has 3 inputs that change the current state of the phone.
inputs:
- power button
- lock button
- string enter button
the power button does what it says either turns the phone off or on, the lock button too does what it says on the tin locks the phone, lastly the string enter button enters what every string is in the phones buffer this is used for entering things like passwords to unlock the phone.
using these buttons you determine you need 4 states for your phone these are:
states:
- off
- unlock
- locked
- debug
and you want the inter-action between these states to work as follows:
pwr : power button pressed
lck : lock button pressed
str=x : string enter button press with string x
------------ state diagram ------------
pwr ┌──────────┐ pwr
┌─────┴┬────►│off state │◄────┴┐
│ │ └────┬─────┘ │
│ │ │ │
│ │ ┌─────┘ │
│ │ ├ pwr │
│ │ ▼ │
│ ┌──┴─────┐ str=pwd ┌────┴───┐
│ │lock ├─────┴─────►│unlock │
│ │state │ │state │
│ └──────┬─┘◄───┬───────┴──┬─────┘
│ ▲ │ lck │
│ lck ┤ ├ str=dbg ├ str=dbg
│ │ │ │
│ │ │ │
│ │ ▼ │
│ ┌─┴──────┐ │
└───┤debug │◄──────────────┘
│ │
└────────┘
----------- \ state diagram -----------
now you may read these requirements for the device and think ill just implement this with a state machine how bad can it be. Well your tech lead tells you that this systems needs to scaled to over 100 different states as they make the system more complex. you ask him "why do we need this" they reply back with management said so. And so between the idea of getting fired from throwing a brick at the management team and implementing this state diagram you decide you need this job and to implement state pattern. this pattern allows for greater flexibility when creating new states, since you dont have to add to an ever gowning state machine. we do some simple math to see how out of hand this can get, if we assume that you end up with 100 states in the end and each state takes up 20 odd line
100_{\text{states}} \times 20_{\text{loc}} = 2000_{\text{loc}}
that would be a 2k line state machine and a nightmare to debug (i should know ive seen them in the wild)
so to start implement this pattern we first draw up a uml diagram of how it should go together. we can see we have the device itself and which is composed of a device state, and the entered string.
This "class" also has 3 methods pressPwrButton, pressStrInputButton, and pressLockButton. these methods are used to create an abstraction from the device state so the underlying state can change while using the same input functions.
classDiagram
class Device_t{
+DeviceState_t state
+String entered_string
+pressPwrButton() pressPwrMethod
+pressStrInputButton() pressStrInputMethod
+pressLockButton() pressLockMethod
}
class DeviceInterface_s {
+pressPwrMethod()
+pressStrInputMethod()
+pressLockMethod()
}
class DeviceState_t{
+String state_name
+DeviceInterface_s methods
}
Device_t *-- DeviceState_t
%% DeviceInterface_s --o Device_t
Device_t o-- DeviceInterface_s
DeviceInterface_s --o DeviceState_t
(note not real uml diagram because no one knows how to read them)
-
so to implement this we will first start off with the device struct
typedef struct Device_s{ DeviceState_t state, char* entered_string, } Device_t; -
Next we will implement the methods which in this case will be an interface. These methods are all the changes that can be made to the device i.e. your inputs.
typedef struct DeviceInterface_s{ void (*pressPwr)(Device_t*); void (*pressStrInput)(Device_t*); void (*pressLock)(Device_t*); } DeviceInterface_t; -
and finally we will implement the struct to hold the actual state of the device. (note there is some funkiness in c with implementing this since you will need forward decls)
typedef struct DeviceState_s{ const char* state_name; DeviceInterface_t methods; }DeviceState_t; -
now we need to implement the 3 functions for device ive implemented one here as the rest are very similar. So this function takes the give device and then uses the methods defined within the
void pressPwrButton(Device_t *device) { device->state.methods.pressPwr(device); }
now to actually create the meat of this device, we need to implement the logic that changes the state. To do this we will first create a header file to store all the functions that change the state of the device. now we will implement and state so we will start off state with the easiest one the off state (no bother having the rest if the device cant turn on). to create this state we will implement the functions that are defined within the interface we created, this functions will define what happens to the current state based on the given input. so pressPwrMethod is what happens when the power button is pressed when its in the off state (to make the code grep-able these functions should probably be prefixed with the given state like OffStatePressPwrMethod). these function are prefix with the static keyword as they should only ever be used here and not exposed. the other methods should be self explanatory. lastly we must implement the function that actually changes the state of the device to this given state, so to change the state we just set the devices state to a new struct with the given methods that we defined within this file.
static void pressPwrMethod(Device_t *device) {
printf("turning on device\n");
setDeviceStateToLock(device);
}
static void pressStrInputMethod(Device_t *device) {
// cast it to void since its unused
(void) device;
printf("nothing happens\n");
}
static void pressLockMethod(Device_t *device) {
// cast it to void since its unused
(void) device;
printf("nothing happens\n");
}
void setDeviceStateToOff(Device_t *device) {
device->state = (DeviceState_t){
.state_name = off_state_name,
.device = device,
.methods = (DeviceInterface_t){
.pressPwr = &pressPwrMethod,
.pressStrInput = &pressStrInputMethod,
.pressLock = &pressLockMethod,
},
};
}
and finally we expose the set method in our header file so the other states can set to this state. next we just implement the rest of the state Lock, Unlock, and Debug and can finally test our creation in main.c.
now that you have implement this basic state pattern your tech lead comes to you and tells you to implement one more state then they will let you throw a brick at management as a treat this state is:
- A calling state where:
- when the phone is unlocked you can type in a number in the the string input and it will call it
- you can also turn off the phone from this state
- the lock button ends the current call and returns back to the unlocked state
so try implementing this yourself.
other good resources for learning how this patterns works is bob nystrom gameprogrammingpatterns as well as refactoring Guru however both of these implement this in the fun languages with object and interfaces.
how to build and run it
this demo uses a simple build system called nob a header only build system for c projects.
to build run these command in the root of the project:
# bootstraps the build system
> gcc nob.c -o nob
# runs the build system
> ./nob
# runs the program
> ./build/main
and if you have a different compiler you want to use that is posix compliant just change the CC macro in the nob.c with your one.