Maintainable code requires the programmers to make conscious decisions about code design at the smallest level, because the code is not ruined in a single day but over time, when unconscious or bad decisions are made day after day.
This is why I find so much value in books like Implementation Patterns, because the author focuses on the small details of coding which have a huge impact on the maintainability of the code in the long term.
Every function, class or module that may be accessed/consumed by programmers in other areas of the code base, should be carefully designed to avoid ripple effects. Any dependence comes with a cost. When a certain function is private, it doesn’t have an impact on the design of the whole code base, just a local one. This is why it’s so important to watch out for the granted visibility of the artifacts we write. Set function’s visibility to private or protected as much as you can. Every public/accessible function or module we write is a design commitment as it affects how the rest of the code is going to be written and also how hard it will be to make future changes, including backwards and forward compatibility constraints. The same applies to field accessors like getters and setters, limit them to the bare minimum.
If the function is a public one pay close attention to its design, this is its signature and behaviour. A good signature already exposes part of the behaviour. Imagine that the function was already written by somebody else and you were to call it from other function.
Answer these questions before coding the function, think before coding. Let’s visit an actual example, the template engine. We are about to code a function that parses a template replacing variables in it. The template is a simple string and the variables are delimited by `$` symbols. Each variable has a name that comes after the $ symbol. If the name in the template matches a variable name in a given bag of variables, the function will replace it. The first design decision is the signature. What data types and structures are the most simple, obvious and expressive for the arguments and the returned value of a function like this? in which layer of the system is it going to be? shall we use primitive types or custom types? Assuming the function will be located within a layer of the system close to the UI, we decide to use primitive types and built-in classes:
public String parse(String template, Map<String, String> variables);
Thinking about the behaviour before coding - absolutely recommended practice:
Variable is missing in dictionary:
Variable is missing in template:
With the happy path case we get a good understanding of how the function should work, what is it intended to do. On the other hand, the corner cases force us to face more design decisions. In the case of missing variables, when the map does not contain the variable existing in the template, what should the function do?
Several people say option A. What would be the consequence of a parse function that throws an exception when a variable is missing? One consequence is that developers calling that function will probably be afraid of it and will become defensive, always surrounding the call with “try-catch” blocks. Can you see the ripple effect in the design of the other parts of the code base? I personally would prefer a parse function that is more resilient, to be honest. I only throw exceptions when it’s impossible to resolve the situation in a way that is intuitive, when the function can’t work because of an exceptional circumstance.
Now, if we go for option B, the caller will not notice that a variable was missing and yet the template have suffered changes as variables are gone, replaced with empty spaces. This behaviour will definitely surprise the caller, it totally violates the Less Surprise Principle as it seems to be an arbitrary decision with no apparent rationalization.
Option C is what most template engines do and this is a good point - if you are designing a tool that is similar to other tools that have been out there for a long time, mind that most people will expect a similar behaviour. Thus the first thing you can do is to research other template engines, compare them, learn from them. We don’t have to reinvent the wheel every day, specially when it comes to how things work. It’s always useful to consider how others solved the sample problem we are facing. Leaving the template as it is, allows for the user to see exactly which variables haven’t been replaced and, moreover it opens up the possibility for a template to be parsed more than once. We could call the parse function several times with variables from different sources:
Map<String,String> source1 = findVariablesFromSource1(); Map<String,String> source2 = findVariablesFromSource2(); template = parse(parse(template, source1), source2);
This design is the most resilient, yet expected, we don’t want surprises in the code.
If you still need to know that some variables haven’t been replaced with some level of automation, there are also solutions for this. Often libraries dump warning messages to some output stream. We could do the same:
The class needs a dependency which is injected via constructor and used in the function whenever a variable is missing. You don’t necessarily have to always use a warning mechanism, it depends on your use case. This is adding certain level of complexity so I would only do it if necessary.
The example of the template engine is very interesting for deliberate practice purposes. In the book Test Driven, Lasse Koskela explains TDD implementing a template engine. The book is really good if you are learning about TDD and code design.
Do you want more? We invite you to subscribe to our newsletter to get the most relevan articles.
If you enjoy reading our blog, could you imagine how much fun it would be to work with us? let's do it!