This Is My Ball of Mud
In college I wrote a solution for the Knight’s Tour. Since then I’ve planned to build a full chess program for playing chess games (not to be confused with a chess engine, which is more like an artificial intelligence that plots and plans chess moves). Not only did I want to build a chess game, but I wanted to use it as an experiment to test what I’ve learned about object-oriented design. I’ve made a few trial runs at it over the last few months, but always hit a design wall. Every few weeks I pick it up again and eventually hit a wall that stops me dead in my tracks. I’ve rebuilt the project from scratch about ten times now. Picking it up again recently, I decided this time I’m going to build this damn thing once and for all. Guess what happened? Wall. All these walls are starting to look like a castle, and I don’t mean the fun bouncy kind.
I’ve spent the last few months obsessing with design and binging on design books, methodologies, philosophies, etc. causing me to make some mistakes. I’ve noted two mistakes in particular:
- Lost sight of what’s important: Instead of solving the problem at hand, became lost in design, abstraction, and metaphor.
- Writing code without a guarantee that it works: Rushing, and not testing my code.
In the case of the first mistake, sometimes it’s okay to get lost in design, but not always and especially not when you just want to produce a working program. Trying to begin a project by shoehorning classes into inheritance hierarchies that you aren’t sure should exist yet is crazy, and unproductive. Cleverness eventually gets the best of you. The second mistake is unforgivable and is deadly in combination with the first. I don’t unit test my code. Until recently, most of my programs were small enough that I never considered testing individual parts. This is not so much a mistake on my part, but a result of circumstance.
A set of unreasonable abstract concepts that are untestable are not program-building tools, they’re wall-building tools. We don’t want walls. Walls are bad, mmkay? I predict in the future I’ll get better at choosing initial abstractions, but in the meantime I should offer myself and anyone who reads this some friendly advice. Design what is necessary, there is always time later to experiment with what is unnecessary.
There Are Many Like It
I think a lot of novice programmers end up in this situation where we’ve passed the point of writing trivial programs and want to improve our program design (At least you should. You don’t want to write rigid, fragile, ugly code forever, do you?). In my case, I’m looking to start a career as a developer, so I’m practicing to write well-abstracted, reusable and maintainable code. I’m aware that production code is often far from these attributes, but you don’t train to lose a race.
One thing that’s difficult is working without feedback. I’m not surrounded by programmers, so I have no way to tell if I’m correctly applying the things I’m learning. One thing this causes me to do is seek information less frequently from books, and more often from blogs and videos. There are some great videos online (if you can find them) made by professional programmers that walk you through coding exercises. You get to see their thought processes first hand. The benefit of this is you get to see where they make mistakes in their design and how they eventually recover them. I listed some resources at the end of this post.
At the beginning of a project I do upfront design to try and find the key abstractions for my project, but at some point you have to end upfront design and start building something. The problem with starting a project with very abstract ideas is it quickly grows beyond insight and beyond reasoning, and we end up face-to-wall with a ball of mud in our hands. Also, if we don’t know what the program is doing, what are we abstracting anyway? Anything we imagine at first will likely be wrong and we’ll just end up refactoring it later. I’ve found that its good to have abstract concepts in mind, but to start writing code with something more concrete and refactor to more generalized code. Anthony Ferrara has a great blog post about a technique he calls DIRTI code. Your initial abstractions should be very close to the problem. If you’re starting with
Thing2 inherit from
AbstractThing and are created by a
ThingFactory; that’s all very clever, but you’re doing too much.
I watched a talk recently about architecture by Robert C. Martin (Uncle Bob). He said that when he asks developers what their architecture is for a system, he often gets responses that are lists of tools, e.g., Ruby on Rails, MySQL, HTML, etc. He says this is not an architecture, these are tools. He says that a good architect focuses on use cases, and defers decisions as much as possible. He says, “A good architecture maximizes the number of decisions not made” and, “A good architecture allows you to push aside decisions, making them as irrelevant as possible, for as long as possible”.
He was talking about software tools and frameworks, but I think these principles apply to design tools as well. Who cares if the class you dreamed up has a single responsibility, uses inheritance or composition, extends an abstract class or implements an interface. At this point, if your class is just a thought in your head or a UML diagram, then it’s more than likely wrong, as you’re bound to find out during development. These are design decisions, and they are decisions we don’t want to make at the start of a project. The only thing we want to decide is what our program does in the context in which it is used. What does the client want to do? Does it include the factory pattern, or observer pattern? Who cares? Do you really think your upfront design decision is going to survive the first attempt to code it? Your upfront design is nothing more than speculation. Maybe over time you can get better at speculating, but the truth is “no plan survives contact with the enemy”. Your code may resemble your initial design, but its likely going to be refactored, and if you’re inexperienced, the refactoring is more than likely to be drastic. Does it do what it’s supposed to do? That’s what matters. Make it work first, then refactor it. If you abstract it first, you’re going to refactor it anyway, except its not going to work because all you have are a bunch of confusing abstractions that don’t accurately represent the problem domain.
This One Is Mine
So I decided I need to get back to actually programming and not just thinking and designing; solving the problem at hand, and not solving imaginary problems. Mostly, I need to write code that works, guarantee it works, and then build on that working code. Then I need to cleanup and abstract my code later when I actually understand it. For the more experienced reader, you’ll recognize what I just described as test-driven development with unit tests. As I said before, most of my projects were too small to bother with testing, but this chess program (each time I’ve tried to build it) quickly grows large. My mess of invented abstractions naturally becomes too confusing to reason about. My code has unreasonable dependencies (as in, I can’t reason why x depends on y; what was the purpose of y again?). Furthermore, I have no way to guarantee whether anything actually works.
I began studying unit testing and test-driven development. After my introduction to the practices I think I can safely say that this is something novices should study immediately after having a foundation in good design practices. I realized too, that I had actually stumbled upon this practice myself in the past and didn’t realize it. Many people probably have. In the past I would write short snippets of code to test the output of something, and when I knew it was working I would delete the snippet and move on writing my program. It just never occurred to me to keep these silly tests and use them as a contract to guarantee that all of my code works as expected. Furthermore, TDD using unit tests helps you focus your code on the problems you need to solve, and focus on writing the code from the perspective of the client. TDD elicits what the pros call emergent design through refactoring.
I find the biggest effect of TDD is it allows you to zoom in close to a problem. If you look at a material from far away it seems smooth, but if you look at it under a microscope you can see its surface has many tiny bumps, cracks and weak spots. Maybe you could see a point that if you bent it, the material would easily break giving you two independent pieces. Looking at your application from too far away has a similar effect. It appears smooth and rigid, and it’s difficult to see the parts that can bend, the orthogonal parts, the parts that change independently from each other. Looking closely, focusing on each unit with the “TDD microscope”, and making incremental changes gives you the chance to the the cracks so you can bend them and break your code apart into independent pieces.
I don’t know why testing wasn’t more obvious to me. I’m testing and refactoring this very post as I’m writing it. I’m writing this from the client’s perspective (a reader interested in programming and TDD). I’m writing my thoughts first, and then going back to refactor what I wrote to make it more readable and organized. Face-palm much?
Some helpful resources I found on my introductory TDD journey:
- Test-Driven Development By Example by Kent Beck is a great book to get introduced to TDD.
- Roy Osherove is a proponent of TDD and has a lot of resources on his site. He did a video series where he pair programmed a Go game. It was very helpful to see the TDD process first hand.
- I found another good video walkthrough of TDD by James Shore. Again, very good to see the process from an experienced developer.
- Erik Dietrich’s blog is a great blog for just about anything related to writing clean code. His blog includes posts about TDD, and incredibly insightful posts behind much of the advice and best practices like SOLID principles and refactoring. He often includes interesting code samples and walks readers through refactorings. He’s also doing a short series on TDD and modeling a chess game.