The art of keeping things simple

As a software developer you go through different phases in your personal development. Many, but far from all, end up valuing simple code. Simple does not imply that the code doesn’t do what it needs to do, it means that the code is written in a way that puts minimal cognitive load on the human brain while working with it.

For some, striving for simplicity seems to come natural and they appear to have the ability to envision other developers reading their code and how to make it easy for them to follow along. Other developers – for unknown reasons – do not appear to reach this level of insight, even after many years.

Let me take an example, a few days back I was reading through a code forum where a solution to a toy problem was being discussed. The problem was a simple one: write a function – to_robberlang – that converts it’s input to ”The Robber Language”. I thought that I would write my own version of the function first and then compare it with the suggested solution. My code looked like this:

std::string to_robberlang(const std::string& in) {
    std::stringstream result;

    std::for_each(begin(in), end(in), [&](char c) {
        result << c;
        if (!isvowel(c)) {
            result << 'o' << c;
        }
    });

    return result.str();
}

I did not include the code for isvowel but it is a simple helper function that checks if c – in lowercase – is one of ’a’, ’e’, ’i’, ’o’, or ’u’. To me this was a simple, straight forward solution that should do the trick. The suggested solution posted in the forum however was a beast!

The first thing I noticed was that the suggested solution used templates. It did not say clearly but my understanding is that since the problem statement did not mention the type of the input parameter, the author of the solution thought it was a good idea to make it generic (which then also applied to the type of the return value).

Secondly the author wanted the function to work given a slice of a range of characters, for example only a substring or the characters between index 5 and 10 in a large vector. For this to work he made the to_robberlang function take two iterators as input arguments. The first iterator was expected to point to the starting character in the range, the second one past the ending character in the range.

Thirdly the author wanted the caller to be able to supply a back_inserter iterator pointing to the start of the resulting output. This would enable the caller to decide the type of the output container and where inside it to place the output.

The function’s prototype looked something like this (full implementation left out for brevity):

template <typename TIn, typename TOut>
TOut to_robberlang(TIn&& begin, const TIn& end, const TOut& outputbegin);

To me this is a classic case of what I like to call the ”What if”-syndrome. The author probably started out with the simple std::string case in his mind, and then a series of ”What if”:s came over him. ”What if the input is not a std::string?”, ”What if the caller just want to transform a slice of the range?”, ”What if the caller wants to place the output into an existing collection?”. I have seen this happen many times in the development of real world applications. Usually the development team has a quite good understanding of the requirements and the constraints that apply, but then someone says ”But what if someone wants to <insert non-existing use case here> in the future?” and before you know it things have started to spin out of control. When this happens you need to stop it. My response to this concern is usually something in the lines of ”We’ll make sure to make the code easy to change so that we can add support for that use case when it is needed.” This is usually accepted by the team.

So how flexible and generic should you make the code? A good principle to follow comes from Ron Jeffries:

”Do the simplest thing that could possibly work”

https://ronjeffries.com/xprog/articles/practices/pracsimplest/

Going back to the to_robberlang function. Imagine that it is part of a library that you are using and that you have a piece of text that you want to transform to Robber Language. How would you prefer that the interface looked like?

// As a user of the to_robber function, would you prefer calling it like this:
auto encrypted{ to_robberlang("Secret text") };
// or like this:
const std::string original{ "Secret text" };
std::string encrypted;
to_robberlang(begin(original), end(original), std::back_inserter(encrypted));

I would go for the first one any time of the day.

To finish off this post I would like end with a final quote. The KISS principle, coined by Kelly Johnson, says:

”Keep it simple, stupid”

https://en.wikipedia.org/wiki/KISS_principle

Happy coding!

Lämna en kommentar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *