Tuesday, December 15, 2009

TDD: the Failed Panacea

Yes, I admit it.  TDD is not a panacea; it fails as a cure-all. Some bigots might claim it is (and, yes, I probably sound like a bigot every now and then), but TDD isn't perfect.

Over the last few months I've been working on a compiler at Soph-Ware Associates. The compiler supports annotations mostly identical in syntax to Java's annotations.  After a few hours using acceptance test driven development (ATDD), I had developed a nice structure for defining annotations using builders:


builder.withAnnotationName("AnnotationName")
.addRequiredArgument("attributeId", DataTypes.STRING)
.addOptionalArgument("answerId", DataTypes.INT,  42)
.build();
// arguments can also be created with builders if desired


My next step was to query various data from the annotation to be used in the compilation process, but I couldn't find a way to refactor my previous definition and validation work to retrieve the needed data. All the information was there, sort of, but not in a way that was amenable to refactoring.

TDD Failure

TDD had failed me. Okay, unlikely. TDD cannot force me to take into consideration everything I need consider.  Had I spent a bit more time thinking about the information I would need later, and less about up front validation, I may have realized that had I done things differently, I may have been able to accomplish both quite easily. TDD didn't force me to use the validation algorithm I did.  In fact, the more I think about it the more I believe I failed at closely adhering to the single responsibility principle, as applied to functions.

Even though TDD isn't the panacea I might like, at least I reap the benefits of its other strengths.

Friday, December 11, 2009

Featuritis: Cleaving to the Big Picture

A gray rubber mat sat in front of me as I figured out the rough dimensions needed so it would fit snugly in our car. Once I knew where the various slits and slots needed to be, I grabbed a pair of scissors and started to cut, being very attentive to cut in a straight line.

After cutting a few inches my eyes wondered up to the top of the gray mat. To my utter dismay and disgust, I found that I had indeed cut a straight line... that was off by about 20 degrees. I had been so focused on cutting a straight line that I hadn't confirmed that my line fit with the bigger picture.

Infected

Most programmers, myself included, seem to infected with the featuritis virus, the one that eats at us trying to convince us that we absolutely must build extraneous features or implement a design pattern that we think is necessary only later to have this new feature sit stagnant and untouched. It's a vile little virus that wastes our time and diminishes our ability to meet the truly important goals.

Goals

The problem with featuritis is that it distracts us from our ultimate goal -- to develop, as quickly as possible, quality software that meets its users needs and provides no unnecessary features. I'm going to concentrate on only one part of this -- meets user needs.

If we build an unnecessary feature, the program will take longer to build than is necessary. If we must consistently retest some feature, we're wasting our time. If we must consistently work around a poor architecture, design, or implementation, we're wasting time.

Defeating Featuritis

So, how do you defeat the despicable virus? Acceptance test driven development, a form of top-down design.  In brief:
  1. Write an acceptance test firstDon't lose sight of the end-user need.
  2. Write only the code necessary to satisfy that requirement. This will often entail writing many different unit tests, each of which exercises part of the user's required functionality. Be diligent; be strong. It's hard, but with the end-goal, or big picture in mind, progress is evident.
  3. Refactoring to follow the single responsibility principle (SRP). When you keep your code clean, it's much easier to spot when you start getting off on tangents.
  4. Repeating the above steps as necessary
The above is also known as acceptance test driven development (ATDD).  A much richer explanation is available in Lasse Koskela's Test Driven. He has a sample chapter available that explains ATDD.

Remember the big picture, the ultimate goal.  Whenever I forget, I ultimately regret my lack of foresight.  When I get distracted by scenery, I don't end up where I want to go.

Tuesday, December 8, 2009

You Want to be Good, Eh?

Hindu Prince Gautama Siddharta, founder of Buddhism, once said:

The mind is everything. What you think you become.


Another author whose name I no longer remember defined our individual character by the quality of our thoughts during our idle moments.  Our thoughts form the connecting thread that binds these two statements together.

When idle, what do you spend most of your time thinking about? At work? At home? If we use our thoughts as a scale against which we measure ourselves, how do we fair?

Devotion Indicator

If I spend all my time doing "big design up front" (BDUF), thinking about its ramifications, and dealing with its subtleties, it's fair to expect that it's what I'm going to be good at.  If I devote all my time working on UML diagrams and designing architectures, it will become one of my areas of expertise.  If I dedicate my concentration and focus on writing lots of code, my ability to pump out volumes of code will increase.  But, is this really what I want?

Quality Focus

Although some things aren't worth dedicating our time and energies to, quality deserves our most tenacious focus.  Whether we're focusing on quality code, quality user experiences, or performance as quality, maintaining a focus on quality leads to skill improvement in that area -- "What you think you become."

So, you want to be good, eh? Then focus on quality.  Study it.  Learn it.  Absorb it. You won't build a remarkable product nor a purple cow without it.  With it, however, maybe you'll build the next Amazon.

Wednesday, December 2, 2009

Bad Layered Architecture

I recently discovered something I should have realized a long time ago -- Layers are less about modularity and more about isolation.


I've been working on a compiler that I'll describe as four different layers.  It could be depicted graphically as follows:




Here's a rough and simplified description of each layer:

  1. Lexer and Parser - The lexer converts standard human readable text into tokens which are then processed by the parser.  The parser examines the tokens and makes sure they adhere to the specified context free grammar and builds an abstract syntax tree (AST) used for later phases.
  2. Semantic Analysis - Examines the AST and confirms that variables are not used without being declared and that the operators and functions used exist for the specified types.
  3. Type Checker - Verifies that types are used appropriately and as declared, e.g., that strings are not compared to integers.
  4. Code Generator - Processes the AST and generates the appropriate machine code.
And there you have it -- a nice layered architecture, or so I thought. What I had really created was a layered mountain, one that required that I scale each prior layer in order to work with the next.  Each layer was its own module, but it was mildly coupled to the next, or worse, to the next and the previous layers.

Layering Is Not Enough

TDD alone didn't solve the problem.  In this case, it was quite easy to write tests, but the tests were basically acceptance tests and less unit tests.

Yeah, I thought layers were simple.  I made sure each layer was its own module. My code was even easy to test:

@Test
void generatedCodeContainsExpectedPattern()
{
    String input = /* ... */;
    MachineCode code = compiler.toCode(input);
    assertThat(code, behavesAsExpected());
}

But I had failed.  As time progressed, it became harder and harder to identify exactly where an error occurred.  For a while I justified myself, after all, it wasn't my fault I needed a compiler generator.  I eventually had to admit my failure.

Create Isolated Layers

I needed isolated layers, layers that permitted me to work independently of the others, layers that protected me from change, layers that could be examined individually.  The result didn't look much different, but was considerably easier to work with:



Between each layer that previously existed now stands an isolation layer with a single purpose -- to isolate behavior and functionality.

For example, the foundation of my my first code generator was a set of helper functions that were used to generate the correct code.  These helper functions have now been simplified and augmented with a builder class.  The builder allows me to test just the specific pieces of the code generation, without regard for my input. The helper functions now truly have a single responsibility. As long as the code generator calls the builder in the same way as my unit tests, I am now guaranteed that the generated machine code will be correct.

My newly inserted isolation code was dead simple and almost trivial to understand, yet I reaped huge paybacks.  Although I failed at first, in the end I learned something -- isolate my layers -- and that's a win.