One of the continuing problems of software design is managing complexity in large programs. The problem is easily defined: as a program grows, the number of interactions between code objects within a program grows at an ever-increasing rate, creating a maintenance nightmare. While defining the problem is easy, solving the problem has proven to be extremely difficult.
There have been numerous white papers devoted to the subject, new programming languages created, C++ for example, notations developed and guidelines created. All have a particular take on the problem, and all address certain aspects of the problem, but as of yet, there is no all-inclusive solution to the problem, and one may not exist.
I don’t pretend to have the answer to this problem, but I have discovered techniques that help to mitigate the problem and ease the burden of tracking down bugs and minimizing the effect of code changes. When I made my living as a programmer, I had to write large, enterprise level programs, and I learned that taking the time up-front to organize code in an orderly fashion helped in managing the complexity that is bound to creep into any large-scale program. Here are some of the lessons I learned, most the hard way.
Define the Problem
Before you start to code, think about what problem you are trying to solve. All computer programs should solve one or more problems. For example, an electronic address book may solve the problem of storing names and addresses that can be quickly retrieved. Strange as it sounds, even games should be designed to solve a problem.
For example, in Unreal Tournament 2004, you have a game type called Capture the Flag. The problem definition is actually simple: move to the opposing team base, grab their flag and take it back to your base. In a chess game, the problem to be solved is to capture the enemy King. Once you clearly define the problem, you can then create a set of action steps to solve the problem.
Have a Plan
The problem to be solved is the goal of your program. In the Capture the Flag (CTF) problem, once you are able to grab a flag and take it back to your base, you have achieved the goal of the program. The task is to define how that goal is achieved. One thing I like to do is to work backwards from the problem, breaking
down the program into ever-smaller components and then tackle those components individually.
Let’s look at the CTF problem. There are basically 3 pairs of components: two different flags, two different bases and two different teams. The flags can be broken down into red flag and blue flag. The bases can be broken down into red base and blue base. The teams can be broken down into red team and blue team.
Since CTF is a symmetrical game, we really only need to define one side of the equation, a flag, a base and a team, and that definition could be used for both the red and blue sides of the game.
Let’s define a flag. A flag is an object that resides in a base and is manipulated by a team member. Using this definition we can write down some preliminary action steps that need to be addressed when coding a flag.
1. A flag is an object, so we need to have a model of the flag.
2. A flag resides in a base, so we need some place to put the flag.
3. A flag can be manipulated so a flag must have some properties.
This is a good start, but it is too general, so we need to refine the action steps.
Looking at 1 above, a flag is a model so we will need to have a base image of the flag. Since there are two flags, red and blue, we will need to have two texture sets for the flag. So for action step 1, we will need to create an untextured model of the flag and create two texture sets for the flag. For the red flag, we will paint the red texture set on the model, for the blue flag, we will paint the blue texture set.
At this point we have enough information to implement in code, a flag object. We have no place to put it and can’t interact with it, but we can create the object. Notice the progression of definitions: we defined the problem in general, then restated the problem in specific problem components, and then redefined those
components into even smaller, solvable problems. Going through the same process with each aspect of the CTF game will yield a list of small action items that can be directly translated into code.
The next step is to create the code, and many times this is where the programmer fails. It isn’t simply a matter of jumping in and coding the action steps, the coding has to be done intelligently. Look at the action steps and find the commonality.
For example, displaying a flag and displaying a team member,
involve similar techniques. They both have a model and texture set. It doesn’t make sense to have two routines, one to load a flag and one to load a team member. It would better to have one routine that does both.
The rule is, if you do have to do something twice, write the code once. Look through all the action steps and group the common functionality into general routines that can be called from different action steps. In other words, you write one routine to load a model, regardless if the model is a flag, player character or weapon. By using one routine instead of three, you have reduced the complexity of the program.
If there is a bug in the model loading code, then you only have to fix one routine, not three. And, more importantly, if you need to make a change, you only have to change one routine, automatically reducing the risk of introducing a bug into the system. One routine to fix, one place to look for bugs.
The next step is to organize the code in an intelligent manner. Don’t put everything in one huge source file; break up the source file into logical groups. Routines that manage the models should be placed into a single file. Routines that manage textures should be placed into another file.
Non-specific routines, a random number generator, for example, should be in yet another file. The same principle that we used to define a problem should also be used to organize the source code. Group the code into small, logical groups so that if something breaks, you know where to look and if something needs to be changed, you can do it in one place.
Document the Code
For small programs, documentation usually isn’t necessary. For large programs, it is vital. Let’s say your program works for six months, and suddenly one user does something that no one has ever done before and your program quits working. When you go back to the code after not looking at it for six months, are
you sure that you can remember what you were really
doing? Some people can, I for one cannot.
Taking a few moments to write a note about the purpose of a
routine, the input and output expected and the expected results can save hours when it comes time to debug that long-forgotten routine.
As I said, this isn’t a grand solution to the problem of program complexity. However, I found that following these steps greatly reduces the time and effort needed to produce and maintain quality beyond a simple one-page program. The important point is to have a method, and then stick to that method. We will never solve the problem of complexity, but we can at least get a handle on it and make it somewhat more manageable.