Today Mike Pirnat and I were discussing a particularly nasty piece of legacy code we often have to interface with. I described one technique he had used as the Tourniquet Pattern.
This Tourniquet is when you declare a section of your system to be too complex to directly work with or improve safely, and so you seal it off behind cleaner interfaces.
When You Need A Tourniquet
A favorite interview question of mine is “Do you always refactor and fix legacy code when you come across it?”. This is by no means a clear issue. A person can get lost in refactoring, turning a one-day activity into three weeks of hacking, with no guaranteed results.
Some legacy systems, though, are so arcane, so toxic, that the safest thing is to get in, make changes, and get the eff out before anyone gets hurt.
This is the kind of system that may require the Tourniquet.
When You Don’t Need A Tourniquet
Just as in real life, if you need a tourniquet, things are dire. For the sake of the larger system, you’re cutting off a section of code.
You don’t need the Tourniquet if you’re writing new modules or if you have any practical way to refactor or rebuild the problem systems. Use it sparingly, and always as part of a larger improvement plan.
An Example: Man-Purse Purchasing System
Our man-purse store has a purchasing system we inherrited from the previous owner. It’s gnarly, and was written with total disregard for human life or maintainability. It’s interface looks like this:
def __init__(self, a, x, qw="default", roching="", extra=True):
def buy_it_now(self, itema, itemb, opt_itemc, coupon="FX"):
# and so on for ~ 1000 lines or so
This kind of setup makes it almost impossible to cleanly add new features, especially when that module gets to be a few hundred or thousands of lines long (please, stop sighing in disgust, you know you’ve seen something like this).
To apply a Tourniquet, we might pull out the calls we know about and make them easier to call:
def __init__(self, legacy_purchaser, new_purchaser):
self.legacy_purchaser = legacy_purchaser
self.new_purchaser = new_purchaser
"""a new feature! we sell hats now
"""an old feature, but with a cleaner interface.
self.legacy_purchaser.isnt_debugging = True
def buy_other(self, a, x, qw="default", roching="", extra=True):
""" support the old gross interface until we're done porting it all
return self.legacy_purchaser.buy_it_now(a, x, qw, roching, extra)
In this case we’ve picked out a case where the new feature is added, and we leave the rest of it alone, making better interfaces where possible, or just acting like the old module everywhere else.
Over time, the newer, cleaner methods should replace the older ones. The Tourniquet has given us a place to start to gradually and safely make things better.
Hey, You’re Just Hiding BAD CODE
Life is full of hard choices. If we had unlimited time, we could bite the bullet and re-engineer systems from the ground up as they mature. Sadly, we almost never have that luxury. This pattern admits that the code is bad, but at least provides a clean cut from which to build newer and better stuff.