(Apologies to Star Trek fans everywhere…)
One of the popular characteristics of a language is how quickly it can be used to implement new features. A language that requires developers to constantly rewrite lots of nit-picky fiddly bits of code just to get a string allocated (I’m looking at you, C!) is out of favor, even if the techniques for doing just that are very well understood and almost a “reflex” for most developers.
However, the faster you chug along, grinding out new features, you will often be incurring “technical debt” — that is, your code might become fragmented and fragile, with odd little bits of special-case code here and there, making it more and more difficult to make significant changes later on.
With a high-velocity language (like Ruby), particularly if you’re using a good test harness (and you are, aren’t you?) changing out bits of code for refactored pieces can be pretty fast and fairly safe. Of course, an ounce of prevention is worth a pound of cure, so keep in mind opportunities to refactor during development, rather than after.
How do you refactor during development? That’s an art more than a science, but here’s an example that might be all too familiar…
You have a function “foo()” and you need it to do something just a smidge differently in the new feature. The common reactions are:
- Write a brand new function “foo2()” and copy the bits that are the same.
- Add a parameter to “foo()” and then edit the function to check for whether that parameter is set or not.
The first one is probably what you’d be inclined to do if your language doesn’t support default arguments. After all, if you need to go back through all your code and add an argument to all the existing calls, you’re more likely to miss one, or spend just as much time trying to retrofit all the existing calls as you would just cutting & pasting the function.
Then you’ve just bought yourself some debt, though — duplicated logic. Later on, someone goes and modifies the business logic in foo(), and, not knowing that logic was duplicated in foo2(), you now have an edge case where the behavior is different where it probably shouldn’t be — or, you spend lots of time being judicious with comments and leaving breadcrumbs for future developers to follow so they know that if they edit foo() they have to also edit foo2() (and when they get there, they find out it was further copied into foo3(), etc.)
The second solution is popular when your language of choices allows for default values. You can then just add a parameter, set the default to 0, nil, null, empty string, pick your poison, and then twiddle the code so the logic checks the new parameter and does something different if it’s set. Then you don’t have to go check that all the existing code is retrofit, and you don’t have to cut and paste. The bonus being, hey, you still only have one copy of the business logic, so there’s no technical debt, right?
Well, sure, until that function gets modified a few more times and you end up with some horrific logic structure like:
def foo(is_bar = false, is_biff = false)
A if is_bar
C if is_biff
if is_bar or is_biff
E if is_bar
return F unless is_bar
Have you seen this code? I have. Quick — what does foo() return? Does foo(true) execute A? What does foo(true, true) do? Should it? Is foo(true, true) even supposed to be allowed?
This is technical debt. Here’s a chart of inputs and outputs:
|is_bar||False||B = F||BCD = F|
|True||ABDE = G||ABCDE = G|
So how does one do this effectively? Well, that’s where the art comes in. The answer is usually some combination of both techniques — creating a new method, but reusing the existing methods as much as possible.
For example, biff() is an easy solve, and could be implemented by calling foo() directly:
x = foo()
Now biff() will be able to take advantage of any fixes/changes to foo() that are made, but we’re not mixing up the purpose of a call to foo() with a call to bar(). Of course, if biff() is relying on a specific behavior of foo(), there’s still fallout, but that’s another blog post.
So what about bar()? Well, that’s a good question:
But now we’ve duplicated code D with biff()… Well, if D is actually a method/function of its own right, then we’re not really duplicating anything, but if it isn’t, if it’s a block of redundant code, refactor that into its own function. If it’s being called from two places, it’s probably going to eventually get called from a third place anyway, so refactoring at that point is a win.
Remember, it’s a lot easier to go find all the places where a function is called, than to find all the places where a function was re-implemented! And if you do trip over some code that looks awfully familiar, take a few minutes to figure out if it’s the same or not. If it is the same, replace both instances with a function call and save yourself a future headache.
Every project has its own pressures, of course, and sometimes the project plan just won’t allow this kind of diligence — that’s okay, I understand. The trick is to make sure that you recognize you’re incurring this debt, and then make sure you go back and refactor it later, either as part of the next project, or as a standalone project to go “repay technical debt”. If you’re constantly incurring technical debt due to time pressures, and/or if you’re not able to get time to repay some of that debt, that’s a problem of task estimation — and should be taken up with your Scrum master.
You do have a ScrumMaster, right? If not, then let us help you or make member(s) of your team become one! CollabNet offers a variety of ScrumMaster Certification Courses. Click here to learn more about our programs and resources.