![]() |
Articles Feed |
Categories
Archives
- December 2008 (3)
- November 2008 (5)
- October 2008 (4)
- September 2008 (6)
- August 2008 (4)
- July 2008 (5)
- June 2008 (5)
- May 2008 (4)
- April 2008 (2)
- February 2008 (4)
- January 2008 (2)
- December 2007 (2)
- November 2007 (2)
- October 2007 (2)
- September 2007 (1)
- August 2007 (3)
- July 2007 (1)
- June 2007 (4)
- May 2007 (7)
- April 2007 (2)
- February 2007 (3)
- January 2007 (3)
- November 2006 (3)
- October 2006 (3)
- September 2006 (17)
- November 2004 (1)
Test, Code, There is no Step Three!
by: eric | April 13th, 2007 | 0 comments »
Occasionally even great developers fall into bad habits, and the rest of us do it more than occasionally. This is especially true when under deadline and suddenly everything you’ve ever learned is thrown out the window in a desperate effort to get something to finish. I’ll share a story from a recent project to demonstrate just how much trouble rushing can get you into, and why we shouldn’t chuck our good habits at the first sign of adversity.
The story was relatively simple. Multiple XML documents needed to be generated based on information from another application in the system. My pair and I sat down and wrote our first test, which looked a little something like this:
specify “valid data should generate valid document†do data.valid = true doc = doc_generator.generate(data) doc.valid?.should eql(true) doc.header.should eql(“Valid Headerâ€) doc.text.should eql(“Valid Textâ€) doc.footer.should eql(“Footerâ€) end
So then we wrote a little code:
def generate(data)
if data.valid
doc = Document.new
doc.header = “Valid Headerâ€
doc.text = “Valid Textâ€
doc.footer = “Footerâ€
end
end
Okay we’ve got a passing test, with the simplest thing that could possibly work.. Great. Now let’s do the next one. Looking at the acceptance test we need to generate a different document when the data is notifying us of inaccessible data. Unfortunately there isn’t a flag for that, so we’ll have to figure it out from a combination of booleans. We write another test, we get it to pass with this code:
def generate(data)
if data.valid
doc.header = “Valid Headerâ€
doc.text = “Valid Textâ€
doc.footer = “Footerâ€
elsif data.invalid and data.accessible
doc.header = “Different Headerâ€
doc.text = “Different Valid Textâ€
doc.footer = “Footerâ€
end
end
That’s getting pretty ugly, but hey all the tests pass and we’re under deadline. We could replace the boolean checks with a query, but we’ve really got to finish and can’t be bothered with that kind of change. It would take almost a minute! Of course the big problem isn’t this code, it’s that we have quite a few more acceptance tests to go. Eventually this code looked something like this:
def generate(data)
if data.valid
doc.header = “Valid Headerâ€
doc.text = “Valid Textâ€
doc.footer = “Footerâ€
elsif data.invalid and data.accessible
doc.header = “Different Headerâ€
doc.text = “Different Valid Textâ€
doc.footer = “Footerâ€
elsif data.reset and not data.invalid
if data.new
doc.header = “Yet more Different Headersâ€
doc.text = “Valid Textâ€
doc.footer = “New Footerâ€
else
doc.header = “Another Different Headerâ€
doc.text = “Random Textâ€
doc.footer = “a completely different Footerâ€
end
else
doc.header = “Yet more Different Headersâ€
doc.text = “Oh for crying out loud.â€
doc.footer = “Good god another Footerâ€
end
end
What do all those boolean combinations check for? Heck I don’t know. On top of that we’ve got several messages that are sent by this class that don’t use the data passed in, which look like this:
def send_error(message) doc.header = “Error Header†doc.text = message doc.footer = “Footer†end
Clearly we had a problem, and my pair and I knew it the entire time, yet each time the refactor step in TDD arrived we would grin and bear it, with that deadline looming. Each time the step would take longer and longer, as the code became more complex, but we wouldn’t change because of the immediate “benefit.†It’s always quicker to just change the one piece of code, right?
Obviously the answer is no, it isn’t. My pair and I demoed our code, made the customer happy and than frantically began rewriting the code. After about two more days of broken tests we had a reasonably clean and elegant solution, and I didn’t have to hide the code from Micah anymore. How long did the first “draft†take? Oh, about two days all told. So we laugh and realize we could have made our deadline even if we had done the job right the first time, but even that isn’t true. Remember the code became more and more complex as we worked through the story, and began to resemble legacy code. Each new document required us to figure out each boolean check, and usually led to us breaking old tests as the fragile code changed. I firmly believe that if we hadn’t continues using the blunt tools of conditional code and implemented the object-oriented solution we later refactored to we would have actually been done more quickly. Furthermore while cleaning up that ugly code I wasn’t providing business value to the customer, thereby hurting the following iteration. A few days later I needed to add a new document to the system. What had previously taken a half-hour or so for each case took seconds. Imagine if we had done that the first time. The moral of the story clear. CleanAsYouCode, and don’t let deadline pressure cause you to abandon your own principles. In the long run and short run you’ll improve your codebase, make your customer happy, and probably enjoy your job more too.
Thanks to Instiki for the headline
Understanding Statemachines, Part 4: Superstates
by: micah | April 7th, 2007 | 4 comments »
Superstates
Often in statemachines, duplication can arise. For example, the vending machine in our examples may need periodic repairs. It’s not certain which state the vending machine will be in when the repair man arrives. So all states should have a transition into the Repair Mode state.

