[ Home | Resume | Programming | Engineering Philosophy | Family ]

Hackery

Development time has become such an important priority that it has blinded people to other important considerations, even to considerations that ultimately affect development time.
— Steve McConnell, Rapid Development

A hack is defined as any action that appears to effect rapid progress toward immediate goals, but entails a substantial probability of negative consequences that outweigh the benefits.

Hacks are related to AntiPatterns . For example, they tend to have similar root causes. However, a given hack may not occur commonly enough to be called an AntiPattern, and a given AntiPattern may never result in apparent progress.

Examples

Here are a bunch of examples of the most common categories of hacks:

Relying on Unspecified Properties

When you rely on unspecified properties, you put yourself in jeopardy of those properties being violated. When that happens, then the best case is the Vendor Lock-In AntiPattern or the Dead End AntiPattern. However, in the future you may discover cases in which a presumed property of the service turns out to be violated. Avoiding such cases may then require further hacks.

Exploiting unintended properties of a specification is just as bad as relying on unspecified properties. For example, it used to be legal to say "#define private public" in C++, even though this clearly subverts the language. One should always assume that such shenanigans will be deprecated. Of course, relying on properties that are neither specified nor intended is just asking for trouble.

Not Specifying Guaranteed Properties

By failing to specify the properties of your subsystem, you force the subsystem's clients to rely on unspecified properties, which is likely to lead to problems. The implementation should never be considered the specification, because then it is impossible to improve the implementation without violating the specification.

Another flavor of this category of hack is the failure to guarantee properties that a reasonable client may require. This generally requires the client to reimplement your subsystem, which is very poor code economy.

It is usually overkill to specify all of the guaranteed properties with exquisite accuracy. As long as a reasonable observer is very likely to infer the guaranteed properties correctly, no additional specification is necessary. However, avoid the policy that the vendor's interpretation of the specification is always correct, as this tends to nurture antisocial vendors who change their "interpretation" as it suits them, usually without notice or admission.

Not Satisfying Guaranteed Properties

A vendor may claim that certain bugs are not worth fixing. Such claims generally ought to be disregarded out of hand, because specifications are meaningless if they are not supported.

If a bug is really extraordinarily difficult to remedy, then one might consider changing the specification in violation of the previous specification. This should be done only if a thorough assessment of the impact on all existing and future clients indicates that it is globally economical. If it is, then most likely the subsystem was inadequately designed to begin with.

Obfuscation

Obfuscation is anything that makes it unnecessarily difficult for an observer to decipher the implementation. For example, rather than renaming all instances of an identifier, a lazy developer might "#define" the old name to the new one, which might be difficult to detect, and which might break another namespace. (An entertaining list of such practices can be found in How to Write Unmaintainable Code . Examples of abuses of the C language can be found in The C Puzzle Book .)

Obfuscation makes verification and maintenance much more costly than necessary. Even the obfuscator himself may have difficulty deciphering his own hacks as times goes by.

Inviting Nondeterministic Failures

Nondeterministic failures are by definition difficult to reproduce systematically. That makes them difficult to diagnose and remedy, but no less real. They are therefore very costly.

Nondeterministic failures commonly arise from poorly designed interprocess communication (in particular, signal handling) or asynchronous logic. In these cases, the solution is usually to be very conservative when implementing the interfaces among processes or clock domains, usually by building such interfaces out of proven library elements.

Premature Optimization

Automated optimizers are sometimes bested by humans. Therefore, efficiency metrics can be improved by implementing optimizations in the source code, for example, by utilizing properties of the system that the optimizer is incapable of determining. While the resulting efficiency improvements might appear as progress, they usually require sacrificing maintainability, for example, because the system breaks when the assumed properties no longer hold.

If manual optimization results in small efficiency improvements, then it is very unlikely to be worth the resulting maintenance costs. On the other hand, if the efficiency improvements are large, that usually indicates a problem that should be attacked at a higher level, in which case it is very likely that improvements can be effected in a maintainable fashion. If low-level optimizations are really justified, then the best way to implement them is to incorporate them into the automated optimizer. Failing that, low-level optimizations should be made only near the end of the development cycle, when the system is as stable as possible, and should be thoroughly documented for reuse.

Many programmers call premature optimization "the root of all evil." However, I think that there is plenty of blame to go around.

Techniques that Scale Poorly

Techniques that scale poorly in the number of times you apply them generally ought not to be applied even once. An example of such a technique would be "In case this happens, repeat everything that you would otherwise do."

