[
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
]