Diagram 1 - Without Superstates
In this diagram, both the Waiting and Paid states have a transition to the Repair Mode invoked by the repair event. Duplication! We can dry this up by using the Superstate construct. See below:

Diagram 2 - With Superstates
Here we introduce the Operational superstate. Both the Waiting and Paid states are contained within the superstate which implies that they inherit all of the superstate’s transitions. That means we only need one transition into the Repair Mode state from the Operational superstate to achieve the same behavior as the solution in diagram 1.
One statemachine may have multiple superstates. And every superstate may contain other superstates. ie. Superstates can be nested.
History State
The solution in diagram 2 has an advantage over diagram 1. In diagram 1, once the repair man is done he triggers the operate event and the vending machine transitions into the Waiting event. This is unfortunate. Even if the vending machine was in the Paid state before the repair man came along, it will be in the Waiting state after he leaves. Shouldn’t it go back into the Paid state?
Superstates come with the history state which solves this problem. Every superstate will remember which state it is in before the superstate is exited. This memory is stored in a pseudo state called the history state. Transitions that end in the history state will recall the last active state of the superstate and enter it.
You can see the history state being use in diagram 2. In this solution, the history state allows the vending machine to return from a repair session into the same state it was in before, as though nothing happened at all.
Code
The following code builds the statemachine in diagram 2. Watch out for the _H. This is how the history state is denoted. If you have a superstate named foo, then it’s history state will be named foo_H.
- require 'rubygems'
- require 'statemachine'
- vending_machine = Statemachine.build do
- superstate :operational do
- trans :waiting, :dollar, :paid
- trans :paid, :selection, :waiting
- trans :waiting, :selection, :waiting
- trans :paid, :dollar, :paid
- event :repair, :repair_mode, Proc.new { puts "Entering Repair Mode" }
- end
- trans :repair_mode, :operate, :operational_H, Proc.new { puts "Exiting Repair Mode" }
- on_entry_of :waiting, Proc.new { puts "Entering Waiting State" }
- on_entry_of :paid, Proc.new { puts "Entering Paid State" }
- end
- vending_machine.repair
- vending_machine.operate
- vending_machine.dollar
- vending_machine.repair
- vending_machine.operate
Output:
Entering Repair Mode Exiting Repair Mode Entering Waiting State Entering Paid State Entering Repair Mode Exiting Repair Mode Entering Paid State
Next we should cover pseudo states.
