More Consistency Questions
3 min read08-Feb-21
Recently, I wrote a post about whether code should remain consistent with subpar patterns or improve over time (albeit inconsistently).
While I still don't have a catch-all answer for that question, I did run into another related case today.
Consider the following situation:
You are building an API that exposes interactions with a Cuisine
resource containing a type
field. This type
field has a restriction: for now, it can be any of ['Italian', 'Chinese', 'Mexican', 'Thai']
. It's natural your list
API would expose some interaction where users can provide a type
and see recipes matching that style of cuisine. However, what about your picky eaters?
How can we best satisfy the requirement for users to say "I hate Thai food!"?
Think Long Term
This is the real catalyst for the consistency conversation. When making design decisions — no matter how simple they seem — it's crucial to put yourself in the shoes of someone building a similar API in your codebase two years from now. Sure, right now we're trying to satisfy the requirement users can say they dislike one particular type of food. What about later? What if they dislike both Italian and Chinese food?
Think about the last post: it would have been trivially simple when first introducing a deletedExists
parameter to change its name or specific implementation. Now, though? It's complex enough and has enough ripple effects it's not even a realistic option.
For this example, the simplest solution might be something like this:
1// Query the database for values that don't match the provided type.2// With Hibernate, this might look like:3List<Recipe> recipes = Recipe.createCriteria().list (params) {4 if (params['notType']) {5 not {6 eq('type', params['notType'])7 }8 }9}
Of course, we can debate the name notType
as well. It could be any of notType
, excludeType
, typeNotEqual
, etc. This is definitely the time and place to get picky with naming conventions. Better to "waste" ten minutes debating than pick a bad name you have to live with for years.
This is also the stage of the design process where the question should be asked: what could this look like in a few years? What kind of precedent are we setting by introducing the notFoo
pattern? Is there an argument to make this a typeNotIn
and providing an array of types instead?
Compare the above code with this:
1List<Recipe> recipes = Recipe.createCriteria().list (params) {2 if (params['typeNotIn']) {3 not {4 in('type', params['typeNotIn'])5 }6 }7}
Review
The change is tiny, but has an oversized impact on how people would (and could) use the API. Instead of passing in a single string to check against our type
field, we pass in an array. For now, they'll look almost identical. Instead of notType: 'Italian'
, we'll say typeNotIn: ['Italian']
.
Our code is barely different, but we've covered a clear use case to future-proof our decision as much as possible. Frankly, if it never comes up our code will have barely changed. However, if it DOES come up, we won't have to walk back this decision or manage two almost identical APIs. The worst case scenario would be juggling two options, causing consumers to guess whether a resource exposes params.notFoo
or params.fooNotIn
.
Today's lesson: when it comes to API design, think beyond the immediate problem — especially when introducing new patterns. Two years from now, you will thank yourself for not building systems that can't be maintained consistently.