5 Worst Software Anti-Patterns and How to Refactor Them

Every codebase has room for improvement. Here are some low hanging fruit items you can spot and how to fix them, right quick! I've also included some larger effort problems that might take you months to address. Good luck!

5. Magic Numbers

Ever wonder why the value is -1 or 0? So does everyone else. These non-values just confuse both people and likely downstream code. Bugs magically appear when this anti-pattern is used.

The easiest way to get out of this is by naming all those numbers.

if (variable == null) {
    variable = 0;
}

Just give it a name! Make it obvious what 0 number means in this context.

final Integer start = 0;
if (variable == null) {
    variable = start;
}

One step up is to use functions to pass these numbers around, so they are friendly with dependency injection. Which brings us to the next sin:

4. The New Keyword

If you are practicing dependency injection, you should have very few or no usages of the new keyword - I'm talking Java here. Here's why: it makes testing very hard. That should be enough for everyone to tackle this one.

We don't need anything fancy to fix these, just passing your dependencies as parameters.

String dateToString() {
    DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
        .appendMonthOfYearText()
        .appendLiteral(' ')
        .appendYear(4, 4)
        .toFormatter();
    return DateTime.now().toString(dateTimeFormatter);
}

This locks us into using this particular formatter and we're always formatting now. Living in the now is great, but not so good when you want to format dates.

String dateToString(final DateTimeFormatter formatter, final DateTime dateTime) {
    return dateTime.toString(formatter);
}

Yes, this function is almost worthless, but now you can actually tell that!

3. Copy and Paste

I think just about everyone who codes knows this one, but it can still be prevalent. DRY. Of course, don't go crazy, but if you find at least three instances of one piece of logic, fix that.

To fix it, use a fancy feature in your IDE to refactor the logic out to a new function or class, or simply cut it out and do it yourself. Just make sure you have tests in place to make sure you don't break anything in the process.

2. Not Invented Here

I think this one gets overlooked a lot in bigger companies, but doubt it is a problem in smaller ones. Just because we can recreate something that already exists doesn't mean we should.

Be very questioning when some one wants to build a new authentication system or a dependency injection framework (actually, you should never use one!). I've been guilty in the past of poking holes, though microscopic, in solutions to no goal. I did used to be a quality engineer, so it was basically what I was paid for.

We're all good at coming up with something new to code, of course we are. We love coding. BUT, just because you can doesn't mean you should. Think of all the lines you (or some unsuspecting engineer in a few years) will be responsible for maintaining. Pay an expert in the job you need done and focus on your unique business requirements.

1. Untested Spaghetti Code

Yes, I'm sort of lumping two things together here, but I need to make a point about these two. Untested code, bad. Spaghetti code, bad. Both together, REALLY BAD. It makes for nearly impossible to change code without breaking something.

The solution here will require multiple steps:

  1. Find a way to test it. This could be end-to-end tests that exercise the business rules at the highest level, or component tests that hit the controller level. Whatever your app looks like, get to test coverage that you or your team is comfortable with.
  2. Address the previous four anti-patterns. As well as anything else you find difficult to work with. You have the freedom to make the changes needed with the testing safety net.
  3. Create modularity. Chop up that spaghetti into bite size pieces that can be tested individually. Establish a comforable level of test coverage. Bonus points for mutation test coverage and property based testing.
  4. Slice it up logically. Choose a pattern and go with it. Hexagonal. Feature-based. This keeps your logic from intertwinining again. Keep testing!
  5. Convert to functional. If you aren't already doing so, use functional programming where it makes sense. Most applications these days have large pieces that work well functionally over objectly. We're manipulating and reacting to data as it comes into software, so it usually fits. This really helps you keep your logic small, reusable, testable, modular, and simple.

🐾