A detailed example of the KISS principle in C#

Introduction

In this article I'm going to talk about the KISS principle and vent some frustrations about how such a simple principle can be so difficult to explain to other people.

I'm also going to share my very own C# code examples of KISS being both followed properly and violated. I'm very proud of these code snippets and I use them when mentoring other software engineers as I have found that there is a genuine lack of effective examples of KISS which demonstrate the principle properly.

If you're already familiar with the KISS principle and don't care about what I have to say apart from my code examples, you can jump straight to them.

What's the KISS principle?

KISS is a design principle which originated from the U.S Navy in the 1960s.

It is known to stand for:

  • Keep It Simple, Stupid
  • Keep It Simple, Silly
  • Keep It Short and Simple
  • Keep It Simple and Straightforward
  • Keep It Small and Simple
  • Keep It Simple, Soldier
  • Keep It Simple, Sailor
  • Keep It Sweet and Simple

Regardless of what the acronym stands for, the important aspects of the principle are:

  • Most systems work best if they are kept simple rather than made complicated.
  • Simplicity should be a key goal in design, and unnecessary complexity should be avoided.

This might seem obvious and maybe even just common sense, but it's a surprisingly tricky concept to teach people and stress the importance of.

Easy concept, difficult to demonstrate

Despite the core concept of KISS being easy to understand, it can be surprisingly challenging to show people concrete code examples of KISS in action and, perhaps more importantly, clear examples of KISS being violated. I believe there are a few key reasons for this.

Simple examples are too simple

The idea of a piece of code being simple is usually conveyed by an overly simple problem being solved. For example, a method which adds two numbers together and returns the result, like this:

public int Add(int a, int b)
{
    return a + b;
}

This might be a simple solution, but the problem is too simple. There is effectively no other way of solving the problem in a more complex way and there isn't any logical reason to. The KISS principle is being followed, but there is no way that it can't be followed, so the example loses all meaning.

Another downside to simple examples like this is that it conveys to some software engineers that KISS is only applicable when solving simple problems or building systems that aren't complex. In reality, complex systems are the ones which will benefit the most from KISS.

Complex examples are too complex

Another way of demonstrating KISS is to provide a complex problem and then demonstrate how it can be solved with simple solutions.

This might sound effective in theory, but using complex problems can be problematic because:

  • They can be difficult to understand and may require a certain level of context and advance knowledge of the problem.
  • They might involve methods which are very long or difficult to follow, or make use of obscure techniques, algorithms and language features.
  • They might consist of many different and connected parts, such as multiple classes or projects, which can be difficult and time consuming to demonstrate, especially in something like a presentation or a blog post.

Showcasing something that's difficult to understand to an audience can make people feel confused, bored, alienated or overwhelmed.

If the audience have to concentrate more on learning the context and different parts of a problem, they are less likely to be able to fully understand and appreciate the important parts of the demonstration. This can cause some software engineers to resign themselves to the idea that it's impractical to apply this principle to complex systems.

Complex solutions are sometimes the simplest

I believe there's a fundamental misunderstanding about violating KISS by solving a problem with a complex solution. Sometimes you have no choice!

In situations where you absolutely must use a complex solution, it's important to help other software engineers (and yourself!) understand the approach better by writing comments and documentation to help explain the methodology and your motivations. Where possible, you should also make every effort to make your solution easy to follow and debug. KISS isn't so much about avoiding complex solutions as it is about choosing the simplest solution which solves your problem.

If the most simple way of solving your problem is complex, but you've managed to explain and justify it, I would argue that you're still following the KISS principle. This is something which I don't feel is easy to explain.

A good KISS example: my insane FizzBuzz solution

My go-to code examples for following or violating the KISS principle revolve around the classic FizzBuzz problem.

Let's go through this problem and a couple of very different solutions.

A simple problem

For anyone unfamiliar with the FizzBuzz problem, your goal is to:

  • Go through every number from 1 to 100.
  • If the current number is divisible by 3, you should print the word "Fizz".
  • If the current number is divisible by 5, you should print the word "Buzz".
  • If the current number is divisible by both 3 and 5, you should print the word "FizzBuzz".
  • If the current number isn't divisible by either 3 or 5, you should print the number.

It's a relatively trivial problem with an obviously simple solution...right?

A simple solution which follows KISS

As you'd expect, here's a standard (and simple) solution to the problem:

public void FizzBuzz()
{
    for (int i = 1; i <= 100; i++)
    {
        if(i % 3 == 0 && i % 5 == 0)
        {
            Console.WriteLine("FizzBuzz");
        }
        else if (i % 3 == 0)
        {
            Console.WriteLine("Fizz");
        }
        else if (i % 5 == 0)
        {
            Console.WriteLine("Buzz");
        }
        else
        {
            Console.WriteLine(i);
        }
    }
}

This solution adheres to the KISS principle. It solves the problem in the most basic way and has a very clear flow. Even if someone has never seen the FizzBuzz problem before, I can guarantee they would be able to easily determine what this code does.

A complex solution which violates KISS

Here's the same method, implemented in my special (and much less simple!) way:

private readonly string[] outputs = new string[] 
{
    null,
    null,
    "Fizz",
    null,
    "Buzz",
    "Fizz",
    null,
    null,
    null,
    "Buzz",
    null,
    "Fizz",
    null,
    null,
    "FizzBuzz"
};

public void FizzBuzz()
{
    for (int i = 1; i <= 100; i++)
    {
        Console.WriteLine(outputs[14 - (i % 15)] ?? i.ToString());
    }
}

Out of these two solutions, which would you rather write? Which would you rather debug?

I came up with this rather unique solution a while ago when I made a bet with a colleague that the problem could be solved with only a single modulo operator. I've shown this same solution to experienced software engineers who have been completely stumped, which is amusing when we consider that this is just the FizzBuzz problem which they can usually solve in their sleep. I myself sometimes need to take a couple of minutes to figure out how this works, which is hilarious as I wrote the code.

You could argue to some degree that this solution is cleverer and technically speaking it has reduced cyclomatic complexity, but it's certainly not simple.

Looking at this code, consider the following:

  • Would you be able to figure out if it works correctly just by looking at it?
  • Are you immediately able to figure how it works?
  • If I hadn't explained the story and motivation for writing this solution, would you have been able to work out what the point of it was?

This is a clear violation of the KISS principle. I personally really like using this as an example because it involves solving a simple problem in a complex way, where there is an actual reason for doing so (e.g. improving performance, reducing cyclomatic complexity, winning money from a colleague, etc.).

One other thing to consider is that perhaps this didn't have to be a KISS violation. I mentioned earlier that sometimes a complex solution is the simplest and could still follow the KISS principle, provided it's well documented and it's clear that alternative solutions weren't appropriate. Let's explore this idea...

The same complex solution but following KISS

Let's look at the same complex solution but with a lot of comments and some slight changes to formatting.

/*
    This array represents the expected "Fizz", "Buzz" and "FizzBuzz" outputs for 
    numbers in the 1-15 range (null is used for numbers which are not divisible by 3 or 5).
*/
private readonly string[] outputs = new string[] 
{
    null,
    null,
    "Fizz",
    null,
    "Buzz",
    "Fizz",
    null,
    null,
    null,
    "Buzz",
    null,
    "Fizz",
    null,
    null,
    "FizzBuzz"
};

/*
    This FizzBuzz method has been implemented with the challenge of solving
    the problem by using only a single modulo operator. This means that the 
    solution is more mathematical and less intuitive than common solutions to the problem. 
    Instead of using multiple modulo operators and conditions, the result will be looked up from an array 
    of possible string outputs for numbers in the 1-15 number range by calculating what the number would be
    if it were represented in the 1-15 number space. 15 is a significant number because for a number to 
    be divisible by both 3 and 5, it must be divisible by 15. 
*/
public void FizzBuzz()
{
    for (int i = 1; i <= 100; i++)
    {
        // The first step is to get the remainder when dividing the current number by 15. 
        int remainder = i % 15;
        // Once we have the remainder, we can subtract it from 15 to find the value as if it were between 1-15.
        int lowerRangeNumber = 15 - remainder; 
        // The output array goes from index 0-14, so we need to subtract 1 to get the correct index.
        int outputIndex = lowerRangeNumber - 1;
        string output = outputs[outputIndex];
        if(output == null) 
        {
            // If the output is null, the number is not divisible by 3 or 5, so we'll just set 
            // output to the string representation of the number itself. 
            output = i.ToString();
        }
        Console.WriteLine(output);
    }
}

I would argue that despite this being the same solution, it now follows the KISS principle.

As far as I'm aware, this is still the most simple way of solving my problem of wanting to use only a single modulo and every effort has been made to simplify, justify and explain the added complexity. It's also easier for someone to read or debug the code, as there is better use of variables to show how each stage of the algorithm works.

Conclusion

It's highly ironic that I've written such a long article about such a simple concept, where I'm explaining how simple the concept is. Thank you so much for taking the time to read this massive essay!

In a nutshell, my advice is:

  • Solve problems in the most simple possible way, unless you absolutely need to sacrifice simplicity to satisfy some other requirement.
  • When you're deliberately adding complexity, write as many comments as possible which explain exactly what you're doing and why you're doing it. You should also make formatting changes and add any other information where possible to help someone understand and maintain your code.
  • Keep It Simple, Sailor!


David Omid

Hi, my name's David and I'm a senior software engineer from London.