Wednesday, 6 May 2009

Look at the state of your code

Recently I've been working to re-implement an application from scratch. Not normally a sensible thing to do, but the codebase of the original was so far from being maintainable that you couldn't see maintainable from the highest point of the code on a clear day. With a telescope. Ostensibly the main reason it was such a mess is that as the application rolled out, there were an ever-increasing number of tweaks, special cases and optional extras which needed bolting into it. After a bit of thought, though, it seems to me that this is almost entirely not the case.

The root of the problem is that the code was never really designed before it was written. There were no pen-and-paper diagrams, whiteboard sketches or rough attempts at figuring out a structure prior to getting "stuck in" and hacking out code. It's a common mistake, it seems, that writing code is the important, difficult bit. It's really, really not. Figuring out how the code should hang together, that's the important bit. And as part of that figuring out both what the job you need it to do is, and what it might need to do in the future is. Now, obviously, you could take that too far - abstract enough and you just specify a thing that does something. Can't really design that, right? Well... sort of. But you know the special case of what it needs to do right now. Abstract all those things: If you need to be able to accept an input from an edit box before you fire the McGuffin that does your big cool number crunching task, then isn't it a good idea to make sure that you don't care where you get your data from? Yes, in this case it's an edit box, but why make life difficult for yourself in the future? You're going to get your data from something and in your first cut, that something will be an instance of an edit box. Next week it might be a piece of custom hardware. Design the way everything hangs together right, though, and aside from writing the code that details internally how you get data from the hardware, none of the rest of your code changes.

So far, so obvious. I'd hazard that no one really disagrees with that approach. But what really struck me with this project is how cool state machines are, for doing the above but with your business logic program flow. What you see in a lot of projects is a section of code that tells your system to do stuff - either the main loop, or the equivalent for the subsystem that you're looking at. Within it you'll often find a collection of conditional or a case statement. With the project we started with here, that had... I hesitate to say "grown"... mutated into a gigantic cascade of conditionals. Following what the hell was going was difficult, because there was no easy to reference concept of what the system thought it should be doing at any given point. It becomes difficult to add functionality, because checking what conditional statements are going to be executed is far from trivial. And on top of that, where do you sensibly add your code? Most likely as another conditional in the big list - making it even harder to get the next revision in. And because you've had to mess with the internal flow of the entire system, you've potentially destabilised what you had to begin with - even sections of the code you didn't think you changed.

So, here's an alternative approach: Let's abstract our central functional loop. It's going to take some data into it, sit there performing some function until it's done, return some data which determines what needs to be done next then pass whatever data it thinks might be useful out to the next iteration which will do the same thing. So what do we need to do this? Let's define a generic state object which will take care of Doing Stuff. We don't care what it does. We can give it access to a message queue so it can throw data out to non-state based components of the system (such as a GUI or some hardware), so there's no worries about it needing to have access to data that the central class shouldn't really relinquish - we can just pipe data out through the central controller class. Equally, we can pipe data in through the main controller loop. As much data as we like, and of any type. The state object can decide what to do with it, or junk it if it decides it's irrelevant. All we really need to define for the generic state is a HandleData() a Run() and a member variable to tell us when it's finished, and ready to move on to the next state - and, ideally, give us an idea of how this state finished. A binary works well here (success/fail), but you could use anything I guess, provided you can keep track of your state progressions. Now, we can define our states totally independently. Chaining them together into an application is just a case of defining a flow tree for the controller which tells it what state comes next for a given exit condition on the current state. You can define that in XML or whatever and parse it at runtime - no coding involved. The main control class becomes trivially simple - it just sits in a loop piping data into and out of the current state until it's told that the state is finished, then looks up the next state from the flow table and makes it current.

The neat thing about all this is that you inherently know what the system is doing all the time - it's just a state machine. You want to know what code is running when it's in a given state? The code in that state object. You don't have to figure out what conditionals are valid right now - just look at what that state does. You need to add functionality? Either change how the relevant state works if it's a simple change, or add a new state if you want the system to do something new. Then just update your flow table. Need to take something out? Just edit the flow table. You maintain tight encapsulation of your business logic the whole time, but at the same time get a very extensible framework. And because you're not modifying the existing code at all, you've drastically limited your ability to kark something up by mistake - you've got to either mess up your state flow table, or pass garbage into a state while at the same time managing to convince it that the garbage isn't garbage. A well-designed state object should be pretty resillient to this, and a well designed controller should do at least some amount of sanity checking of the state flows it loads to give you confidence that you are trying to run a system that has a chance of being stable.

Now, there's a downside to this: you pretty much have to stop and figure out what you're trying to do as a state machine, rather than hacking in some conditionals. Well, I say downside. Figuring out an actual state machine is probably a good idea in many cases - you've had to formalise what you want to do before you go ahead and try to do it.

Obviously, you don't always want to take this approach - if you're writing heavily optimised, time critical code then you maybe have other considerations to worry about. And no pattern is going to be right all the time, but I would hazard that unless you've got a good reason not to design your code using state machine architecture then it's probably a good idea to at least consider taking this approach. Especially if you know that you're going to need to add or modify functionality in the future - it really does make modification amusingly easy.

No comments: