Loose the debugger
My ideal development team would not use step-through debuggers. If I am responsible for mentoring newbie programmers, my first rule would be – no step-through debuggers. My ideal IDE would include all the editing, analysis, navigation, and refactoring features, that modern incarnations like Eclipse and IntelliJ have, but without the step-through debugger.
Does this make any sense at all?
What is a debugger used for? It is used to understand what a piece of code is doing. The code might be doing something wrong, aka a bug. The debugger can help us understand how a bug came to be. If there were no debugger, what can you do?
Well, read the code.
Less Code
If reading code was the only way to understand the code, wouldn’t you write less code? You will. This will act as a disincentive to proliferation by copy/paste. This will be an incentive to learn to ‘not repeat yourself‘ (the DRY principle).
Intelligible Code
If reading code was the only way to understand the code, wouldn’t you write code that is easy to understand? You will learn the difference between the code, and the intent of the code. You look at code, and ask yourself, WTF, why was this code written? That.
Is there any need for code to be a puzzle? Here is an example.
protected <I, O> O mapIO(Class<O> clo, I in) {
try {
Class<?> cli = in.getClass();
O out = clo.newInstance();
for (Method mo : clo.getMethods()) {
if ((mo.getName().startsWith("set")) &&
(mo.getParameterTypes().length == 1)) {
Class<?> pr = mo.getParameterTypes()[0];
Method mi = null;
Object ob = null;
if ((BaseList.class.isAssignableFrom(pr)) ||
(Collection.class.isAssignableFrom(pr))) {
mi = null;
} else {
try {
mi = cli.getMethod("get" + mo.getName().substring(3));
} catch (NoSuchMethodException e) {
try {
mi = cli.getMethod("is" + mo.getName().substring(3));
} catch (NoSuchMethodException e2) {
}
}
if (mi != null && pr.isAssignableFrom(mi.etReturnType())) {
ob = mi.invoke(in);
} else {
try {
ob = pr.getConstructor().newInstance();
} catch (NoSuchMethodException e) {
ob = null;
}
}
mo.invoke(out, ob);
}
}
return out;
}
catch (Exception e) {
......
}
}
Any idea what the above code does? Right. I looked at it and my eyes started to swim. It is in fact, a decent method. It is short, and once you decipher it, you see that it does one simple thing. The rub, of course, is having to decipher it. I had to spend some time digging into it, doing a little archaeology as it were, to discover the intent of the code. So what does this code do anyway?
Given an input object, and the class of the output, instantiate, and
initialize an output object, in the following manner.
Scalar properties, which exist in the input object, are copied over.
All other properties - properties that do not exist in the input object,
and vector properties (collections), are initialized with the
default no-arg constructor.
That's it.
This is the 'intent' of the code. This is what the code is supposed to
accomplish. This is why the code exists.
Now, why can’t the code just say what it means? Something like this.
protected <I, O> O prepareOutput(Class<O> outputClass, I input) {
try {
List<Field> propertiesToBeCopied =
getMatchingScalarProperties(outputClass, input);
List<Field> propertiesToBeInitialized =
getRestOfTheProperties(outputClass, propertiesToBeCopied);
O output = outputClass.newInstance();
copyProperties(output, input, propertiesToBeCopied);
initializeProperties(output, propertiesToBeInitialized);
return output;
}
catch (Exception e) {
......
}
}
This alternative code is definitely less efficient than the original version. On the other hand, what this code is about, is fairly obvious. Even if it turns out that I must optimize this code, I will have more confidence in that attempt, because I start with a better view of what the code is supposed to accomplish.
Wait, I see how we can make this more efficient, without sacrificing the clarity we are heading towards.
protected <I, O> O prepareOutput(Class<O> outputClass, I input) {
try {
O output = outputClass.newInstance();
for (Field field : clo.getDeclaredFields()) {
if (isMatchingScalarProperty(output, input, field) {
copyProperty(output, input, field);
else {
initializeProperty(output, field);
}
}
return out;
} catch (Exception e) {
......
}
}
As a friend of mine says, whaddyathink? Notice, this version looks a lot like the English description of the intent of the code. I just translated English to Java. Give me code like this, and I don’t need a step-through debugger.
This is also a good example of the oldest truth in design (or writing, for that matter) – you almost never get it right the first time.
Also, see the point Martin Fowler makes about about code that requires comments. It is at the end of the ‘Bad Smells in Code‘ chapter, of his refactoring book.
What you see is what you get
Bob Martin, in his book, “Clean Code: A handbook of Agile Software Craftsmanship“, quotes Ward Cunningham’s notion of clean code – “You know you are working on clean code when each routine you read turns out to be pretty much what you expected“.
You loose the step-through debugger, you get this.
Haven’t you had to deal with a method that had some simple name like getDriversLicense, but went on to do everything from the groceries to changing your baby’s diaper, and in some obscure corner, almost as an after thought, it retrieved your driver’s license. If the method, getDriversLicense, did just that and nothing else, you could skip reading the content of that method.
The more you are forced to read code, the more you will write methods that do one small thing, just the thing that the method’s signature suggests.
Developer tested
Of course that cleanly written, getDriversLicense method, could have bugs. How do you increase your confidence in the getDriversLicense method? As you read the code, you read a call to getDriversLicense, and say, okay, great, I know that works, and move on. You don’t want to have to also read that method’s definition.
You know the answer. How do you produce code that folks can implicitly trust? You test the daylights out of the code that you deliver. Automated developer tests.
Loose the debugger, and you will learn to hate the lack of developer testing.
Log your way out of trouble
Regardless of how clearly your code is written, there will be times when you will want hard evidence of what the code is doing to the data. In the absence of a step-through debugger, you will necessarily have to rely on logging. You can understand what your code is doing by logging inputs, outputs, and execution paths, which trace the code’s work.
Any enterprise system worth its salt must have good tactical logging anyway. Clear, and configurable logs, is useful for system maintenance, and business monitoring.
Loose the debugger, and you will be forced to nail down your application’s logging.
And so,
Do any of these alternatives to step-through debugging sound like a bad thing? No. Taken at face value, each of these alternatives, and in fact all of them together, add a lot of value, which the step-through debugger does not.
Think about it another way.
Why do you need the step-through debugger. 9 times out of 10, you need it to negotiate bad code. If you are starting from scratch, if you do not have to deal with legacy code, stay away from the debugger. This will force you to learn to write cleaner code.
Reading code makes you feel the pain caused by poor code. Using a step-through debugger helps you turn a blind eye to poor code. At its worst, the step-through debugger enables poor code.
A benchmark?
Say I am building a new software team, my own outfit even. I almost think that the missing debugger can separate folks that I want to rely on, from folks that I am sort of forced to rely on. At the minimum, I want developers that can learn to be productive without the step-through debugger. If you cannot live without that crutch, hmm, well, ….. I don’t know.