One of the problem I see with explaining usefulness of Finite State Machines design to web developers is similar to explaining the monads - once you've get them, you loose the ability to explain them to others. It's sad, as their understanding opens before you a totally new ways of solving the problems. Nonetheless, let's try to do the impossible :)
Example
Let's take following example:
We're responsible for creating logic handling cinema seat reservations. Only free seats can be reserved, and before being paid for, they also need to be reserved first. Cinema customers can revoke reserved seats. They also have a limited time to pay for reservation - after time has passed, seat becomes automatically freed.
This can be simply modeled using diagram below:
Looks quite easy, right? The actual problem is that this is nowhere close to real life. Quoting Morpheus from "Matrix" movie:
You've been living in the dream world, Neo. This is the world as exists today:
void Reserve(SeatSession seat, out CancellationToken cancel)
{
if (seat.ReservationDate != null) throw new SeatAlreadyReservedException();
seat.ReservationDate = DateTime.UtcNow;
cancel = Scheduler.Schedule(this.ReservationDate + paymentTimeout,
() => RevokeReservation(seat));
// some more stuff
}
void RevokeReservation(SeatSession seat, CancellationToken cancel = null)
{
if (seat.ReservationDate == null) throw new InvalidOperationException();
if (seat.HasBeenPaid) throw new SeatAlreadyPaidException();
this.ReservationDate == null;
if (cancel != null) cancel.Cancel();
// some more stuff
}
void ConfirmPayment(SeatSession seat)
{
if (seat.ReservationDate == null) throw new InvalidOperationException();
if (seat.HasBeenPaid) throw new SeatAlreadyPaidException();
seat.HasBeenPaid = true;
}
This is not yet truly convoluted code, but I think, even at this point you can hardly tell, what was the original intent of it. To say, which methods are valid to call from current seat state, you need to go through the whole code. Now try to maintain or develop code written this way. I think, most of you already know that pain too well. The original idea has leaked from this representation. Why? Because most of us, when we think about business logic, we want to think in terms of those higher abstractions, states and transitions. Boolean flags and ifs are good for machines, not humans.
This is place, where Finite State Machines comes in. Instead of using cryptic if/else branches, we may try to represent original logic structure of states and transitions directly in our code.
In functional languages (like F#), you could express them using mutually recursive functions, like follows:
// lets define all valid state change triggers
type Transition = | Reserve | Revoke | Pay
// each recursive function defines current state
// while pattern match describes valid transitions from it
let rec free seat = function
| Reserve ->
let cancel = schedule(DateTime.UtcNow + paymentTimeout, Revoke)
// some more stuff
reserved cancel seat
| other ->
unhandled other
free seat
and reserved cancel seat = function
| Revoke ->
// some more stuff
free seat
| Pay ->
cancel()
// some more stuff
paid seat
and paid seat = function
| other ->
unhandled other
paid seat
Looks closer to original domain design, isn't it? It's essentially a written representation of our original state diagram, to (and from) which we can go at any point in time.
Now think about modifying current logic? In first case we usually end up trying to guess original idea while singing "WTF?!" song of our people. In FSM approach we can easily recreate the mental model of this approach directly from the code (no need to look into outdated docs).
PS: While you may think "it's nice, but I'm a C# guy", some frameworks (like Akka.NET) allows you do build dynamic behaviors which can be utilized to build FSM also on the C# side.
Comments