If such hacks are really necessary, then they should be considered and approved up-front as part of the design process, such that the number of times they are applied is bounded.

Breaking Global Properties

Sacrificing a property of the whole system for the sake of a single subsystem is almost always uneconomical. An example of such a sacrifice is violating coding conventions. A more serious example is violating exception safety, because it basically precludes the entire process from using any exceptions.

Insufficient Design

This is the Architecture by Implication AntiPattern. When you don't spend enough time designing, you can start ramping up your accumulated lines of code sooner, but the rate of actual progress is diminished, because the resulting code is less effective, and much of it will need to be reimplemented later.

Copying Code

This is the Cut-and-Paste Programming AntiPattern. It is to be avoided because it leads to poor code density, and more importantly because improvements do not automatically propagate from one copy to another.

Unreliable Build System

A given result (in particular, a compiled executable) must be reproducible in order to be useful in light of changing inputs. The procedure used to produce the result must therefore be documented, just as the source files are necessary to document the inputs to that procedure. This usually takes more effort up front than producing the result a single time, but pays off rapidly because a given result may need to be reproduced thousands of times.

Ideally, the build system operates automatically, and rebuilds only those results that depend on modified source files. This is especially necessary when clean building the system is expensive, which is usually the case at the highest levels of integration. The make utility can be used toward this end, but the Makefile must be carefully crafted in order for it to capture all the dependencies, and thus be relied upon without costly clean building. (See Generating Prerequisites Automatically .) There are also alternatives to make, such as jam , that can accomplish the same thing more easily.

The point of all this is that in order to reproduce results correctly and efficiently throughout the life of a product, an up-front investment in the build system is necessary. It is a hack to neglect or postpone this task, or to perform it without attention to correctness.

Insufficient Verification

This is the Minefield AntiPattern. It is uneconomical for a vendor to neglect to verify his subsystem, because that forces each of his customers to deal with his bugs, which is a duplication of effort. Furthermore, defects are more easily diagnosed in isolation than within the context of a larger integrated system.

Working Around Other Hacks

This is the Stovepipe System AntiPattern. When a hack is discovered in another developer's code, it may be politically intractable to arrange for that hack to be corrected expediently. When this happens, it is tempting to compensate for the hack by putting corrective code in the client.

This is a bad idea, because the corrective code is usually a hack itself (for example, it might rely on unspecified properties). It therefore tends to result in an uncontrollable proliferation of hacks. It is also poor code economy, because there are usually more clients than servers.

The preferable solution is to correct the server. This generally requires slipping the client's schedule, and may also require the client's developer to modify the other developer's server, so you mustn't have a policy precluding that.

Hackery is a Disease

At this point, it is instructive to observe that hackery has some of the attributes of a disease:

Hackery is Difficult to Cure

Hacks are undertaken because they are easier to implement than the clean solution. Correcting a hack generally involves undoing the hack and replacing it with what should have been done instead. It is therefore usually more work to correct an existing hack than to do the right thing in the first place.

Hackery is Contagious

As we have seen, hacks can easily proliferate. This actually makes them even more expensive to correct, because fixing one hack might cause another hack to fail. Because of this, there are only two stable alternatives: to eschew hacks completely, or to resign yourself to building Stovepipe Systems.

Hackery can Reach an Incurable Level

Once hackery has thoroughly proliferated throughout a system, the system is almost certain to become uncompetitive. If the hacks are not corrected, then the system will be too brittle to upgrade and maintain effectively. Conversely, correcting the hacks might take so long that the system becomes obsolete in the meantime.

Tolerating Hacks is a Management AntiPattern

It is very common for managers to tolerate or even encourage hackery. This probably arises from the fact that the state of a project is most easily evaluated by assessing the rate of apparent progress. Extrapolating the scalability and maintainability of actual progress requires a great deal more effort and expertise.

Hacks are sometimes justified. For example, they may be necessary as the deadline for demonstrating a prototype looms. However, it must be impressed upon management that such hacks come at a cost. Otherwise, managers are likely to assume that hackery is sustainable, and attempt to accelerate progress by encouraging hacks at every opportunity.

More importantly, managers who tolerate hackery reward the hackers within the organization for their apparent accomplishments, despite the fact that they actually create more problems than they solve. This tends to drive out the non-hackers, who wind up being relegated to cleaning up the hackers' messes, and without whom the organization cannot survive.

It is always the right time to do the right thing.
— Martin Luther King

Anders Johnson, last modified $Date: 2004/02/09 $

[ Home | Resume | Programming | Engineering Philosophy | Family ]