Thursday, May 24, 2018

Is Elegance Achievable?

Editor’s note: This is a three-year-old draft that I found.  I’m bad at coming back to things. Regardless, what follows is the post in its near-original form; I reworked some “recently” wording because it is certainly not recent anymore.

The best solutions are “clean,” “elegant,” and “simple,” right?  But what does that mean, and are those things even achievable?


It seems that “elegant” solutions are always small bits of code that solve really small, neatly-defined problems.  That elusive quality is something that doesn’t seem to scale to real-world systems.  Why not?

Complex Requirements

It seems like I’m usually implementing some ‘business process’ into code.  Those processes are designed by people and initially executed by people, who are messy creatures and have a completely different set of strengths compared to computers.  So the business ‘logic’ never ends up very elegant because it has to handle a stack of interdependent conditions on all its data.  The UI state and validation required all depend on a complex flow through the form.

For instance, on a particular form I’ve worked on, contract availability depends on the source (division of the company selling the contract, roughly), individual store making the sale, and the VIN being covered.  The VIN has to be checked with AJAX once the user provides it, and the check relies on an external data service.  When the source changes, there may be a “basically equivalent” contract type, and we want to pre-select that contract if it’s valid.  We have some code to give out warnings about duplicate VINs as well, something along the lines of “Hey. Your customer is already covered on that vehicle.” There are other fields I’m leaving out for brevity—this is only covering events relevant to VIN.

All that ends up coordinating a pretty massive dance between front- and backend code that can never hope to be ‘elegant’ because the fundamental constraints are not themselves simple.

Over-‘simplifying’

Taking DRY to the extreme has also bitten me once or twice.  Sometimes, a couple of paths through the business architecture will look similar enough that I try to thread them together.  Then they turn out to have annoying differences, like a seven-step process where steps 2 and 5 are anywhere from slightly to completely different.

Only those steps also depend on four or five local variables from the previous step, in addition to half the arguments to the function that will allegedly implement both tracks.  But I hack something together that ‘works’ until invariably, the business changes and we want a third path that’s a touch different in step 4 but otherwise the same.

I’ve also written code that’s a big ball of closures, that takes just enough parameters to do its original job. And then I want to reuse it in a slightly different context and it's a big fight to untangle it enough to work. But in its first form, it was very ‘simple’ and self-contained.

Cross-cutting concerns

Sometimes, I use a library for something that introduces its own way of handling things.  Like memcache-dynamo, which uses memcached’s server protocol to take all kinds of cache data and store them into Amazon DynamoDB instead.  (This ‘simplifies’ the rest of our site by adding a layer, since everything knows how to talk to memcached, while DynamoDB support ranges from first-class to practically nonexistent across our sprawling platform.)

The memcached server library is async, so the whole process has to be async.  So it has async logging and async DynamoDB access as well.  Most of it is using AnyEvent because it’s quite a mature event API, and years of usage patterns have been distilled down to the AE interface.  It’s really nice…

And then Amazon::DynamoDB came into the picture, and it’s built on IO::Async, which is notably inconsistent, hard to auto-detect, and makes heavy use of Futures.  Which is not a bad concept (I have nothing against Promises/A+) but the specific Future module is equally difficult to wrangle with.

So just by including one async DynamoDB library, the event and future libraries it depends on get sprayed across my code as well.  By including multiple async libraries, multiple async frameworks get dragged in.

Conclusion

We romanticize elegance… because we so rarely have the opportunity to appreciate it.  Very little code the average programmer writes is about solving a narrowly-defined problem, in a simple way, free from the cares of the world around it.  In other words, real business problems don’t look very much like Project Euler.

Looking at it in reverse, it feels like it’s not worth trying to make business code really elegant, because the inelegance of the domain will come to dominate.  It’s often more important to be clear and straightforward rather than tiny, clever… and fragile.  Similar code might not be identical code, and while copypasta has its issues, bringing too much together into a function is equally painful.

No comments: