I often stumble upon business logic that’s driven by a potentially large permutation of cases. A big series of case statements? Some might argue that’s an automatic smell for splitting up those cases into their own functions.
But, sometimes those little cases can get rather unwieldy—and I’d rather swallow the whole mess into a single innocuously-named function regardless of how much kicking and yelling is going on inside the brackets. Even further, many of those cases end up producing nearly identical implementations, and as much as I’m an advocate of the DRY principles of programming, there are times where I actually prefer repeating myself over and over again.
So, it seems like I’ve committed two cardinal sins here. Lots of repeated code buried inside some tangled mess of conditionals or case statements, and burying all of that inside of one big method. But, if writing good code is ultimately about readability, assuming you’re not doing too much in the way of sacrificing performance for readability, then sometimes breaking these rules-of-thumb is the way to go. Can the next programmer, or more likely, the you that exists a year from now, easily make an update or rewrite your bit of code if need be?
Here’s a case where I went for the repeated-code-stuffed-into-one-bread-basket approach. It’s a rather simple scenario with straightforward code but highlights my argument for similar code I’ve written that gets a bit more complex.
In DoneDone, our web-based issue tracker that you’re company should be using to get projects done, we calculate “active users” to determine whether you’ve reached a user limit for the pricing plan you’ve purchased. Active users are governed by just two simple rules:
- All admins count as an active user
- All non-admins (normal users) are active only if they belong to one or more projects
Though the logic seems simple, it actually creates a deceptive mess of scenarios that we must make sure get accounted for when users are added or updated. For instance, if a current normal user now becomes an admin, then the active user count goes up by one, unless they already belonged to a project. Then, the active user count doesn’t change at all. A normal user who now belongs to ten projects instead of five doesn’t change the active user count, nor does a newly created user who isn’t a member of any projects.
As we start walking through the scenarios of adding and updating users, there are actually twelve different potential scenarios that may or may not have an effect on the active user count. They’re drawn up in the regions of code shown below. This bit of code’s sole job is to take a few pre-calculated parameters and decide what effect they each have on the account’s active user count.
For new users, there are three potential situations—a new admin is an automatic +1, and a new normal user is either +1 or 0 based on whether they’ve been assigned projects. In the update case, there are nine scenarios.
As it turns out, when you go through each scenario, there are lots of cases provide the exact same result. In fact, exactly 50% of the scenarios yield no net active user change. Shown below are two identical snippets of code inside two distinct cases (creating a new admin and creating a new normal user belonging to at least one project):
There might be the natural tendency to, after the fact, go in and refactor all the cases that produced the exact same results into some other abstraction, or at the very least, condense all 6 “net-result-of-zero” producing scenarios into one big if statement and refactor out some of the conditional logic from there. Something, anything, to get rid of this WETness and excruciatingly verbose area of code. But, in the end, my main goal is readable code. And, in the case of this particular bit of logic, shelling out each permutation of newly created or updated users makes the code super-easy to follow, even if it does produce repeated bits of code.
The different scenarios, regardless of whether they produce the same result, are what I care about—and so, I make that the center of my attention. If we ever change our definition of “active user”, I can rest assured that I had accounted for every case in the past and it will be far easier for me to ensure I’m covering all the cases in the future. I bury all these cases inside of one function, instead of breaking them out into smaller encapsulations, because no other part of the app really should concern itself with the deceptive complexities of the question “how many active users does this account have?”
In the end, the best measurement of your code’s maintainability isn’t by strictly adhering to principles—it’s by just looking at it and saying, yes, I understand what I see.