DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Understanding the State Design Pattern P5

Introduction

Building state machines from scratch can be rewarding, but using a dedicated library like Stateless can streamline the process and simplify complex state transitions. Stateless is a hierarchical state machine library in C#, developed by Nicholas Blumhardt, that provides an easy-to-use interface for defining states, triggers, and conditions.

In this article, we'll set up a state machine in a scenario inspired by reproductive health, which includes various states a person might experience throughout different stages of life. This example will demonstrate the power of the Stateless library in handling complex state transitions and conditional actions.

Why Use Stateless?

The Stateless library provides a robust and extensible way to manage state machines with:

  • Declarative Syntax: Easily define states, triggers, and transitions.
  • Conditional Transitions: Apply conditions to control state changes.
  • Hierarchical State Management: Organize states and manage complex transitions efficiently.

Step 1: Setting Up the Stateless Library

To use Stateless, start by installing the package via NuGet:

dotnet add package Stateless
Enter fullscreen mode Exit fullscreen mode

or in the NuGet Package Manager Console:

Install-Package Stateless
Enter fullscreen mode Exit fullscreen mode

Once installed, we can begin defining our states and triggers.


Step 2: Define States and Triggers

Let’s define two enums: HealthState for different life stages and Activity for actions or events that trigger state transitions.

using Stateless;

namespace HealthStateMachineExample
{
    // Define the states related to reproductive health
    public enum HealthState
    {
        NonReproductive,
        Reproductive,
        Pregnant
    }

    // Define activities that can trigger state transitions
    public enum Activity
    {
        ReachPuberty,
        HaveUnprotectedSex,
        GiveBirth,
        HaveAbortion,
        UndergoSurgery
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • HealthState Enum:

    • NonReproductive: The individual is not yet capable of reproduction.
    • Reproductive: The individual is capable of reproduction.
    • Pregnant: The individual is currently pregnant.
  • Activity Enum:

    • ReachPuberty: Transition to reproductive state.
    • HaveUnprotectedSex: May lead to pregnancy if conditions are met.
    • GiveBirth: Transition back to reproductive after pregnancy.
    • HaveAbortion: Return to reproductive state from pregnancy.
    • UndergoSurgery: Transition to non-reproductive by removing reproductive capability.

Step 3: Initialize the State Machine

Let’s initialize our state machine with Stateless by defining the initial state and setting up triggers for state transitions.

public class HealthStateMachine
{
    private readonly StateMachine<HealthState, Activity> _stateMachine;
    public bool ParentsNotWatching { get; set; } = true; // Conditional property

    public HealthStateMachine()
    {
        // Initialize the state machine starting in the NonReproductive state
        _stateMachine = new StateMachine<HealthState, Activity>(HealthState.NonReproductive);

        ConfigureStateMachine();
    }

