Escaping Boolean Hell: Why Your State Machine Needs to be Data-Driven
We've all been there: a simple boolean flag here, a nested if statement there. It seems harmless at first, but soon your codebase is a tangled mess of conditional logic.
Let's take the humble Door class. It can be opened and closed. Easy, right?
The Simple, Intuitive Code
public class Door
{
public bool IsOpen { get; set; } = false;
public void Open()
{
IsOpen = true;
Console.WriteLine("The door is now open.");
}
public void Close()
{
IsOpen = false;
Console.WriteLine("The door is now closed.");
}
}
The Inevitable Complexity Creep
In the wild, the simple, intuitive code you first wrote quickly starts to smell. The moment we try to add a new feature, such as a lock, our elegant two-state object becomes a mess of conditional logic.
To accommodate the "locked" state, we add a new boolean flag, IsLocked, and then we must modify all existing methods to account for it.
public class Door
{
public bool IsOpen { get; set; } = false;
public bool IsLocked = false; // We added this!
public void Open()
{
if (!IsLocked) // We modified this!
{
IsOpen = true;
Console.WriteLine("The door is now open.");
}
else
{
Console.WriteLine("The door is locked and cannot be opened.");
}
}
public void Close()
{
IsOpen = false;
Console.WriteLine("The door is now closed.");
}
public void Lock() // We added this!
{
IsLocked = true;
Console.WriteLine("The door is now locked.");
}
public void Unlock() // We added this!
{
IsLocked = false;
Console.WriteLine("The door is now unlocked.");
}
}
What about a Broken state? Or a Sliding door? Or a RollUp door? We can approximate these with more booleans, but each new addition requires us to go back and add more and more if/else statements to every method. It’s not scalable, it’s brittle, and it’s full of boilerplate.
| Pros | Cons |
| Simple and quick to implement for basic cases. | Tight Coupling: Logic is tied directly to a single object. |
| No external dependencies or abstractions. | Boilerplate: if/else checks for every state change. |
| Difficult to Scale: Adding new features breaks existing logic. |
| Difficult to Maintain: Code quickly becomes a tangled mess. |
The Typical FSM Solution and Its Pitfalls
A traditional Finite State Machine (FSM) approach uses the State Pattern, where you create distinct classes for each state that implement a common interface. The main Door class holds a reference to its current state object.
Traditional State Pattern Implementation
public interface IState<T>
{
void Enter(T context);
void Update(T context);
void Exit(T context);
}
public class DoorContext
{
public bool IsOpen { get; set; } = false;
}
public class DoorClosedState : IState<DoorContext>
{
public void Enter(DoorContext context)
{
context.IsOpen = false;
Console.WriteLine("The door is now closed.");
}
public void Update(DoorContext context) { }
public void Exit(DoorContext context) { }
}
public class DoorOpenState : IState<DoorContext>
{
public void Enter(DoorContext context)
{
context.IsOpen = true;
Console.WriteLine("The door is now open.");
}
public void Update(DoorContext context) { }
public void Exit(DoorContext context) { }
}
Now, what does it take to add a "locked" state to this? It's not as simple as adding a boolean. We need to create a new state class, DoorLockedState, and then modify our existing states and the context to allow for transitions to this new state. This still requires a fair amount of boilerplate and modification of existing code.
FSM Solution Trade-offs
| Pros | Cons |
| Clean State Separation: Logic for each state is in a distinct class. | Boilerplate: Requires a lot of code to set up states, transitions, and the state machine itself. |
| Enforced Transitions: The model naturally enforces valid transitions. | Rigid Design: FSM definition is static and difficult to modify at runtime. |
| Well-Established Pattern: A common and well-understood design pattern. | State Explosion: Can lead to a large number of classes for each state, making it difficult to manage. |
A Better Way: FSM_API and Centralized State
The FSM_API embraces a declarative, data-driven approach that is designed to be blazing-fast, software-agnostic, and fully decoupled from your application's logic.
The core concept is that the FSM_API operates on Plain Old C# Objects (POCOs) that simply implement the IStateContext interface. This ensures a clean separation between your FSM logic and your application's data.
1. Define the Context
Our Door class is now a simple POCO. All its transition and state-entry logic is handled externally by the FSMs we are about to create.
public class Door : IStateContext
{
public bool IsOpen { get; set; } = false;
public bool IsLocked = false;
public string Name { get; set; } = "FrontDoor";
public bool IsValid => true; // FSM API will not operate on this if false.
}
2. Multiple FSMs on a Single Context
The true power of FSM_API is its ability to manage multiple, independent behaviors on a single context object. This allows us to separate our "open/close" logic from our "lock/unlock" logic.
Here is the declarative definition for the DoorFSM:
FSM_API.Create.CreateFiniteStateMachine("DoorFSM")
.State("Closed", onEnter: (ctx) => { ((Door)ctx).IsOpen = false; })
.State("Open", onEnter: (ctx) => { ((Door)ctx).IsOpen = true; })
.State("Locked")
.WithInitialState("Closed")
// Transition to Open only if not locked
.Transition("Closed", "Open", (ctx) => !((Door)ctx).IsLocked)
.Transition("Open", "Closed")
// Transition to Locked from Closed
.Transition("Closed", "Locked")
// Transition from Locked back to Closed only if unlocked
.Transition("Locked", "Closed", (ctx) => !((Door)ctx).IsLocked)
.BuildDefinition();
Because the FSM_API allows for runtime redefinition of FSMs, we can dynamically add states and transitions to our door object without recompiling. This capability makes it a powerful choice for highly configurable or live-updating systems.
FSM_API Trade-offs
| Pros | Cons |
| Centralized State: FSM logic is fully separated from data. | Learning Curve: New concepts might take time to grasp if you are used to traditional FSM implementations. |
| Runtime Redefinition: FSMs can be modified on the fly without recompilation. | Minimalism: The API is minimal by design; you will need to rely on the provided documentation to get started. |
| Lightweight & Performant: Minimal memory allocations and optimized performance. | |
| Framework Agnostic: No external dependencies or frameworks required (works with .NET Framework 4.7 through .NET 8.0). | |
| Thread-Safe: Designed for safe, deferred mutation handling. | |
This is just a glimpse of what's possible with the FSM_API. Its features make it a robust choice for any C# application requiring sophisticated state management.
If you'd like to get started, you can find our NuGet package and all the documentation on our GitHub repository:
If you feel this tool is valuable to you, please consider supporting the project. Donate via PayPal
