Articles Feed

Authors

Categories

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.

  1. require 'rubygems'
  2. require 'statemachine'
  3. vending_machine = Statemachine.build do
  4. superstate :operational do
  5. trans :waiting, :dollar, :paid
  6. trans :paid, :selection, :waiting
  7. trans :waiting, :selection, :waiting
  8. trans :paid, :dollar, :paid
  9. event :repair, :repair_mode, Proc.new { puts "Entering Repair Mode" }
  10. end
  11. trans :repair_mode, :operate, :operational_H, Proc.new { puts "Exiting Repair Mode" }
  12. on_entry_of :waiting, Proc.new { puts "Entering Waiting State" }
  13. on_entry_of :paid, Proc.new { puts "Entering Paid State" }
  14. end
  15. vending_machine.repair
  16. vending_machine.operate
  17. vending_machine.dollar
  18. vending_machine.repair
  19. 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.