Compare commits

...

16 Commits

Author SHA1 Message Date
sirlilpanda
81fbaf02ac Merge branch 'main' into making-readme-more-tutorial-like 2025-09-07 16:08:57 +12:00
sirlilpanda
589c4b645b this should now fix #2 some sentances have been reworded so the links to the code make more sense 2025-09-07 16:08:16 +12:00
sirlilpanda
f5711600cb fixed capitalization issue#3, added more rational for why the user would want to implement the state pattern issue#4, tweaked wording and changed mermaid diagram to ASCII art to be consistant 2025-09-07 15:50:33 +12:00
d7132c6620 Merge pull request 'making-readme-more-tutorial-like' (#1) from making-readme-more-tutorial-like into main
Reviewed-on: #1
2025-09-02 00:51:18 -07:00
sirlilpanda
a5846fa659 implemented the tutorial 2025-09-02 19:49:48 +12:00
sirlilpanda
98d39992a0 start of a new tutorial 2025-09-02 16:08:16 +12:00
sirlilpanda
baf20d5cc3 added more places to look at state pattern 2025-09-02 13:19:54 +12:00
sirlilpanda
1104b011e8 added an eq and fixed some spelling mistakes 2025-09-02 13:16:52 +12:00
sirlilpanda
21800cc2bc fixed some formatting 2025-09-01 23:30:20 +12:00
sirlilpanda
7492df0d1b added some comments and removed the pointer back to device since it was unused 2025-09-01 23:23:37 +12:00
sirlilpanda
04aaf93071 added comments 2025-09-01 23:21:57 +12:00
sirlilpanda
f73b377911 added some comments and removed the static keyword from the now exposed functions 2025-09-01 23:21:39 +12:00
sirlilpanda
5efd42326b exposed input functions rather then hiding them in a method struct and added more comments 2025-09-01 23:20:56 +12:00
sirlilpanda
1c10c10ca7 updated device input functions to be more c like 2025-09-01 23:20:13 +12:00
sirlilpanda
9b23f9bded tweaked some spaceing 2025-09-01 23:19:43 +12:00
sirlilpanda
16faa5aacb added some context of why you want to use the state pattern and how its implemented 2025-09-01 23:18:58 +12:00
10 changed files with 325 additions and 46 deletions

251
README.md
View File

@@ -1,12 +1,255 @@
# c_state_pattern # c_state_pattern
a simple example of how to implement the state pattern in c. check [`main.c`](src/main.c) for the implementation A simple example of how to implement the state pattern in c. Check [`main.c`](src/main.c) for the implementation.
The state pattern is a pattern allows for greater flexibility, maintainability as well as reducing decoupling between states over using a state machine using conditional statements. 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.
This
#### 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:
#### state diagram
```
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 diagram. To start you beginning to implement this state diagram with a bog standard conditional state machine pattern as can be seen below.
```c
void changeState(Device_t* device) {
State_e state = device->state;
switch (state) {
case OFF:
if (offButtonPressed()) {
device->state = LOCK;
}
break;
case LOCK:
if (offButtonPressed()){
device->state = OFF:
}
if (strcmp(device->entered_string, "dbg") == 0) {
device->state = DEBUG:
}
if (strcmp(device->entered_string, "pwd") == 0) {
device->state = UNLOCKED:
}
break;
...
```
However like a good engineer you think back to what your tech lead said this system is going to have to scale to over 100 state. Currently you have implemented 2 states and the function is already over 20 lines of code (LOC) long. Assuming that as you add more states each state grows a little in lines of code and ends up somewhere with 20 LOC per state. We can do some simple math to see how out of hand this can get:
$$
100_{\text{states}} \times 20_{\text{LOC}} = 2000_{\text{LOC}}
$$
That would be a 2k LOC state machine and a nightmare to debug and maintain (i should know ive seen them in the wild, and let me tell you some LSP dont like it).
Therefor due to this mess of unmaintainable and heavily coupled code, you decide you need a different solution, and there for you decide to implement the namesake of this article **The state pattern**. Using this pattern all your states become singular easily maintainable [c files](/src/states/) as well as decoupling some states from the dependencies that other states need.
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. This means that the state field will change its self when one of the transition functions is called. These transition functions are the ones defined within the interface and the ones defined within the `Device_t` struct with the ones in the `Device_t` struct just calling the functions that are implemented by the `DeviceState_t`. To give and example when the devices `pressPwrButton` method is called this calls the `DeviceState_t` `pressPwrMethod`.
#### uml diagram
```
---------------------------------- UML Diagram ----------------------------------
┌──────────────────────────────────────────────┐ ┌────────────────────────┐
│ Device_t │<>────┤ «interface» │
├──────────────────────────────────────────────┤ │ DeviceInterface_t │
│ +DeviceState_t state │ ├────────────────────────┤
│ +String entered_string │ │ │
├──────────────────────────────────────────────┤ ├────────────────────────┤
│ +pressPwrButton() : pressPwrMethod │ │ +pressPwrMethod() │
│ +pressStrInputButton() : pressStrInputMethod │ │ +pressStrInputMethod() │
│ +pressLockButton() : pressLockMethod │ │ +pressLockMethod() │
└──────────────────────────────────────────────┘ └─────┬──────────────────┘
▲ │
▼ │
│ │
│ │
┌────────────┴───────────────┐ │
│ DeviceState_t │<>────────────────────────────┘
├────────────────────────────┤
│ +String state_name │
│ +DeviceInterface_s methods │
├────────────────────────────┤
│ +setDeviceStateToStateX() │
└────────────────────────────┘
--------------------------------- \ UML Diagram ---------------------------------
```
(note not real uml diagram because no one knows how to read them)
1. So to implement this we will first start off with the [device struct](/src/device.h#L64)
```c
// device.h
typedef struct Device_s{
DeviceState_t state,
char* entered_string,
} Device_t;
```
2. Next we will implement the [device interface](/src/device.h#L34) which as the name implies is an [interface](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming)) bascially a struct that holds methods that creates a common set of functions that both structs need to implement. This means the [device interface](/src/device.h#L34) will defined 3 methods all relating to these transition functions which are all the changes that can be made to the device i.e. Your [inputs](#inputs).
```c
// device.h
typedef struct DeviceInterface_s{
void (*pressPwr)(Device_t*);
void (*pressStrInput)(Device_t*);
void (*pressLock)(Device_t*);
} DeviceInterface_t;
```
3. And finally we will implement the [struct to hold the actual state](/src/device.h#L52) of the device. (note there is some funkiness in c with implementing this since you will need forward declaration)
```c
// device.h
typedef struct DeviceState_s{
const char* state_name;
DeviceInterface_t methods;
}DeviceState_t;
```
4. Now we need to implement the 3 transition functions for device inputs, ive implemented one here as the rest are very [similar](/src/device.c#L14). So this function takes the given device and then uses the methods defined within the device state to change the current state of the device as defined within the [uml diagram](#uml-diagram).
```c
// device.c
void pressPwrButton(Device_t *device) {
device->state.methods.pressPwr(device);
}
```
5. Now we will create a new header file called state, this file will contain the functions that sets the state of the given device to a new state. So we will implement a function per out defined [states](#states).
```c
//state.h
void setDeviceStateToUnlock(Device_t *device);
void setDeviceStateToDebug(Device_t *device);
void setDeviceStateToLock(Device_t *device);
void setDeviceStateToOff(Device_t *device);
```
6. Now we can implement one of these states as the rest of them should be fairly self explanatory. The state we will be implementing is the [lock state](/src/states/lock_state.c) as defined within the [state diagram](#state-diagram) since this should show off most of the different transitions. We will first start out with implementing the [set state function](/src/states/lock_state.c#L36). This function takes the given device and sets that devices state to this new state.
```c
// lock_state.h
void setDeviceStateToLock(Device_t *device) {
device->state = (DeviceState_t){
.state_name = lock_state_name,
.methods = (DeviceInterface_t){
.pressPwr = &pressPwrMethod,
.pressStrInput = &pressStrInputMethod,
.pressLock = &pressLockMethod,
},
};
}
```
7. Now that we have the function for setting this state we can now implement the functions defined within the [device interface](/src/device.h#L34). To start we will implement the simplest transition function, the [function](/src/states/lock_state.c#L10) called when the power button is press i.e. When the function is called we set the devices state to off.
```c
// lock_state.c
static void pressPwrMethod(Device_t *device) {
printf("turning off device\n");
setDeviceStateToOff(device);
}
```
8. Now we will implement the next transition function, this time the [function](/src/states/lock_state.c#L31) is called when the lock button is pressed, which in the lock state will do nothing.
```c
// lock_state.c
static void pressLockMethod(Device_t *device) {
(void) device;
printf("nothing happens\n");
}
```
9. Finally we create the transition function for handling the text input, this [function](/src/states/lock_state.c#L15) will check the entered string to see if its either "dbg", or "pwd" and if it is enter debug or unlock respectively.
```c
static void pressStrInputMethod(Device_t *device) {
if (strcmp(device->entered_string, "dbg") == 0) {
printf("entering debug state\n");
setDeviceStateToDebug(device);
return;
}
if (strcmp(device->entered_string, "pwd") == 0) {
printf("entering unlock state\n");
setDeviceStateToUnlock(device);
return;
}
printf("unknown string %s\n", device->entered_string);
}
```
10. Now we just need to implement the other [states](#states) in the same way we implemented this first one.
11. Finally we need to test our device. Firstly within [main](/src/main.c) we create a new device and set its initial state to off.
```c
// arrange:
Device_t device;
initDevice(&device);
setDeviceStateToOff(&device);
```
12. Now we can test that the device correctly travels through each [state](#state-diagram), by pressing one of the buttons ([invoking the input method functions](/src/device.h#L34)) then checking that the `state_name` is equal to the set state name.
```c
// act :
// turn on the device
pressPwrButton(&device);
// assert :
// check that the device entered the right state
if (strcmp(device.state.state_name, "lock_state.c") != 0) return 0;
```
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 state yourself.
Other good resources for learning how this patterns works is [bob nystrom's game programming patterns book](https://gameprogrammingpatterns.com/state.html) as well as [refactoring Guru](https://refactoring.guru/design-patterns/state) however both of these implement this in the fun languages with object and interfaces.
# how to build and run it # how to build and run it
this demo uses a simple build system called [nob](https://github.com/tsoding/nob.h) a header only build system for c projects. This demo uses a simple build system called [nob](https://github.com/tsoding/nob.h) a header only build system for c projects.
to build run these command in the root of the project: To build run these command in the root of the project:
```bash ```bash
# bootstraps the build system # bootstraps the build system
@@ -19,4 +262,4 @@ to build run these command in the root of the project:
> ./build/main > ./build/main
``` ```
and if you have a differnt complier you want to use that is posix compliant just change the `CC` macro in the `nob.c` with your one. 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.

12
nob.c
View File

@@ -28,12 +28,12 @@
#define CC "gcc" #define CC "gcc"
#define SRCS \ #define SRCS \
SRC_FOLDER"main.c", \ SRC_FOLDER "main.c", \
SRC_FOLDER"device.c", \ SRC_FOLDER "device.c", \
SRC_FOLDER"states/lock_state.c", \ SRC_FOLDER "states/lock_state.c", \
SRC_FOLDER"states/unlock_state.c", \ SRC_FOLDER "states/unlock_state.c", \
SRC_FOLDER"states/debug_state.c", \ SRC_FOLDER "states/debug_state.c", \
SRC_FOLDER"states/off_state.c", \ SRC_FOLDER "states/off_state.c", \
#define C_ARGS \ #define C_ARGS \
"-Wall", \ "-Wall", \

View File

@@ -1,15 +1,21 @@
#include "device.h"
#include <string.h> #include <string.h>
static void pressPwrMethod(Device_t *device) { #include "device.h"
// these functions are where the bulk of the ugliness happens
// these function just pass through and call the method that
// is defined within the state structs methods
void pressPwrButton(Device_t *device) {
device->state.methods.pressPwr(device); device->state.methods.pressPwr(device);
} }
static void pressStrInputMethod(Device_t *device) { void pressStrInputButton(Device_t *device) {
device->state.methods.pressStrInput(device); device->state.methods.pressStrInput(device);
} }
static void pressLockMethod(Device_t *device) { void pressLockButton(Device_t *device) {
device->state.methods.pressLock(device); device->state.methods.pressLock(device);
} }
@@ -17,12 +23,6 @@ void initDevice(Device_t* device) {
*device = (Device_t){ *device = (Device_t){
.state = NULL, .state = NULL,
.entered_string = "frogs", .entered_string = "frogs",
.entered_string_len = 6,
.methods = (DeviceInterface_t){
.pressPwr = &pressPwrMethod,
.pressStrInput = &pressStrInputMethod,
.pressLock = &pressLockMethod,
},
}; };
} }

View File

@@ -14,7 +14,7 @@
// │ │lock ├─────┴─────►│unlock │ // │ │lock ├─────┴─────►│unlock │
// │ │state │ │state │ // │ │state │ │state │
// │ └──────┬─┘◄───┬───────┴──┬─────┘ // │ └──────┬─┘◄───┬───────┴──┬─────┘
// │ ^ │ lck │ // │ │ lck │
// │ lck ┤ ├ str=dbg ├ str=dbg // │ lck ┤ ├ str=dbg ├ str=dbg
// │ │ │ │ // │ │ │ │
// │ │ │ │ // │ │ │ │
@@ -23,8 +23,11 @@
// └───┤debug │◄──────────────┘ // └───┤debug │◄──────────────┘
// │ │ // │ │
// └────────┘ // └────────┘
// the state pattern was primarly made in languages that had access to such great things
// like object and interfaces but in c we must implement these ourselves
// forward decl
// forward decl for Device struct
typedef struct Device_s Device_t; typedef struct Device_s Device_t;
// the interface that you would use to interface with the state // the interface that you would use to interface with the state
@@ -34,27 +37,55 @@ typedef struct DeviceInterface_s{
// but as we dont have a protected keyword // but as we dont have a protected keyword
// we have to pass in the device to change // we have to pass in the device to change
// the state // the state
// the function called when the power button is pressed
void (*pressPwr)(Device_t*); void (*pressPwr)(Device_t*);
// the function called when we want to enter the string
void (*pressStrInput)(Device_t*); void (*pressStrInput)(Device_t*);
// the function called when the lock button is pressed
void (*pressLock)(Device_t*); void (*pressLock)(Device_t*);
}DeviceInterface_t; } DeviceInterface_t;
// the states themselves // the states themselves
typedef struct DeviceState_s{ typedef struct DeviceState_s{
// the name of the current state the device is in
// i mainly use this for debugging
const char* state_name; const char* state_name;
Device_t* device;
// the methods that this struct has (also called a vtable)
DeviceInterface_t methods; DeviceInterface_t methods;
}DeviceState_t;
} DeviceState_t;
// the device // the device
struct Device_s{ struct Device_s{
// in theroy this is also the flyweight pattern
DeviceState_t state;
char* entered_string;
int entered_string_len;
DeviceInterface_t methods; // the current state of the device
DeviceState_t state;
// the string that the user has inputed null terminated
char* entered_string;
}; };
// creates the device /// @brief creates a new device at the given pointer
/// @arg device: a pointer to where the struct is created
void initDevice(Device_t* device); void initDevice(Device_t* device);
// this is your interface between the state
/// @brief presses the Pwr button on the device
/// @arg device: the device that has it button pressed
void pressPwrButton(Device_t *device);
/// @brief presses the StrInput button on the device
/// @arg device: the device that has it button pressed
void pressStrInputButton(Device_t *device);
/// @brief presses the Lock button on the device
/// @arg device: the device that has it button pressed
void pressLockButton(Device_t *device);

View File

@@ -10,25 +10,26 @@ int main() {
if (strcmp(device.state.state_name, "off_state.c") != 0) return 0; if (strcmp(device.state.state_name, "off_state.c") != 0) return 0;
// turn on the device // turn on the device
device.methods.pressPwr(&device); pressPwrButton(&device);
if (strcmp(device.state.state_name, "lock_state.c") != 0) return 0; if (strcmp(device.state.state_name, "lock_state.c") != 0) return 0;
// send the current string // send the current string
device.methods.pressStrInput(&device); pressStrInputButton(&device);
if (strcmp(device.state.state_name, "lock_state.c") != 0) return 0; if (strcmp(device.state.state_name, "lock_state.c") != 0) return 0;
// send in the right string // send in the right string
device.entered_string = "pwd"; device.entered_string = "pwd";
device.methods.pressStrInput(&device); pressStrInputButton(&device);
if (strcmp(device.state.state_name, "unlock_state.c") != 0) return 0; if (strcmp(device.state.state_name, "unlock_state.c") != 0) return 0;
// try to enter debug mode // try to enter debug mode
device.entered_string = "dbg"; device.entered_string = "dbg";
device.methods.pressStrInput(&device); pressStrInputButton(&device);
if (strcmp(device.state.state_name, "debug_state.c") != 0) return 0; if (strcmp(device.state.state_name, "debug_state.c") != 0) return 0;
// try to power down // try to power down
device.methods.pressPwr(&device); pressPwrButton(&device);
if (strcmp(device.state.state_name, "off_state.c") != 0) return 0; if (strcmp(device.state.state_name, "off_state.c") != 0) return 0;
return 0; return 0;

View File

@@ -3,6 +3,7 @@
#include "state.h" #include "state.h"
#include "../device.h" #include "../device.h"
// this is me being lazy
const char debug_state_name[] = __FILE_NAME__; const char debug_state_name[] = __FILE_NAME__;
static void pressPwrMethod(Device_t *device) { static void pressPwrMethod(Device_t *device) {
@@ -23,7 +24,6 @@ static void pressLockMethod(Device_t *device) {
void setDeviceStateToDebug(Device_t *device) { void setDeviceStateToDebug(Device_t *device) {
device->state = (DeviceState_t){ device->state = (DeviceState_t){
.state_name = debug_state_name, .state_name = debug_state_name,
.device = device,
.methods = (DeviceInterface_t){ .methods = (DeviceInterface_t){
.pressPwr = &pressPwrMethod, .pressPwr = &pressPwrMethod,
.pressStrInput = &pressStrInputMethod, .pressStrInput = &pressStrInputMethod,

View File

@@ -4,6 +4,7 @@
#include "state.h" #include "state.h"
#include "../device.h" #include "../device.h"
// this is me being lazy
const char lock_state_name[] = __FILE_NAME__; const char lock_state_name[] = __FILE_NAME__;
static void pressPwrMethod(Device_t *device) { static void pressPwrMethod(Device_t *device) {
@@ -35,7 +36,6 @@ static void pressLockMethod(Device_t *device) {
void setDeviceStateToLock(Device_t *device) { void setDeviceStateToLock(Device_t *device) {
device->state = (DeviceState_t){ device->state = (DeviceState_t){
.state_name = lock_state_name, .state_name = lock_state_name,
.device = device,
.methods = (DeviceInterface_t){ .methods = (DeviceInterface_t){
.pressPwr = &pressPwrMethod, .pressPwr = &pressPwrMethod,
.pressStrInput = &pressStrInputMethod, .pressStrInput = &pressStrInputMethod,

View File

@@ -2,6 +2,7 @@
#include "../device.h" #include "../device.h"
#include <stdio.h> #include <stdio.h>
// this is me being lazy
const char off_state_name[] = __FILE_NAME__; const char off_state_name[] = __FILE_NAME__;
static void pressPwrMethod(Device_t *device) { static void pressPwrMethod(Device_t *device) {
@@ -10,11 +11,13 @@ static void pressPwrMethod(Device_t *device) {
} }
static void pressStrInputMethod(Device_t *device) { static void pressStrInputMethod(Device_t *device) {
// cast it to void since its unused
(void) device; (void) device;
printf("nothing happens\n"); printf("nothing happens\n");
} }
static void pressLockMethod(Device_t *device) { static void pressLockMethod(Device_t *device) {
// cast it to void since its unused
(void) device; (void) device;
printf("nothing happens\n"); printf("nothing happens\n");
} }
@@ -22,7 +25,6 @@ static void pressLockMethod(Device_t *device) {
void setDeviceStateToOff(Device_t *device) { void setDeviceStateToOff(Device_t *device) {
device->state = (DeviceState_t){ device->state = (DeviceState_t){
.state_name = off_state_name, .state_name = off_state_name,
.device = device,
.methods = (DeviceInterface_t){ .methods = (DeviceInterface_t){
.pressPwr = &pressPwrMethod, .pressPwr = &pressPwrMethod,
.pressStrInput = &pressStrInputMethod, .pressStrInput = &pressStrInputMethod,

View File

@@ -1,7 +1,8 @@
#pragma once #pragma once
#include "../device.h" #include "../device.h"
// each of these are each state // each of these are the functions that prove
// the given state transition
void setDeviceStateToLock(Device_t *device); void setDeviceStateToLock(Device_t *device);
void setDeviceStateToUnlock(Device_t *device); void setDeviceStateToUnlock(Device_t *device);
void setDeviceStateToDebug(Device_t *device); void setDeviceStateToDebug(Device_t *device);

View File

@@ -13,6 +13,8 @@ static void pressPwrMethod(Device_t *device) {
} }
static void pressStrInputMethod(Device_t *device) { static void pressStrInputMethod(Device_t *device) {
// check to see if the entered string is the one to enter the debug state
if (strcmp(device->entered_string, "dbg") == 0) { if (strcmp(device->entered_string, "dbg") == 0) {
printf("entering debug state\n"); printf("entering debug state\n");
setDeviceStateToDebug(device); setDeviceStateToDebug(device);
@@ -27,9 +29,8 @@ static void pressLockMethod(Device_t *device) {
} }
void setDeviceStateToUnlock(Device_t *device) { void setDeviceStateToUnlock(Device_t *device) {
device->state = (DeviceState_t){ device->state = (DeviceState_t) {
.state_name = name, .state_name = name,
.device = device,
.methods = (DeviceInterface_t){ .methods = (DeviceInterface_t){
.pressPwr = &pressPwrMethod, .pressPwr = &pressPwrMethod,
.pressStrInput = &pressStrInputMethod, .pressStrInput = &pressStrInputMethod,