    private void ConfigureStateMachine()
    {
        // Configure NonReproductive state
        _stateMachine.Configure(HealthState.NonReproductive)
            .Permit(Activity.ReachPuberty, HealthState.Reproductive);

        // Configure Reproductive state
        _stateMachine.Configure(HealthState.Reproductive)
            .PermitIf(Activity.HaveUnprotectedSex, HealthState.Pregnant, () => ParentsNotWatching)
            .Permit(Activity.UndergoSurgery, HealthState.NonReproductive);

        // Configure Pregnant state
        _stateMachine.Configure(HealthState.Pregnant)
            .Permit(Activity.GiveBirth, HealthState.Reproductive)
            .Permit(Activity.HaveAbortion, HealthState.Reproductive);
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Initialization:

    • We define a StateMachine object, _stateMachine, with HealthState as the state type and Activity as the trigger type.
    • The initial state is set to NonReproductive.
  2. Conditional Property:

    • ParentsNotWatching simulates a real-life condition for allowing state transitions (e.g., pregnancy only if parents aren’t watching).
  3. ConfigureStateMachine Method:

    • NonReproductive State: The individual can transition to Reproductive state by triggering ReachPuberty.
    • Reproductive State:
      • Triggers a conditional transition to Pregnant state if HaveUnprotectedSex is called and ParentsNotWatching is true.
      • Allows transition to NonReproductive if UndergoSurgery is triggered.
    • Pregnant State: Transition to Reproductive state by either GiveBirth or HaveAbortion.

Step 4: Testing the State Machine

Now let’s simulate some transitions to see how the state machine responds to different activities.

class Program
{
    static void Main(string[] args)
    {
        var healthMachine = new HealthStateMachine();

        Console.WriteLine($"Initial State: {healthMachine.CurrentState}");

        // Transition from NonReproductive to Reproductive
        healthMachine.PerformTransition(Activity.ReachPuberty);
        Console.WriteLine($"After Puberty: {healthMachine.CurrentState}");

        // Attempt to transition to Pregnant (will succeed if ParentsNotWatching is true)
        healthMachine.PerformTransition(Activity.HaveUnprotectedSex);
        Console.WriteLine($"After Unprotected Sex: {healthMachine.CurrentState}");

        // Give birth to return to Reproductive
        healthMachine.PerformTransition(Activity.GiveBirth);
        Console.WriteLine($"After Giving Birth: {healthMachine.CurrentState}");

        // Undergo surgery to become NonReproductive
        healthMachine.PerformTransition(Activity.UndergoSurgery);
        Console.WriteLine($"After Surgery: {healthMachine.CurrentState}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Transitions:
    • ReachPuberty: Moves the state from NonReproductive to Reproductive.
    • HaveUnprotectedSex: Moves the state to Pregnant if ParentsNotWatching is true.
    • GiveBirth: Moves the state back to Reproductive from Pregnant.
    • UndergoSurgery: Moves the state to NonReproductive, making further reproduction impossible.

Step 5: Using Conditions and Actions

Stateless allows for advanced customization of transitions using conditions and actions.

  1. Conditional Transitions:

    • The PermitIf method enables state changes only when specific conditions are met.
    • In our example, HaveUnprotectedSex only transitions to Pregnant if ParentsNotWatching is true.
  2. Actions on Entry and Exit:

    • Stateless allows executing actions upon entering or exiting a state.
    • We could modify the configuration to log when a transition occurs, send notifications, or perform other actions.
private void ConfigureStateMachine()
{
    _stateMachine.Configure(HealthState.NonReproductive)
        .Permit(Activity.ReachPuberty, HealthState.Reproductive);

    _stateMachine.Configure(HealthState.Reproductive)
        .OnEntry(() => Console.WriteLine("Entered Reproductive state."))
        .OnExit(() => Console.WriteLine("Exiting Reproductive state."))
        .PermitIf(Activity.HaveUnprotectedSex, HealthState.Pregnant, () => ParentsNotWatching)
        .Permit(Activity.UndergoSurgery, HealthState.NonReproductive);

    _stateMachine.Configure(HealthState.Pregnant)
        .OnEntry(() => Console.WriteLine("Entered Pregnant state."))
        .Permit(Activity.GiveBirth, HealthState.Reproductive)
        .Permit(Activity.HaveAbortion, HealthState.Reproductive);
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • OnEntry: Executes an action when entering a state.
  • OnExit: Executes an action when exiting a state.

Summary of the Code Flow

  1. Initialization: Sets up the state machine with HealthState.NonReproductive as the initial state.
  2. State Configurations: Defines each state and its possible transitions, using Permit and PermitIf to control allowed transitions and conditions.
  3. Conditional Property: Uses ParentsNotWatching to control whether an activity leads to a transition.
  4. OnEntry/OnExit: Demonstrates how to add actions when entering or exiting states, providing flexibility for additional logic.

Example Output

Running the code might produce output like:

Initial State: NonReproductive
After Puberty: Reproductive
After Unprotected Sex: Pregnant
After Giving Birth: Reproductive
After Surgery: NonReproductive
Enter fullscreen mode Exit fullscreen mode

This output indicates how the individual transitions through different states, with each action having a specific outcome based on the current state and conditions.


Benefits of Using Stateless for State Machines

  1. Declarative Syntax: Stateless allows for easy definition of states, triggers, and transitions.
  2. Flexible Configuration: Supports conditional transitions and complex workflows.
  3. Hierarchical State Management: Ideal for handling complex scenarios in larger applications.

Conclusion

The Stateless library in C# is an excellent tool for managing state machines in a declarative and structured way. In this example, we demonstrated how to use Stateless for a scenario in reproductive health,

leveraging its features to define states, triggers, and conditional transitions.

For more complex state management needs, Stateless provides further customization options, including hierarchical state machines, allowing you to create robust workflows efficiently. Whether you’re handling application states, user workflows, or other finite state systems, Stateless simplifies the process, making your code more maintainable and easier to understand.


Additional Resources:

  • Stateless Documentation: Stateless GitHub
  • Further Reading: Check out other state management tools and libraries if you need a more specialized solution.

Top comments (0)