I was asked what I tell people to do that I don’t do myself. The answer may surprise you.
The question came on a private Slack, from @ClareSudbery, who asked:
Do you ever follow practices in your own private software development which you discourage when teaching others? Or, inverting the question, do you ever miss out steps / practices in your own hobby projects that you recommend when teaching / coaching others? Guaranteed safety. I won’t tell anyone.
I gave a brief answer on the Slack, but today I want to provide a longer one. I’ll begin at roughly the same spot:
- I don’t tell people what to do or what not to do.
- I just show people what I do and what happens when I do it. And I try to explain my understanding of why whatever happened, happened.
I invite people to think about what they see, and to decide for themselves what to try, and what practices to adopt, drop, modify, change, …
That said, there are things that I think are good but that I don’t always do, and things that I think are not so good, that I sometimes do. Let’s look at a few.
- Test-Driven Development
- TDD, when it’s going well, is the best, fastest way that I know to develop software. Begin with a test, see it fail, make it work with the least code you can, see it run, refactor the code to be better, repeat endlessly.
It’s really the best way I know. And yet, sometimes, I don’t do it. I can think of three reasons that I don’t, none of them very good reasons.
Sometimes I don’t immediately see how to test the thing, but I do see how to write it, so I write it. Then, quite often, I don’t even test it much after I write it. And, quite often, when I do that, I find myself debugging for longer than doing with TDD would have taken.
Sometimes I don’t even see how to write the thing. For some reason, when that happens, instead of writing the test, which designs how to use the thing, I start banging in code that will generally lead me, after a few tries, to knowing what to do. If I were wise, I’d throw it away and TDD it. It wouldn’t take long at all. Instead, I often save the thing, untested. This often leads to defects later, and usually leaves me with code that isn’t easy to understand.
Sometimes the test is hard to write. That’s usually—I believe always—a sign that the code is poorly structured. The right thing to do, I suppose, is to improve the structure as soon as I notice the problem. What actually happens is that I’m working on some idea or feature, improving the structure seems like a deviation from my plan, so I put it off. It should not be a surprise that the code slowly gets worse.
Because I’m usually writing about what happens, I have a pretty good picture built up around what happens when I don’t use TDD and the short answer is that sometimes it goes OK, but often I find myself in a long debugging session, and I’m sure those sessions slow me down far more than writing the tests would have done. I could be wrong, but I’m not uncertain.
I’d go faster if I always did TDD. But sometimes, I do not.
- No one writes really good code all the time. At least, I hope that no one does, because I certainly do not. So, when I’m working well, when I spot code that isn’t all that good, I refactor it. Too often, I’m not working well.
I am disinclined to refactor code that isn’t well tested. This is another good reason to do TDD, but now that we’re here under refactoring, we need to observe that a reluctance to refactor tells us that there’s something wrong: the code is too complex and probably poorly tested. So we back away slowly.
When code needs improvement and we don’t improve it, things get worse. Somehow they don’t manage to stay the same: they get worse. Worse is not good.
I’d go faster and be happier if I refactored more. But sometimes, I do not.
- Small Methods and Objects
- I have come to prefer small code over large. Small functions that do one thing, small objects with one responsibility, and so on. I started out in a world where we didn’t always keep things small, and where we in fact often avoided functions to save time. But once I entered the Smalltalk world, I really began to see the value of tiny things whose purpose and operation are easily visible.
But sometimes, especially when some result requires the code to do one thing, then another, then another, I write all three of the things into one function, This has drawbacks. The large function is harder to understand than three small ones. It is less likely to be useful in more than one context, while the individual ones are more likely to be able to be applied in other situations. I would prefer it if I didn’t write large functions and my code would be better for it. And yet … I sometimes do it.
It’s the same with objects. This has shown up here in the dungeon program, where the GameRunner, the Dungeon, and some other classes have turned into a sort of attic storeroom where you just dump things that have no better place to go. These classes are hard to understand, because they don’t really have a coherent theme, and they seem almost invariably to be hard to set up, which at the same time they are needed in many tests of smaller objects. The result is that I do less TDD, and the code gets less reliable and harder to work with.
I’d do better with smaller objects and methods. But sometimes, I make them too large.
- Revert, Don’t Debug
- Often, especially when I’m working without decent tests, I’ll take a moderate-size step toward some goal. Then, commonly, I’ll test that step by running the whole program, since the tests aren’t serving well. And the program won’t work. And then I decide to debug the code. And then … a half hour later, maybe I find the bug. By that time there are prints all over the program, and there’s a general mess.
I believe that when I’m starting in a working state, and make a moderate-sized change and it doesn’t work, the best move is quite often to revert right then and there. And, after the revert, there are two very nice choices. The better one, generally, is to take a smaller step. The still OK choice is to make the moderate step again, not from memory but from current understanding. Quite often, just doing it again is enough to change what I type, avoiding the typo or logic error.
I would benefit from reverting more often. But too often, I do not.
- Commit, Dammit!
- The flip side of the revert coin is commit. I believe that whenever the code has changed and is again in a good state, I should commit. It doesn’t take long, and it amounts to a game save point to which I can return. I almost never need to, but when I do need to, the nearer the last save is, the easier it is to redo whatever has cause me to revert.
I would benefit from committing more often. But too often, I do not.
Perhaps you are thinking something like
No wonder he doesn’t tell people what to do, he’d be a world-champion hypocrite if he did.
You could be right, but the real reasons that I don’t say what people should do are that I don’t like to be told what to do, and that I don’t know what people should do anyway. I know what I do, and observe what happens to me, and I have explanations for it that may or may not hold water. I’ve observed many other programmers along many dimensions, and I see common behaviors and common results.
I’m pretty confident that small steps, driven by tests, cleaned up by refactoring, committed often in a ready-to-go state, will often make most efforts go faster and help most developers to be happier and more productive. I might even say usually rather than often.
But I don’t care to be told what to do, even when it’s really good advice, so I consider it common courtesy not to tell other people what to do, since I don’t want them telling me.
So I show you what I do, try to explain why I do it, show you what happens when I do it, and I invite you, explicitly or implicitly, to take a look and decide for yourself what to do.
And I’m not telling you to do that, either. You do you. I hope you enjoy it.