In our previous installment in the TLYF series we took "time" to cover the important topic of Time and Timing. Next up, we're going to get into what we call Intentional Code.
As crafters of code we spend our days (and sometimes nights) writing code. Code that does stuff. And the purpose we have when we generate code is to express our intent to the computer(s) that will execute the code. We do that through language. And how we express that, the assertions we make (explicit and implicit), are our way of communicating with the computer.
But wait ... is the computer our only audience?
Absolutely not. In fact perhaps our most important audience is our fellow developers. They will read our code. They likely will review our code. They will try to understand the INTENT of our code. So the more we communicate intent as we write code, the more chance that they, and the computer, and our future selves a few months from now, will understand precisely what was expected of that code - the input, the processing rules, the data, and subsequently the nature of the outcome. How do we best accomplish this?
We must write intentional code.
As I do much of my development today in Ruby, let's use that language as the lens for this. You can certainly find similar traits in almost every computer language - both functional and object-oriented - that provide equivalent capabilities. As we craft our code to meet requirements we must constantly ask ourselves "what could go wrong?". It is the answer to that simple question that informs us as to how to express intent - both what should happen and what should never happen.
Failure to accomplish implicit intent occurs most often at interfaces. Whether it is making an API request, using a framework method, or addressing a local OSS package bundled into your application, those "conversations" are fraught with uncertainty. Should I be checking for sending the right query parameters, or do I expect the callee to have that responsibility? Is the interface exchanging just simple primitive data, e.g. numeric values or strings, or is there sufficiently rich metadata to determine the class of the data sent and received for me to ensure that units of measure are compatible, or missing data is discovered?
As we TLYF across longer Total Operations Chain Timeframes, we have broader intentions to verify. Satisfying a customer of an e-commerce application involves the integral activities of accepting orders, managing payment, handling shipping and confirming satisfaction. It is hundreds of details with dozens of interfaces where a gap could occur. First time activities include establishing customer profiles, capturing that first payment and ensuring initial e-mail was delivered.
Confused? You shouldn't be. Each line of code, each variable, each method, each class, each function - every single character - is in place for a reason. If not then it can be eliminated without impact on the outcome of the mission. Our intent then, is to craft the minimum amount of code that is necessary to fulfill the objective. How do we recognize when we have unnecessary code? Through testing. Ideally an automated style of testing referred to as "mutation testing". Mutation testing (for example mutant) simply runs our tests, then mutates our code in small but powerful ways, replacing or removing code, and then rerunning the tests. If the tests still pass - especially after removal of code - than one of two inferences can be drawn. One, that code is unnecessary to the overall objective. It is removable. Or two, the tests confirming the need for that code are insufficient to confirm that code. In the latter case the tests (or possibly the strictness of the code) need to be enhanced.
Stateless code - or code that at least encapsulates the state as locally as possible for as short a period as possible - is easier to reason about and ensure correctness. Code that has no "side-effects" is code we can predict.
Ensuring data that should not change during execution does not change is ideal (Adamantium)
We all have a favorite IDE or editor that focuses our thoughts as we write. And depending upon whether your language is statically or dynamically typed, compiled or interpreted, you always have the choice to convey through your code syntactic correctness.
Use getter access (
abc) rather than instance variable access (
@abc) to ensure compile-time fails
attr_reader's provide simple mechanisms for getter method generation
Don't just expect - DEMAND - a particular outcome using the strongest assertive methods possible
Hashaccess rather than
[:key]to communicate intent of key presence
find_by!where content presence/existence is required
Prove that your intent is met
Excellent question. It allows us to more clearly reason about the code. Thus...
Write with intent.
Next up in our TLYF Series → Logging and Failure Modes.