The Skeptical Methodologist

Software, Rants and Management

Doing the simplest thing that can possibly work considered harmful

I’m not sure if there’s a blogger out there that hasn’t written a “Considered Harmful” blog.  If you go back to the original article by Dijkstra, he never comes out and completely condemns GOTO’s but rather advocates capturing popular GOTO patterns and using those in “Structured Programming”.  I.e., loops, ifs and switches.  We still have reasons to pepper our code with GOTO’s in some cases, although following the rule that the use of a GOTO is either a) bad form and there’s a better way or b) bad form and there ought to be a better way, has granted us some of the more rich control structures in our high level languages.  Exception handling is probably the more recent advancement.  Anywho, enough with history.  I just thought it’s particularly humorous to use the phrase “considered harmful” with “simplest thing that can work” that’s all.

TDD advocates a method that you test first, then code up the “simplest thing that can possibly work” until the test passes, refactor and repeat.  TDD also happens to be a pretty damned effective method of producing code quickly and easily, as well as a huge pile of unit tests that make refactoring a breeze and aid in ensuring the quality of your code.  As I said in Unit Tests as a Negative, unit tests and the tests that TDD pushes you to create also serve as an excellent specification and documentation of your code – each and every one of them are contracts, enforced each test cycle, that explain “When my code gets a, it returns b.  Always.”

I can see the attraction to the “simplest thing that can possibly work” mentality.  Indeed, many of the problems we arrive at in software come from over-engineering a solution.  But blindly following the simplest thing, including the KISS principle, can lead to tragically comical under-engineering.  I’m reminded of a post from months ago where a TDD advocate attempted to develop a Sodoku solver using test driven methods.  It was pretty painful to watch.

The problem he ran into is that the main problem in software design is and always has been: How do we define the problem?  The problem is the problem.  When we can specify the problem well, and understand it, generally speaking there are always algorithms and data structures to solve it completely and elegantly.  It’s when we cannot specify the problem that we run into boundary errors and extensibility issues.  TDD is a very effective way to explore a problem domain one step at a time, when it’s unfamiliar.  Unfortunately, the problem of solving a Sodoku has an already well defined, elegant algorithmic solution.  There’s no reason to plod away step by step using TDD to explore it.  Moreover, TDD is one of many methods of problem domain exploration.  It gets points for probably being one of the most general, and applicable to any sub domain, but certainly gets points taken away in this case as it’s particularly bad at solving a mathematical problem.

In our Calculus courses, we didn’t spend all day making sure an equation worked when x = 1, then x = 2, then x = 3.  No, we instead explored a few examples, then attempted to algebraically generalize the solution in a proof.  Likewise, TDD can help you explore a domain, but too much emphasis might be given on the KISS regimen and too little on the refactor.  Just like our normal generalization ‘instinct’, if we’ve done something three times, it’s time to generalize it.  Likewise, three tests for any problem with a fundamentally algorithmic solution should give us enough ‘exploration’ to allow us to put our computer scientist hats on and provide a robust algorithm + data structure solution.

Not everything turns into an algorithm, obviously, we have message forwarding code, simple switches and our object structures.  TDD works here too, but you can’t always generalize.  However, the hardest things to test to prove an absence of defects also tend to be the ones easiest to abstract into a mathematical problem.  Red-Black trees have certain characteristics about them that have been proved logically.  Once we identify a problem as one that a Red-Black tree solves, the only testing we need to do is to ensure we didn’t screw up the implementation of the Red-Black tree.  We don’t need to keep testing for new inputs and outputs once we’ve taken an established solution to a problem, and frequently, TDD helps us find those solutions.

So while the first cycle of testing, using the KISS approach might work, frequently we need to always keep our trusty old computer scientist hats at the ready and, upon having a few examples/use cases/tests on how something should work, design the proper algorithm not just to satisfy the use cases.  In other words, abandoning the “simplest thing that can possibly work” for another methodologist cliche, “over-generalization is the root of all evil” (apparently there are two roots) might be a more effective way to utilize TDD.  Generalizing a solution over it’s variable inputs is one thing, but generalizing a solution over it’s types, functors, uses, policies, etc… is quite another.  One provides for more robust code as there is usually a ‘known’ proper, elegant solution.  The other makes code reuse much easier – however, it makes no sense when you only use your code once and can frequently send us down rabbit holes.

