Compare commits
7 Commits
0c49a11a7b
...
7492df0d1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7492df0d1b | ||
|
|
04aaf93071 | ||
|
|
f73b377911 | ||
|
|
5efd42326b | ||
|
|
1c10c10ca7 | ||
|
|
9b23f9bded | ||
|
|
16faa5aacb |
131
README.md
131
README.md
@@ -1,6 +1,135 @@
|
||||
# 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 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.
|
||||
- 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 determin you need 4 states for your phone these are:
|
||||
- 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 managment said so. And so between the idea of getting fired from throwing a brick at the managment team and implementing this state diagram you decide you need this job and to implement state pattern.
|
||||
this pattern allows for greater flexablity when creating new states since you dont have to add to an ever growning state machine (lets do some simple math if we assume that you end up with 100 states in the end and each state takes up 20 odd line 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 togeather. we can see we have the device itself and which is composed of a device state that has the pointer to the device that it is in
|
||||
|
||||
```mermaid
|
||||
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 create a struct for the device well call this [Device_t](/src/device.h#L67) this device holds its current state ([DeviceState_s](/src/device.h#L52)) and the string that was entered. The [DeviceState_s](/src/device.h#L52) actually holds all the [functions](/src/device.h#L34) or [methods](/src/device.h#L34) that this device uses, with the pressPwrButton, pressStrInputButton, and pressLockButton methods just being alias to these methods as can be seen in [device.c](/src/device.c). these method are defined within the [DeviceInterface_t](/src/device.h#L34) struct which really just acts as a [vtable](https://en.wikipedia.org/wiki/Virtual_method_table) for the given inputs (power button, lock button, string enter button) that will effect the device.
|
||||
This whole interface allows for the underlying state to change with out having to use differnt function calls depending on the current state.
|
||||
|
||||
now to actually create the meat of this device the logic that changes the state, to do this we will first create a [header file](/src/states/state.h) to store all the functions that change the state of the device. now we will implement and state so we will start [off state](/src/states/off_state.c) with the easest 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](/src/device.h#L34) 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](https://morizbuesing.com/blog/greppability-code-metric/) 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.
|
||||
|
||||
```c
|
||||
|
||||
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](/src/states/state.h) 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`](/src/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 managment 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.
|
||||
|
||||
# how to build and run it
|
||||
|
||||
|
||||
12
nob.c
12
nob.c
@@ -28,12 +28,12 @@
|
||||
#define CC "gcc"
|
||||
|
||||
#define SRCS \
|
||||
SRC_FOLDER"main.c", \
|
||||
SRC_FOLDER"device.c", \
|
||||
SRC_FOLDER"states/lock_state.c", \
|
||||
SRC_FOLDER"states/unlock_state.c", \
|
||||
SRC_FOLDER"states/debug_state.c", \
|
||||
SRC_FOLDER"states/off_state.c", \
|
||||
SRC_FOLDER "main.c", \
|
||||
SRC_FOLDER "device.c", \
|
||||
SRC_FOLDER "states/lock_state.c", \
|
||||
SRC_FOLDER "states/unlock_state.c", \
|
||||
SRC_FOLDER "states/debug_state.c", \
|
||||
SRC_FOLDER "states/off_state.c", \
|
||||
|
||||
#define C_ARGS \
|
||||
"-Wall", \
|
||||
|
||||
20
src/device.c
20
src/device.c
@@ -1,15 +1,21 @@
|
||||
#include "device.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);
|
||||
}
|
||||
|
||||
static void pressStrInputMethod(Device_t *device) {
|
||||
void pressStrInputButton(Device_t *device) {
|
||||
device->state.methods.pressStrInput(device);
|
||||
}
|
||||
|
||||
static void pressLockMethod(Device_t *device) {
|
||||
void pressLockButton(Device_t *device) {
|
||||
device->state.methods.pressLock(device);
|
||||
}
|
||||
|
||||
@@ -17,12 +23,6 @@ void initDevice(Device_t* device) {
|
||||
*device = (Device_t){
|
||||
.state = NULL,
|
||||
.entered_string = "frogs",
|
||||
.entered_string_len = 6,
|
||||
.methods = (DeviceInterface_t){
|
||||
.pressPwr = &pressPwrMethod,
|
||||
.pressStrInput = &pressStrInputMethod,
|
||||
.pressLock = &pressLockMethod,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
61
src/device.h
61
src/device.h
@@ -14,7 +14,7 @@
|
||||
// │ │lock ├─────┴─────►│unlock │
|
||||
// │ │state │ │state │
|
||||
// │ └──────┬─┘◄───┬───────┴──┬─────┘
|
||||
// │ ^ │ lck │
|
||||
// │ ▲ │ lck │
|
||||
// │ lck ┤ ├ str=dbg ├ str=dbg
|
||||
// │ │ │ │
|
||||
// │ │ │ │
|
||||
@@ -23,8 +23,11 @@
|
||||
// └───┤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;
|
||||
|
||||
// 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
|
||||
// we have to pass in the device to change
|
||||
// the state
|
||||
void (*pressPwr)(Device_t*);
|
||||
void (*pressStrInput)(Device_t*);
|
||||
void (*pressLock)(Device_t*);
|
||||
}DeviceInterface_t;
|
||||
|
||||
// the states themselves
|
||||
// the function called when the power button is pressed
|
||||
void (*pressPwr)(Device_t*);
|
||||
|
||||
// the function called when we want to enter the string
|
||||
void (*pressStrInput)(Device_t*);
|
||||
|
||||
// the function called when the lock button is pressed
|
||||
void (*pressLock)(Device_t*);
|
||||
} DeviceInterface_t;
|
||||
|
||||
// the states themselves
|
||||
typedef struct DeviceState_s{
|
||||
|
||||
// the name of the current state the device is in
|
||||
// i mainly use this for debugging
|
||||
const char* state_name;
|
||||
Device_t* device;
|
||||
|
||||
// the methods that this struct has (also called a vtable)
|
||||
DeviceInterface_t methods;
|
||||
}DeviceState_t;
|
||||
|
||||
} DeviceState_t;
|
||||
|
||||
// the device
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
11
src/main.c
11
src/main.c
@@ -10,25 +10,26 @@ int main() {
|
||||
if (strcmp(device.state.state_name, "off_state.c") != 0) return 0;
|
||||
|
||||
// turn on the device
|
||||
device.methods.pressPwr(&device);
|
||||
pressPwrButton(&device);
|
||||
|
||||
if (strcmp(device.state.state_name, "lock_state.c") != 0) return 0;
|
||||
|
||||
// send the current string
|
||||
device.methods.pressStrInput(&device);
|
||||
pressStrInputButton(&device);
|
||||
if (strcmp(device.state.state_name, "lock_state.c") != 0) return 0;
|
||||
|
||||
// send in the right string
|
||||
device.entered_string = "pwd";
|
||||
device.methods.pressStrInput(&device);
|
||||
pressStrInputButton(&device);
|
||||
if (strcmp(device.state.state_name, "unlock_state.c") != 0) return 0;
|
||||
|
||||
// try to enter debug mode
|
||||
device.entered_string = "dbg";
|
||||
device.methods.pressStrInput(&device);
|
||||
pressStrInputButton(&device);
|
||||
if (strcmp(device.state.state_name, "debug_state.c") != 0) return 0;
|
||||
|
||||
// try to power down
|
||||
device.methods.pressPwr(&device);
|
||||
pressPwrButton(&device);
|
||||
if (strcmp(device.state.state_name, "off_state.c") != 0) return 0;
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "state.h"
|
||||
#include "../device.h"
|
||||
|
||||
// this is me being lazy
|
||||
const char debug_state_name[] = __FILE_NAME__;
|
||||
|
||||
static void pressPwrMethod(Device_t *device) {
|
||||
@@ -23,7 +24,6 @@ static void pressLockMethod(Device_t *device) {
|
||||
void setDeviceStateToDebug(Device_t *device) {
|
||||
device->state = (DeviceState_t){
|
||||
.state_name = debug_state_name,
|
||||
.device = device,
|
||||
.methods = (DeviceInterface_t){
|
||||
.pressPwr = &pressPwrMethod,
|
||||
.pressStrInput = &pressStrInputMethod,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "state.h"
|
||||
#include "../device.h"
|
||||
|
||||
// this is me being lazy
|
||||
const char lock_state_name[] = __FILE_NAME__;
|
||||
|
||||
static void pressPwrMethod(Device_t *device) {
|
||||
@@ -35,7 +36,6 @@ static void pressLockMethod(Device_t *device) {
|
||||
void setDeviceStateToLock(Device_t *device) {
|
||||
device->state = (DeviceState_t){
|
||||
.state_name = lock_state_name,
|
||||
.device = device,
|
||||
.methods = (DeviceInterface_t){
|
||||
.pressPwr = &pressPwrMethod,
|
||||
.pressStrInput = &pressStrInputMethod,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "../device.h"
|
||||
#include <stdio.h>
|
||||
|
||||
// this is me being lazy
|
||||
const char off_state_name[] = __FILE_NAME__;
|
||||
|
||||
static void pressPwrMethod(Device_t *device) {
|
||||
@@ -10,11 +11,13 @@ static void pressPwrMethod(Device_t *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");
|
||||
}
|
||||
@@ -22,7 +25,6 @@ static void pressLockMethod(Device_t *device) {
|
||||
void setDeviceStateToOff(Device_t *device) {
|
||||
device->state = (DeviceState_t){
|
||||
.state_name = off_state_name,
|
||||
.device = device,
|
||||
.methods = (DeviceInterface_t){
|
||||
.pressPwr = &pressPwrMethod,
|
||||
.pressStrInput = &pressStrInputMethod,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
#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 setDeviceStateToUnlock(Device_t *device);
|
||||
void setDeviceStateToDebug(Device_t *device);
|
||||
|
||||
@@ -13,6 +13,8 @@ static void pressPwrMethod(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) {
|
||||
printf("entering debug state\n");
|
||||
setDeviceStateToDebug(device);
|
||||
@@ -27,9 +29,8 @@ static void pressLockMethod(Device_t *device) {
|
||||
}
|
||||
|
||||
void setDeviceStateToUnlock(Device_t *device) {
|
||||
device->state = (DeviceState_t){
|
||||
device->state = (DeviceState_t) {
|
||||
.state_name = name,
|
||||
.device = device,
|
||||
.methods = (DeviceInterface_t){
|
||||
.pressPwr = &pressPwrMethod,
|
||||
.pressStrInput = &pressStrInputMethod,
|
||||
|
||||
Reference in New Issue
Block a user