Have you ever written complex code that felt cumbersome, error-prone, fragile, difficult to reason about? Chances are the reason for these adverse properties is that you were solving the problem at the wrong level of abstraction.
For a simple example of this, consider writing a program which goes through a list of product orders, finds one that belong to a certain user and are above a certain price, and outputs the sum of these orders broken down by month. Imagine writing this in a language with no standard library, using arrays that do not grow dynamically. Imagine you write the whole thing in a single loop. And then imagine that you are required to speed up the code by making it run in parallel on multiple cores.
If you do this thought experiment “properly” as specified, you may get cold sweat from having to think of dealing with array resizing, and thread safety, aggregation of results, all while actually implementing the user and price filtering criteria. It’s a scary mess.
This is a contrived example, and it is perhaps obvious what makes the situation a scary mess — you want a list abstraction so that the mess of array length manipulation can be hidden away. On top of that, you want a parallel collections API that allows you to parallelize computation easily. With these details hidden away, the task now becomes trivial to implement, easy to read and understand, safe to modify to incorporate changing requirements.
Contrived, but not as contrived as one might think. Failing to separate, or even identify, unrelated concerns, and use clean abstractions to keep things untangled, is surprisingly common. So a good rule of thumb is “if something feels like an uphill battle to code a reason about, you are probably dealing with tangled concerns all at the same level of abstraction, and should consider splitting them apart”.
Comments
Post a Comment