Friday, November 05, 2010

State Creep

State kills. It makes concurrency difficult and transactions brittle. State creeps into your program slowly and, over time, makes things difficult to read, understand, manage and maintain.

What is state? State is data. But data by itself is not complicated, nor is it something we can do without. When I talk about state, I mean data that affects business logic. Depending on the state of a variable, the logic of how a module works changes.

The most common form of state is the Boolean:

is_manager
requires_approval
can_see_salaries

When you see a boolean, you should automatically see an IF statement … and, more than likely, many of them.

If two or more booleans are related in such a way that the value of one affects the value of another, your complexity just shot up 2^N. You can expect to see the most nasty construct in your code: The Nested If (and remember that And/Or clauses are just short forms of nested If's). Two related Booleans means four cases to consider. Three means eight. You get the idea.

def show_salaries:
    get_approval = false
    if requires_approval:
        get_approval = not is_manager
    if can_see_salaries:
        if get_approval:
            return redirect('GetApproval')
        else:
            return redirect('SalaryPage')
    return redirect('NotAllowed')

Ugh.

Recommendation: Once you get beyond two linked booleans … Extract to Class. If you can't avoid these nested If statements, at least keep them in one place.

If you're lucky, the state is atomic. In which case the object can only be in one state at a time. Finite State Machines (FSM's) are great way to manage atomic state. The Poor Man's FSM is the Enum.

enum RegistrationState { Ready, EmailSent, Waiting, Approved, Error }

This is usually followed by state transition statements sprinkled throughout your code. Every time you see a branch in your FSM, you should again see an If statement. It gets worse if you need to worry about multiple threads coming into module and attempting to change state. You can see from the diagram above that the Enum does nothing to tell you about valid or invalid transitions from one state to another. You have to read all the code to understand what is acceptable. 

Recommendation: PMFSM's are generally bad, but better than nested If's. If you have anything beyond the most trivial PMFSM … Extract to Class. It's nice to keep your state transition logic and locking mechanisms in one place which is easy to test.

But the Poor Man's FSM is only good for state within a single instance of an object. When multiple objects (probably of the same type) need to coordinate you need something a little more industrial strength.

Consider two Employee's that need coordinate on an approval for pending purchases. At first blush you might think the state diagram for the process looks like this:

The PMFSM might look like this:

enum PurchaseState { Ready, Review, Approve, Reject, SendEmail, PlaceOrder, Done}

In reality there is a separation of state between both Employee's. The Role of the Employee in this particular FSM determines which states are applicable to each employee. Bob and Alice are both Employees, but Bob is the Requester and Alice is the Approver. So, keeping the state of the process in any one Employee instance is going to get ugly quickly. It will also limit the number of purchase requests that Bob can make concurrently to 1.

The real FSM might look something like this:


We need to break this FSM out and away from the Employee and make them participants in the transitions of the FSM. Petri-Nets (PN) are excellent data structures for modeling these sorts of FSM. With a Petri-Net you can model concurrency, forks, joins, splits, merges and multiple running jobs … everything needed by most business applications.

PN's are the basis for most modern Business Process Modeling (BPM)  / Workflow solutions such as Windows Workflow Foundation, jBPM and a multitude of others. If you live in a big stack like Java or .NET, these are valid options. We're also starting to see many BPM solutions becoming SaaS, so implementation language is becoming less of an issue. 

You'll find that for most business applications, having a good BPM solution in your toolkit will make your life so much easier. And you don't need a heavy-weight solution that has graphical editors, connectors and other bloat … just a simple Petri-net implementation will get you a long way to more maintainable code. You'll find one for nearly every language out there.

Recommendation: When you have complex state interactions between multiple objects, consider using a Petri-net based library to keep your code manageable.

To summarize, state is a nasty thing in your code. Always be aware of it and be vigilant to keeping it under control. Hopefully some of the tips I've listed in this article will make that process easier.  

0 comments: