How to Turn Ordinary Code into an Unmaintainable Masterpiece
There are many ways to write software that works. There are just as many ways to ensure that nobody—including you—can understand, modify, or safely extend it later.
The most effective techniques are not necessarily dramatic. A slightly misleading name, an unexplained unit, a global variable initialized in an unrelated function, or one more layer of abstraction can be enough. Applied consistently, these small decisions can turn a manageable program into an archaeological site.
If builders built buildings the way programmers write programs, then the first woodpecker that came along would destroy civilization.
—Gerald Weinberg, Weinberg’s Second Law
What follows is a deliberately mischievous catalogue of such techniques. It is a guide to writing code that may compile today and still frustrate everyone who has to maintain it tomorrow.
Give every name a chance to mislead
A good name tells the next programmer what a thing is and why it exists. To achieve the opposite, use names that are short, vague, inconsistent, or needlessly difficult to search for.
- Choose names that are easy to type:
Fred,asdf, and anything else that saves a few keystrokes. - Use single-letter variables everywhere:
a,b,c,x,y, andz. When those run out, continue witha1,a2, anda3. - Misspell identifiers creatively.
SetPintleOpeningandSetPintalClosinglook close enough to be confusing, but different enough to make code searches unreliable. - Prefer descriptions so abstract that they convey nothing:
ProcessData,DoIt, andGetDataare especially useful. - Replace meaningful names with abbreviations. Acronyms such as
WTFandRTFSCare excellent; language-specific shorthand and private slang work just as well. - Scatter capital letters at random:
gEtnuMbERis much harder to read than eithergetNumberorGET_NUMBER. - Reuse the same variable name in nested blocks. It gives every reader the pleasant uncertainty of having to determine which instance is currently in scope.
- Use characters that merely resemble familiar keywords.
int íntlooks almost right while declaring something else entirely. - Rely on
_,__, and___as identifiers. - Mix languages. English, German, and romanized Chinese can coexist in the same module, preferably without any convention explaining the choice.
- Name things after punctuation:
slash,asterix, andcommaare more entertaining than names describing their purpose. - Pick unrelated words such as
god,superman, oriloveu. - Confuse lowercase
lwith the number1. If a font makes the distinction difficult, that is a feature rather than a defect.
Hide the meaning in plain sight
The next step is to make the source code and the behavior tell different stories. Comments, macros, formatting, and interfaces are all useful camouflage.
Interleave comments with code
A comment can be placed directly in the middle of an expression or loop so that neither the code nor the comment is easy to follow:
for(j=0; j<array_len; j+ =8)
{
total += array[j+0 ];
total += array[j+1 ];
total += array[j+2 ]; /* Main body of
total += array[j+3]; * loop is unrolled
total += array[j+4]; * for greater speed.
total += array[j+5 ]; */
total += array[j+6 ];
total += array[j+7 ];
}
Conceal macros and line breaks
Hide a macro behind something that looks like an ordinary assignment:
#define a\=b a\=0-b
A reader seeing a=b is unlikely to suspect that the expression is being transformed by the preprocessor.
Break identifiers across physical lines. Searching for xy_z becomes needlessly difficult when it is written this way:
#define local_var xy\
_z // local_var OK
Make the interface disagree with the implementation
If the screen says postal code, call the corresponding variable zipcode. The more familiar the visible label, the more confusing the internal name will seem.
You can also pass a global variable into a function as though it were an ordinary parameter. This makes the function look self-contained while concealing the state it really depends on.
Another useful method is to create a chain of aliases across unrelated files:
#define xxx global_var // in file std.h
#define xy_z xxx // in file ..\other\substd.h
#define local_var xy_z // in file ..\codestd\inst.h
Make similar things look different—and different things look similar
Use near-duplicate names such as swimmer and swimner, or characters that are visually difficult to distinguish: ilI1| and oO08 are particularly effective. parselnt and parseInt, or D0Calc and DOCalc, can keep a maintainer occupied for hours.
For maximum effect, create a family such as xy_Z, xy__z, _xy_z, _xyz, XY_Z, xY_z, and Xy_z.
Overload a function name with implementations that have no meaningful relationship to one another. Readers will assume that the shared name implies shared behavior and will be wrong.
Operator overloading can take the confusion further. For example, overload ! on a class so that it does not mean logical negation at all, but instead returns an integer. Then !! first invokes the class’s overloaded operator and applies logical negation to the integer it returns. Add a third !, and the expression becomes an experiment rather than a readable statement.
Let the preprocessor change history
Preprocessor directives can make the same header declare a different function depending on how many times it has been included:
#ifndef DONE
#ifdef TWICE
// put stuff here to declare 3rd time around
void g(char* str);
#define DONE
#else // TWICE
#ifdef ONCE
// put stuff here to declare 2nd time around<
void g(void* str);
#define TWICE
#else // ONCE
// put stuff here to declare 1st time around
void g(std::string str);
#define ONCE
#endif // ONCE
#endif // TWICE
#endif // DONE
A header that changes its function signature according to inclusion history is a particularly elegant way to make compilation depend on invisible context.
Treat documentation as another source of bugs
Comments and design documents can clarify a system. They can also become a second, contradictory implementation.
- Leave comments unchanged when the code changes. This is how documentation can lie without anyone having to write an actual lie.
- Comment the obvious:
/* add 1 to i */is a reliable way to use words without explaining anything important. - Describe what the code does, but never why it does it.
- Keep the dangerous assumptions secret. If a flight system requires a new flight to be registered in more than twenty-five separate places, do not mention that dependency anywhere.
- When designing a complicated algorithm, document every detail in exhausting depth. Use more than five levels of headings and hundreds of numbered sections, such as
1.2.4.6.3.13 – Display all impacts for activity where selected mitigations can apply (short pseudocode omitted). Then name the corresponding functionAct1_2_4_6_3_13()so the implementation and the document can be matched perfectly—and understood by almost nobody. - Never document units. Is a duration measured in seconds or milliseconds? Is a size in pixels or inches? Is a quantity in megabytes or kilobytes? Leave all of that implicit. Mixing units in the same codebase makes the result even stronger.
- Do not document traps, edge cases, or other gotchas. A hidden hazard is much more durable than an explained one.
- Use comments and design documents to express frustration. Future maintainers will then have to decode both the program and the author’s mood.
Design for maximum dependency
Good design hides unnecessary details and keeps change localized. To reverse that principle, expose everything, duplicate everything, and make every component depend on every other component.
Cast everything back and forth
Java casts are extremely useful for this purpose. Every time an object comes out of a collection, cast it back to what it used to be. Repeat the operation throughout the codebase. If the underlying type changes later, some casts will fail at compile time, some at runtime, and some may survive long enough to become someone else’s problem.
Take advantage of Java’s redundancy. Tiny differences such as:
Bubblegum b = new Bubblegom();
swimmer = swimner + 1;
make it easy to confuse a class name, a variable, or a spelling error with the intended identifier.
Never validate anything
Do not validate input. Do not check return values. Trust the hardware, the operating system, the network, and every other programmer completely. This eliminates tedious error handling and demonstrates admirable confidence in the surrounding environment.
Do not encapsulate implementation details. Callers should be required to know how every called function works internally.
Copy instead of understanding
Copy and paste is an efficient way to produce code without learning what the original code does. Duplicate a block whenever you need similar behavior. If a bug exists in the original, duplication ensures that the bug can mature in several locations at once.
Build one enormous listener
Create a single listener and attach it to every button class. Put a long sequence of if...else branches inside it to distinguish which button triggered the event. This centralizes unrelated behavior while making local changes risky.
Increase dimensions and layers
Use three-dimensional arrays. If three dimensions are not enough, add a fourth. The more indices a developer must track, the less likely it is that anyone will understand the data structure at a glance.
Mix access styles. Use getters and setters in some places, then access the same public field directly elsewhere. The result will frustrate both people who expect encapsulation and people who expect direct access.
Wrap everything repeatedly. Put each API behind six or eight layers of wrappers, with at least four levels of similar-looking abstractions. Create several wrappers that appear to offer the same function while behaving just differently enough to prevent safe substitution.
Make all state public
Declare every member public. This makes future restriction difficult, increases coupling, and allows unrelated code to depend on internal details. Once enough code relies on those details, the original implementation becomes almost impossible to replace.
Rearrange arguments and meanings
Change drawRectangle(height, width) to drawRectangle(width, height). After several releases, change it back. Maintainers will have to determine which version is correct by examining call sites, history, and perhaps the output of the program.
If a method is called setAlignment(int alignment), replace it with several methods such as setLeftAlignment, setRightAlignment, and setCenterAlignment. Encoding the value in the method name creates more surface area and makes future additions more cumbersome.
Keep everything forever
Practice packratting: retain unused variables, obsolete methods, old branches, abandoned classes, and stale code. Removing dead material might make the program clearer, so it is best preserved in case somebody someday needs to guess why it was there.
Make every class final, including all child classes. Once the project is complete, nobody will be able to extend the design through inheritance. The fact that java.lang.String works this way is convenient evidence that the practice must always be appropriate.
Avoid interfaces. In Java, treat interfaces as unnecessary bureaucracy; in C++, rely on virtual functions while keeping the design as difficult to substitute as possible.
Fight the layout system
Avoid layout managers and use absolute coordinates instead. If a manager insists on a layout, choose GridBagLayout and hard-code the grid coordinates. A user interface built this way can be made to fail gracefully whenever a window, font, or platform changes.
Let the environment control object construction
When the application needs environment variables—getenv() in C++ or System.getProperty() in Java—use them during member initialization rather than in the constructor. This ensures that object state depends on hidden external conditions before the object has been properly established.
Use magic numbers wherever possible. A value such as 17, 6291, or 1000 is far more mysterious in context than a named constant, especially when the same number means different things in different modules.
Scatter global state
Global variables reduce the number of parameters, which is convenient. To make them dangerous, initialize each one in a different function, including functions unrelated to the variable’s apparent purpose. Maintainers can then investigate initialization order as if they were solving a mystery.
Configuration files should use names that do not match the names in the program. A setting called postal_code in the configuration can become zipValue, areaText, or something else entirely after it enters the code.
Make classes enormous
Put every conceivable operation into one bloated class. Even if the class supports only one practical case, provide methods for every possible variation. Avoid creating supporting classes; keeping all behavior in one place makes the class both harder to test and harder to understand.
Inheritance offers another path to confusion. If a class has ten members and methods, create ten levels of subclasses and distribute those properties across the hierarchy. If possible, place each of the ten classes in a different file. A simple object will then require a guided tour through the entire project.
Make straightforward code look mysterious
When names and architecture are not enough, obscure the syntax itself.
XML is especially useful. A task that could have taken ten lines can often be expanded to one hundred lines of tags, attributes, schemas, and configuration. Once one component uses XML, require every other component to use it too.
For C code, mix numeric bases. 10 and 010 are not the same value in every context. An array can become difficult to inspect when written like this:
array = new int[]{ 111, 120, 013, 121,};
Use void* whenever possible, then cast it to different types in different locations. In C++, implicit conversions are equally helpful: constructors can perform type changes without making the conversion obvious at the call site.
Rewrite simple conditions into complicated equivalents. Instead of a==100, use a>99 && a<101. The meaning is nearly the same, but the intention is less immediate.
Make semicolons do the work of structure:
if ( a );else;{ int d; d = c;}
Use indirect conversions. To turn a double into a string, write new Double(d).toString() rather than the more direct Double.toString(d).
Nest aggressively. More than ten pairs of parentheses on one line is a good start. More than twenty levels of braces inside one function is better. Replacing nested if...else blocks with chains of conditional expressions can make the result even more compact and difficult to inspect.
Exploit C’s array notation
C allows several equivalent ways to express array access. myArray[i] can become *(myArray + i), or *(i + myArray), which is equivalent to i[myArray]. The less familiar the form, the better.
The same spirit can be applied to function calls. Given:
int myfunc(int q, int p) { return p%q; }
a call can be written as:
myfunc(6291, 8)[Array];
Stretch every line and every branch
Make each source line as long as possible so that readers have to move back and forth across the screen. Avoid early return, break, and goto; force error handling through at least five levels of if...else instead.
Do not use braces with if and else. This is particularly effective when indentation is inconsistent. Experienced programmers can then be caught by a statement that belongs to a different branch than it appears to belong to.
Macros are among the most reliable tools for obscuring C and C++ code. They can change expressions before the compiler sees them, conceal control flow, and make the apparent source differ from the executed program.
Use trivial wrappers as well. Encapsulate a single bool in a class whose only purpose is to contain that boolean. A small, obvious value can thereby acquire methods, constructors, and a lifetime of unnecessary interpretation.
Make loops needlessly clever
Avoid the familiar form:
for(int i=0; i<n; i++)
Use while instead. Swap the roles of n and i, change < to <=, and adjust the step with i--. The loop may still work, but readers will have to reconstruct its starting point, stopping condition, and direction before they can safely touch it.
Testing is for people who lack confidence
Never test error handling. Never inspect the return value of a system call. If a failure occurs, let it travel through several layers until the visible symptom has no obvious connection to the original cause.
Never perform performance testing. If the program is slow, tell users to buy faster machines. Measuring performance might reveal that the algorithm needs to change, or worse, that the architecture needs to be redesigned.
Do not write test cases. Avoid automated tests and code coverage. Tests could expose assumptions, regressions, and boundary conditions before release, which would eliminate many opportunities for future investigation.
Testing can even be dismissed as cowardice. A truly fearless programmer, according to this philosophy, does not need to test code. Concerns about managers, employment, user complaints, or legal consequences merely interfere with productivity. If you have complete confidence in your work, why would you need evidence?
A few organizational techniques
Technical decisions alone are not enough. To preserve unmaintainable code, make sure the surrounding organization helps protect it.
Assume the manager knows everything
Regardless of how ill-informed a manager may be, follow every instruction literally. This is an excellent way to learn new and inventive techniques for making software impossible to maintain.
Keep support away from the truth
Do not tell the maintenance team about bugs in your program. When users call or send email, ignore them if possible. If a response is unavoidable, ask them to reinstall the system or suggest that the problem is with their account. This keeps the development team insulated from inconvenient evidence.
Keep quiet about catastrophic defects
For a major defect—something on the scale of a date-related failure—say nothing. Tell nobody: not family, friends, colleagues, managers, or the people responsible for the system. When the problem finally appears, the resulting emergency may create opportunities that would not exist if it had been disclosed earlier.
Put impressive labels on ordinary confusion
A poor implementation can be given a grand name. Attach a Gang of Four design-pattern label to messy code. Call a failing project “agile.” With enough confident terminology, a design can sound sophisticated even when nobody can explain how it works.
The performance is not complete until the whole team, the company, and perhaps the wider industry are excited and confused by the vocabulary. Once everyone is discussing labels instead of behavior, the path to unmaintainable code is clear.
The guiding motto is simple:
If builders built buildings the way programmers write programs, then the first woodpecker that came along would destroy civilization.