2018-02-05

Mike Nakis on Code Craftsmanship

In a recent job interview I was asked what are my favorite means of ensuring the quality of the code that I write. Off the top of my head I could give three answers, but it occurred to me afterwards that I could of course have said a lot more. I will try to make a list here.

Please note that in this list I try to avoid repeating things that are common practice, or common knowledge from well read books.  So, for example, I will not mention "Use inversion of control" here, it goes without saying.  I will try to say things that might not be common knowledge, or that might even be controversial.
  • Assert everything.  When I look at code, I don't ask myself "should I assert that?" Instead, I ask myself "is there anything that I forgot to assert?"  The idea is to assert everything that could possibly be asserted, leave nothing unasserted. Assertions take care of white-box testing your code, so software testing can then be confined to the realm of strictly black-box testing, as it should.
  • Eschew the dogma of Unit Testing. Heed the advice that says test against the interface, not the implementation. This means do black-box testing, not white-box testing. Unit Testing tests the implementation, so it is white-box testing, and therefore it should be avoided. Do Integration Testing instead, which is black-box. With code that is choke-full of assertions, it works really well. Incidentally, this means that mocking, despite being an admirably nifty trick, should be for the most part unnecessary. If you have to resort to using mocks in your tests, then many chances are that a) you have not designed something well, or b) you are doing white-box testing.
  • Minimize state, maximize finality / readonlyness / immutability. Design so that as much code as possible is dealing with data that is immutable. Eschew technologies, frameworks, and techniques that prevent or hinder immutability.  If you are using auto-wiring, use constructor injection and store in final/readonly members.
  • Minimize flow control statements, especially the "if" statement. If there is any opportunity to design something so as to save some "if" statements, the opportunity should be pursued tenaciously.
  • Move the complexity to the design, not the code. If the code does not look so simple that even an idiot can understand it, this usually means that shortcuts have been made in the design, which have to be compensated for with overly complex code. Make the design as complex as necessary so that the code can be as simple as possible.
  • Refactor at the slightest suspicion that refactoring is due; do not allow technical debt to accumulate. If you are dealing with a project manager who does not understand technical debt and fails to see where is the "customer value" in refactoring, quit that job, find another one.
  • Strive for abstraction. Every problem of a certain complexity and above, no matter how application-specific it seems to be, can benefit from being divided into an abstract, general-purpose part, and a specialized, application-specific part. Strive to look for such divisions and realize them in the design. The general purpose code will be easier to understand because it will be implementing an abstraction. The application code will be easier to understand because it will be free from incidental complexity.
  • Use domain-specific interfaces. Encapsulate third party libraries behind interfaces of your own devise, tailored to your specific application domain. Avoid, or at the very least minimize, the use of frameworks that cannot be encapsulated.
  • Strive for what is easy, not necessarily for what is simple. The easy is the goal. The simple is only a means of achieving that goal. The simple often coincides with the easy, but sometimes it is at odds with it. Eschew languages and frameworks that are trying to provide the illusion of simplicity at the expense of easiness. The fact that "hello, world!" is a one-liner probably means that the thousand-liner that you are aiming for will be unnecessarily hard to write.
  • Avoid binding by name like the plague.
  • Always use strong typing. Avoid weak typing (euphemistically called dynamic typing) and avoid languages and frameworks that require it. This includes pretty much all scripting languages. QQ.
  • Strive for debuggability. For example, do not overdo it with the so-called "fluent" style of invocations, because they are not particularly debuggable.
  • Strive for testability.  Design interfaces that expose all functionality that makes sense to expose, not only functionality that is known to be needed by the application code that will invoke them. For example, the application may only need an interface to expose a `register()` and `unregister()` pair of methods, but `isRegistered()` also makes sense to expose, and it will incidentally facilitate (black-box) testing.
  • Enable all warnings that can possibly be enabled. The fact that a certain warning may, on rare occasions, be issued on legitimate code, is no reason to disable the warning. The warning should be enabled, and specifically suppressed on a case by case basis.
  • Strive for readability. Code is generally write-once, read many. We tend to read our code several times as we write it, and then many more times throughout its lifetime as we work on it, or on nearby code, as we browse through code to understand how things work, as we perform troubleshooting and maintenance, etc.  Therefore, choices that make code easier to read are preferable even if they make code a bit harder to write.
  • Use an IDE with a spell checker.  Avoid acronyms and abbreviations, and anything that fails to pass the spell check.  Modern IDEs have formidable auto-completion features which means that using long identifiers does not mean that you need to type more. (But even if it did, typing is not one of the problems that our profession faces. Unreadable code is.)
  • Pay attention to naming. Strive for good identifier names and for a variety of names that reflect the variety of the concepts. Spend the necessary time to find the right word to name something. A Thesaurus is an indispensable programming tool.
  • Code offensively, not defensively.  This means never fail silently, never allow any slack or leeway, keep tolerances down to absolute zero. Avoid things like a `Map.put()` method which will either add or replace, and instead design for `add()` methods which assert that the item being added does not already exist, and `replace()` methods which assert that the item being replaced does in fact exist. If an add-or-replace operation is useful, (and it rarely is,) give it a name that clearly indicates the weirdness in the way it works: call it `addOrReplace()`. Similarly, avoid things like a `close()` method which may be invoked more than once with no penalty: assert that your `close()` methods are invoked exactly once. If you are unsure just how many times your code might invoke `close()`, you have far greater problems than an assertion failing in your `close()` method.
  • Use inheritance when it is clearly the right choice. The advice that composition should be favored over inheritance was very good advice during the late nineties and the early noughties, because back then people were overdoing it with inheritance: the general practice was to not even consider composition unless all attempts to get things to work with inheritance failed first. That practice was bad, so the advice against it was much needed. However, the advice is still being religiously followed to this day, as if inheritance had always been a bad thing. This is leading to unnecessarily convoluted designs and weeping and gnashing of teeth. The original advice suggested favoring one over the other, it did not prescribe the complete abolition of the other. So, today it is about time we revise the advice to read "know when to use inheritance and when to use composition".
  • Favor early exits over deep nesting. This means liberal use of the `break` and `continue` keywords, as well as early returns. The code ends up being a lot simpler this way. Yes, this means multiple return statements in a function, and it directly contradicts the ancient "one return statement per function" dogma.  It is nice to contradict ancient dogma.
  • Avoid 'public static' mutable state as much as possible. Yes, this also includes stateful singletons. The fact that it only makes logical sense to have a single instance of a certain one-of-a-kind object in your world is no reason to design that object so that only one instance of it can ever be. You see, the need will arise in the future, unbeknownst to you now, to multiply instantiate your world, with that one-of-a-kind object in it.
I will be extending this list as I go along.

No comments:

Post a Comment