Article summary
I always strive to write clean, elegant, self-documenting code and avoid ugly kludges. They can be difficult to understand and update, they are often leaky abstractions, and they can be brittle.
But in reality, that it isn’t always possible. Sometimes there are interactions between third-party components. Or time pressures. Or one of a thousand other situations where you need to do things the “wrong” way.
So, you think you have a problem where the solution is something inelegant? Evaluate whether it really is necessary, and then make sure it’ll behave well in the future. And then after you finish building it, take another look using what you’ve learned. Can you make it better?
When I am evaluating such a hack, I look at several things:
Effectiveness
The baseline question is, “Does this actually solve the problem?” Often I’m just wrong and I’ve only handled only a trivial use case. Back to the drawing board.
Even if it gets the job done, it’s not obvious at this point whether some ugly hack is going to be the best solution for the circumstances. You need to look deeper:
- Is it isolated? – If this ends up spewing special case code all over your application, you probably shouldn’t do it. But if it’s just a few lines in a single class, that’s not as bad.
- Is it going to break tomorrow? – If it’s just affecting your own code, you might not have much to worry about. But if it’s using a private 3rd-party API, you may be dependent on a specific version. Is it going to break in an obvious way? Or are you going to have to debug this whole problem all over again?
- How long is this going to have to live? – Using a quick hack instead of doing things “right” may be okay if we can be confident that this is only going to last until the next release.
Implementation
If your idea passes those tests, it’s time to implement. There are a few things I like to review when I do this:
- Is it obvious what is going on and why? – In this sort of non-optimal situation, it’s rarely going to be obvious why you’re monkey-patching something or calling
instance_variable_set
. But you can make it obvious by making sure things are well commented, explaining what components are interacting in what way and how. - Is it time-limited? – Many times I end up with these sorts of things because I am stuck working around a bug in specific version of a Rubygem. There may be a fix upstream, but I can’t use it yet. If you have something where you’ve hacked it in for a single release, make sure that’s noted in the code so that when someone asks, “Does this still need to be there?” you’ll have an answer.
- Is it tested? – If you can write a single, targeted unit test that hits the specific problematic case, that’s the best thing. If not, try to write a full-stack test that exercises it. But make sure you have something. This way, later if you are wondering, “Did that gem fix that yet?” you can remove the hack, run the tests, and see if you still need it.
These are never bad practices, but they are even more critical when you’re dealing with something you know isn’t perfect. If your software ecosystem forces your hand, this isn’t a bad approach for keeping the workarounds under control.