How to Write Maintenance-Free Software
Bruno Campolo
January 6, 2010
At my job as a full-stack developer I encounter a ton of code that is not only hard to read and understand, but is very high maintenance as well. It doesn't matter how quickly you can write code if it's bad code, because over the lifespan of the application/service you are probably going to spend way more time debugging/fixing code than you spent writing it in the first place. In this post I hope to give you a few tips that could drastically increase the readability of your code and decrease your project's short and long term maintenance.
Standard Techniques
-
Object Oriented Programming (OOP)
There are a lot of OOP haters out there these days, but concepts such as encapsulation, cohesion, and coupling are key concepts to understand. These concepts have been around since the 60s, and widely used since the 90s so despite what your opinions about OOP are you will still find tons of existing OOP code out there and you will need to know how to manage it.
-
Tight Encapsulation
This is a way to separate the public, potentially user-facing interface from the internal implementation of an object. This is achieved by using different access modifiers such as public, private, etc. It is very common to find public variables in an object that get accessed by other objects... this is bad. If you then need to add new variables or change the way an object works internally (and you will), it creates a ripple effect and you now need to update all of the code for each of the objects that access the changed object. The objective with encapsulation is to develop a stable public API for a particular class such that you can change the internal workings of that class (private methods and variables) without having to update the public interface. What this accomplishes is that now you don't have to update any of the other objects that are calling into the changed object... this equates to lower maintenance.
-
High Cohesion
This basically means that all of the responsibilities (methods) of a particular class are very closely related to each other. The more focused a class's purpose is, the less it will need to be maintained and the more reusable it will be. For instance, if you had a Computer class, it could have a turnOn method and a compute method, but it shouldn't have a print method. Instead you would have a Printer class that has a print method that the Computer object may call.
-
Loose Coupling
This means that one class should not know too much about another class, more specifically, class A should not know anything about the implementation of class B. This is achieved through tight encapsulation and good API design. Class A should only know what class B wants the public to know about it. Think of each class as strangers to each other class... you only give access to things (public methods) that you want the stranger to have access to and you never show the stranger your privates :shock: When classes are too 'friendly' with each other (exposing things that should be private) it causes the same kind of ripple effect mentioned above.
-
Class/Variable/Method Naming
Names that you use in your software should clearly reflect their purpose and for the love of god, please don't abbreviate unless its a well known acronym. ssPos could be anything from space ship position to seriously stupid piece of shit... ok, its probably not that, but you get the point. spaceShipPosition would be a more appropriate name. You would know exactly what it was when you look at it a month/year from now. Some argue that the long length of these variable names will slow down your coding process... it wont... especially with most IDEs having some form of code completion these days. Class names starting with an uppercase letter and variables/methods starting with a lower case letter and both continuing with camelCase is quickly becoming the widely accepted standard for many languates, but refer to your favorite language's style guide. The main take away is to be consistent. Don't use ufoCount in one place and alien_count or BulletCount in another.
-
Don't Hard-Code
It's very easy while programming to start 'hard-coding' values directly into your code. For instance, you have an condition that looks like 'if(userCount 100)' in 3 or 4 places. When in reality it should be 'if(userCount MAX_USER_THRESHOLD)' and setting a constant called MAX_USER_THRESHOLD = 100. Now if you want to allow more users, you simply update the constant value (or even better, put this in a config file as mentioned below) instead of updating it everywhere and potentially missing one. Your code shouldn't contain any hard-coded values unless they are what I call 'universal truths'. For instance, 'if(userCount 0)', there's no need to replace the number 0 with a constant called ZERO_USERS. 0 is 0... its never going to change. This includes array sizes too. You should try not to make your arrays a constant size unless the size is dynamically created. Instead you should try to use objects that can expand as entries are added (in Java - ArrayList, etc). If for some reason you need to use a fixed size array you can then create one by using the size of the list (in Java - 'new String[arrayList.size()];')
-
Commenting
We all hate to do it, but you would be surprised at how foreign your code looks a year from now (even less time if you don't follow some of the stuff mentioned above). I'm guilty of leaving my code comment-free from time to time, but I try to comment anything thats not immediately obvious to me. Also, make sure your comments clearly describe what is happening, not just '//do stuff'.
-
Refactoring
We all have deadlines and time crunches to worry about so there are going to be times when you need to write a quick and dirty piece of code... it works, but it irks you to your core (the good programmers out there know what I'm talking about). When you write code like this, mark it somehow (for instance, Eclipse/Java uses FIXME comments) and this will be an indicator that you need to come back to it when you have more time. When you do come back to it, write it in a nice low maintenance way and test it to make sure it does the same thing that the old code did. The more of these little bits of ugly code you leave in, the harder to maintain the software becomes!
Advanced Techniques
-
Configuration Files
There are times when you have a ton of small little pieces of code that do pretty much the same thing except for small differences here and there. Extract out the commonalities and write one piece of code that does the common parts for everything, then extract out the differences and put them in some form of configuration file (I prefer XML). Now when the piece of code that is common runs, it can read from the configuration file to act on any differences. An easy example is if you often have to change a hard-coded start/end date in your code in 4 different places, put the start/end dates in a config file and add code in those four places to read from the config file (which can be cached for speed). This cuts down the number of places from 4 to 1 and removes the need to recompile. The down side to using config files is that most of the bugs you find will end up being typos in the config file. Also, don't go crazy and extract everything otherwise you will end up writing your own scripting language.
-
Reflection
This is a fairly new concept to most programmers, but this allows objects/variables/methods to be used/called just by knowing the name/signature of it at run-time (as opposed to design-time when you would normally do this). This combined with the Configuration Files section above could cut down on code drastically and virtually eliminate needing to update/compile your code. For instance, you write a game that has user definable player characters (different look, different actions, etc). Using the above mentioned techniques you could let the user create a config file that describes the player. This config file could have one entry for a jpeg file for the character's look and another entry that has a class/method name for the character's action. Now using reflection you could actually create the class that the user wrote and call their method... Nice! Most newer languages support reflection and even a lot of scripting languages used in game engines support it (but probably don't call it reflection). Its a powerful thing to have in your mental toolbox.
-
Inversion of Control (IoC)
This concept basically entails the reusable parts of your code calling your customized code instead of what is traditionally the other way around. I can't really do the subject justice, so you can read more about it here: http://en.wikipedia.org/wiki/Inversion_of_control. What I can say is that it will increase the reusability and extensibility of your code quite a bit. The most common forms of this are Factory Patterns and Dependency Injection. This paradigm will make your software easier to unit test and easier to replace modules. This should replace the need for static classes and singletons (for the most part).
-
Test Driven Development (TDD)
I'm not a fan of TDD in general, especially when you are writing something new. How can you possibly know what the tests should look like when you probably don't even know what the product/service/function is supposed to do? In the real world requirements are vague or sometimes non-existent. Try TDD in this environment and I can guarantee you'll be rewriting most of those tests. To make things worse, most unit testing of complex systems requires 'mocks' which IMO require the test to know far too much about the implementation than it should. This makes tests brittle and means the tests will need to be updated whenever the implementation changes, EVEN IF THE API AND BEHAVIOR DIDN'T CHANGE! There is however one scenario that I like to use TDD: When a bug is reported I like to write a unit test which exposes the bug. This unit test should reproduce the bug and will fail until the bug is fixed. This should be part of the same commit that fixes the bug.
Mindset Shift
-
I Never Want to Touch This Code Again
Writing low-maintenance software requires a mindset shift. Every time you design a piece of software or write a line of code, you need to be thinking, "Have I designed/written this in a way that will require future changes?". If you're answer is something like, "Yeah, I'll need to change this whenever the PayPal API changes or whenever the total user count exceeds 1000 or whenever I write a new mission, etc", then take a step back and figure out how to accomplish the same functionality in a way that doesn't need to be changed in the same scenario. Rinse and repeat until you've removed all of your maintenance problem areas. Think to yourself, "I never want to touch this code again!" You may not be able to get rid of all maintenance areas, but using this process you should be able to minimize future maintenance.
-
Make the Time
It's hard to get into this mindset because initially you'll be thinking, "I'm already behind schedule, I don't have time to rewrite that code." See the refactoring section above. You need to get past this short-sighted attitude and make the time. In the long run you will reap the benefit of this extra effort many times over. I keep a fortune cookie message on my desk as a reminder. It reads, "The time is always right to do what is right". I'm sure you can interpret this in many philisophical ways, but I choose to think of it as "Don't take shortcuts. Write the code the best way you know how the first time even if it takes a bit longer, because you will likely move on to some new task and never come back to this piece of code again (until it bites you in the ass)."
-
Be a Rockstar
This mindset is something that separates the superstar developers from the button-mashing, copy-and-paste, Chat GPT coders. The idea here is to learn everything you can about the product/service/code you are touching. Learn it inside and out until you are the expert. It may take months or even years. Adopt this mindset as a way of life and be proud of your maintenance-free code!
Hopefully the above tips have shifted your thought process in the right direction and I won't have to look at code like this from you:
ElpsdTm(int nElpsdTm) {
Tmp = nElpsdTm;
ElpsdTm = 500;
MsgWTm(nElpsdTm);
}