The elegant solution to a problem usually is a mathematical one, but it may not be the simplest.  Don’t confuse mathematical generalization with software reuse generalization as it might make you spend about 100x more manhours on that Sodoku solver than you needed to.

Advertisements

September 5, 2008 Posted by | Testing | , , | Leave a comment

Unit Tests as a Negative

There’s a constant debate between hacker types and PHB(Pointy Haired Bosses) types over what exatly it means to ‘design’ software.  While many things in software that we consider ‘design’ are helpful in one way or another, they don’t suffice for a design like you might find in other engineering disciplines.

For example, if I gave you a ‘design’ for a bridge, a blue-print for a bridge, it would then be a completely mechanical effort after that to build the bridge.  The design is a peice of knowledge that captures all the decisions that must be made to build a bridge, leaving only the physical work to be done.  (I realize here I’m simplifying bridge work but stay with me…)

In software there’s no such thing, and we can prove it by contradiction.  If you were to give me something that could be turned into ‘software’ with only mechanical effort, i.e., some sort of ‘design’, similar to designs in other fields, what would you have actually given me?  You would have given me the CODE for that software.  Compiling IS the mechanical work of software, as our field is entirely knoweldge based.  There are no materials to put together once the ‘design’ is complete, and the idea of removing all decisions means you’ve specified your software so much you might as well have coded it.

Programming languages are, after all, our best attempt at describing a language that allows non-ambiguous description and determination of a system.

In other words, as the hackers have always said, “The Design is the Code”.  This point of view is very attractive, but I want to add a nuance to it that might show room for comprimise between hackers and PHBs.  Software starts out at a high level – if you’re doing waterfall, you start out by building requirements.  If you are doing agile, you start out by gathering user stories/cases.  Most of us probably start out doing a little bit of both – we need to start at a very high level description of the system we’d like to build.

This would be like being asked by a city to build a bridge over some river.  You still need to scout for a location, secure funding, go through designs given to you by architects, etc.  The use case phase is similar.  As we drill down, we can turn use cases into smaller and smaller sequences of behavior the customer wants, or more and more detailed requirements on different parts of the system.  We do this, ideally, until we get to the point that it’s more effective to use code to describe what the system should do than to use high level abstractions like sequences and stories.

But our requirements, our user stories, they are not just providing a means to drill down into what our system is supposed to do – they are levying TESTS on our system.  The highest level we can call verification testing, but ultimately, for every use case, one should imagine that there ought to be an automated way to test that use case to ensure the system we are building fulfills that case.

Like plaster being poured into a mould,  our software is ‘poured in’ to these implicit tests.  The mould defines where our system stops.  Similar to photography – when we create a photograph, we not only have the picture, but the negative.  The negative defines the complete opposite of the picture, and if combined with the picture would simply look like a meaningless gray.  It is through the difference between the negative and the picture that form takes place.  Likewise, it is not just in code, but also in our means of testing that code, that our true design forms.

Specifications, requirements and use cases all are simply high level views of ‘test-driven-development’.  A test is just the negative of the software that fulfills it, and together, the test and the software, do you have a true design.  If we focused more on continually refining our requirements and use cases into actual test automated test cases, at the lowest level, then we can take advantage of TDD from the begining in.

After all, for anyone who’s done TDD, what’s the first thing you do when you start out with a blank slate?  You decide what it is you want your new object to do, and then you write a test for it.  A specification can be seen as a test (unfortunately they do not exist in that form very much today).  A specification can be seen as a negative of the software that it produces.

For every object in your software, there should be a negative, a thing that describes the exact opposite, partnered to that object.  If your software provides a function which you plug in 3 and get out 6, then you should have a negative that plugs in a 3 to some nameless thing and expects out a 6.   These are two ways of describing the same thing, but as art like photography and sculpture shows, you need both to move forward.

August 2, 2008 Posted by | Testing | , , , , | 2 Comments