Maintainable code. We all want it. We all try to write it. We all suck at it.
When was the last time you were asked to work on some legacy code and found it pleasant? Yeah, me either! Well, actually, that's not entirely true. Where I work, we have standard architectures, processes and coding practices that we adhere to. I find that with very few exceptions, any time I have to go into apps written here, I have a relatively easy time of it. It's when I have to work on code written outside the company that things get unpleasant. Even when the developer uses standard patterns and practices, working my way through the labyrinth of code is like an Odyssey. Every coder has their own style. Every app is so very different!
What's so different about the code written where I work? I know what to expect. I know where to find things. The code looks familiar. It takes minutes, not hours or days to get it built and running. The code is relatively flat and I'm in and out pretty quickly.
“The rabbit-hole went straight on like a tunnel for some way, and then dipped suddenly down, so suddenly that Alice had not a moment to think about stopping herself before she found herself falling down a very deep well.”
From Lewis Carroll's “Alice's Adventures in Wonderland”
I recently had the pleasure of making a change to some code written a couple of years ago by another company. The code did some screen scraping from a website on a repeating schedule, an ugly task no matter how well it's coded. The website was changed one day without notice and the work stopped getting done. Every hour the app wasn't working made the problem bigger and bigger. We didn't have much time. I was lucky in that I had previously installed and built the source code a few months back and configured it for testing. That had taken a couple of very long days because the system was entirely over-engineered and had a LOT of working parts and interactions.
We found the source code in the more than two dozen projects by searching for some of the screen scraping keywords. We understood the problem and how we were going to fix it. There was a single method where the failure occurred. The code was easy enough to fix, but we would have to add a couple of parameters to the method signature.
That's when the rabbit hole dipped suddenly down. A quick look at all the places the method was called from and in turn all of the methods those methods were called from (thanks strong typing!) revealed that our code changes would touch 23 different methods in seven different classes and many of the code changes would be substantial. Furthermore, calling methods used this code for several different purposes. It was one generic method, re-used and re-cycled. Our minor code change had spider-webbed into a major change that would require substantial testing in several areas of the application.
Ever optimistic, the new parameters we needed were readily available in all but two of those methods. In fact, in almost every case, the existing parameters were picked out of a class that also contained the values we needed for the new parameters. In many cases, this was all we needed to do. In some cases, we found out that the class being used wasn't fully populated. Some of the values we needed were missing. The developers had simply re-used and re-cycled this existing class because it contained the properties they needed. Instead of creating a new, purpose-built class, they had re-used something that was close to what they needed. Of course, this isn't something you can easily find out until run-time. Once our solution was complete, we ran a couple of test runs against the live site (there was no test system or sandbox we could use). And that's when we found out about the missing values. More code, more testing.
We did make the fix and get it into production, but it wasn't at all easy. Not all of the issues we ran into could have been alleviated completely, but this could have gone a whole lot better and a whole lot faster. Big, flashy red flags went up when we realized that it takes about two days to configure a development environment and each time the app is deployed to a new server, but that's a topic for a different day.
Deep inheritance hierarchies were the second red flag. The partially filled class we found inherited a base class (which inherited from, which inherited from…) as well as implementing several interfaces. The class had obviously become a catch-all for lots of different purposes and in no case could we see that any process used all of what was in that class. The class was widely used, not just in the methods we would be working with and had to have all of that inheritance in order to function as it did. We were stuck with this class and all of its oddities.
Deep call stacks were the third red flag. The more we worked with the code, the more we saw very deep call stacks. Everything that was done in the system seemed to drill through dozens of methods. It was easy to get lost in a debugging session trying to remember what we had started out to do. In addition, every change rippled through the call stack impacting a lot of code. Sometimes that's a good thing. In this case, the app didn't do all that much.
In this and many other cases, the word “deep” keeps popping up on the What Went Wrong side of the ledger. Deep (and wide) inheritance hierarchies and deep call stacks especially. The very word deep implies that changes can (and usually do) ripple through every one of those layers.
One of the key differences I've discovered is that the systems we create in-house tend to be quite flat. Things tend to be purpose-built. These systems use certain building blocks, but rarely, rarely, rarely is there more than one level of inheritance. Most classes inherit directly from Object (the .NET default). Interfaces are used sparingly except as contracts for services. If we find an existing class that is close to what we need, but not exactly what we need, we create a new class that's exactly what we need. In return, I find that the maintenance phase of the system's life is much more straightforward and that fallout from changes tend to be far smaller and more contained. Yes, I've had to make the same change in more than one place on occasion. On the other hand, I know those changes won't snowball through the system. It's the reason we're all scared to touch the code in the “framework” or “foundation” classes of our mature systems. We created that code with the noble goal of putting that code in one place where we could maintain and improve it easily. But now we're far too afraid to touch it because every change could mean drastic and far-reaching consequences.
Whatever your beliefs, my experience is that flatter and more purpose-built code is easier to understand and maintain.