Principle 'Keep it Simple Stupid': How to write simple code?

In the concept of KISS, we simply wish to have only what is necessary.

Principle 'Keep it Simple Stupid': How to write simple code?

It may even sound like a rude term. But when we are talking about maintaining a software, defining the technologies, its architecture and its code, is always better to choose the side of simplicity. In this article we will look at a lot of analogies and examples in code and answer these questions. Are simple and easy the same thing? Why is it important to be simple? How to achieve simplicity?

Simple and easy... are they the same thing?

When we hear about this principle within programming, many link the word simple with easy. But that is not how we should see it. Take a look at the dictionary definition for the word simple:

"without unnecessary or extra things or decorations"

It is interesting to note that simple is to be without unnecessary things. Furthermore, we can compare it to the opposite of simple as the word complicated, see the dictionary definition:

Complicated: "having many parts that are organized in a way that may be difficult to understand".

Right, so we can define complicated as something that is difficult to maintain, because it has many parts that are organized in a way that may be difficult to understand. We can understand that a simple system is opposite to this and is not necessarily a system with only one element. It is a system that may have many parts, but these parts must be organized in a way that is easy to understand.

Cool, I hope you are following the reasoning, now let's see the definition of the word easy:

requiring or indicating little effort, thought, or reflection;

Or it can be defined as:

requiring little work or effort;

To illustrate, imagine you are working on a project where you master the business logic, the language and the framework used, but when you need to change something in the code that needs to be adjusted, this task takes days to be done (weeks depending on the complexity), because the classes are tightly coupled. Is this software easy to change? No. But it is simple for you to understand the requirements and the logic. So that is the difference. In the context of software development there are tasks that are simple to understand, but difficult (not easy) to execute.

So we can say that this is why we want so much that our components, classes, modules or any software atherfact be simple. Now let's understand why we need simplicity.

Is simplicity important?

Something you probably weren't told in college is that software development is a difficult task. Why? Because the code is always changing! Requirements are always changing. Code that doesn't change is a legend, just like Santa Claus. If it never changes, it is probably a study project. Software that generates profit always changes, at some point this will occur and new requirements will arrive! There are always changes that cannot be avoided, contexts such as competition, new target audience and other factors that will cause the software to change. And to change the digital product, the team will have to understand how the different parts of the system work together. And to put this puzzle together and fit new pieces (requirements) together, you need to aim for simplicity wherever possible. I know, easier said than done. There is a phrase that I find very interesting:

It is complicated to build simple things. It is simple to build complicated things.

These words define our profession ๐Ÿ˜„. Why is it complicated to develop simple systems? One reason we have already seen, the requirements change, but there is another that goes beyond that, the system grows! When the system grows a lot, maybe the initial architecture decisions we made 7 months ago no longer meet the current requirements. It may be that new developers are hired for that project and question why the system has that architecture and perhaps they lack the vision from the beginning of the project. It is simple to create a complicated system because it is very easy to get lost in requirements, deadlines, code quality, code test quality and other critical factors surrounding software.

So, to be simple, we must think about how to apply simplicity on a daily basis, both in code design decisions and also in system requirements. And how can we pursue this simplicity? The next topic can help with this answer.

Driving for simplicity

You embrace simplicity when you avoid attaching unnecessary steps to a process that doesn't need them. Our goal is for software to be simple and easy to maintain, because then any developer will need less time to understand how everything connects. If software is considered "simple", then it is likely to be easy to understand and easy to reason about. But for it to be considered simple, its components must be precise in what they do. This is why we increasingly need the project to grow by thinking carefully about every feature that will be developed. Planning the details, questioning and understanding in depth the business rules of the application, because having clear in our mind how the business logic works, we can organize and clearly convey the code we are writing for the new requirement. And this is no easy task! Careful planning of each feature, each user story, takes time and dedication. Unfortunately, time in most projects is short, so we need to measure along with the managers and share with them our questions, suggestions, and concerns about each new requirement. We can ask questions like:

  • This field should be a number, but at the moment I'm not seeing this as an integer type, I see it as a string, is there something I'm not seeing that would require this primitive type?

  • This new column has to be created in the database, but it doesn't say anything about its type here... Is it in definition?

  • I saw that the client requested that this table have this name, but we already have one with a similar name, is the client aware of this fact? Can this new table with this name not generate confusion?

  • Why do we need this feature in this class? Can't we move it to another class? If we need this new field how does the frontend need to receive this field?

These questions make everyone in the meeting also think beyond what is being required. But please be objective and clear in your questions so that everyone can understand your concerns and doubts.

Don't complicate what is already uncertain!

When a new project is started, the decisions made by everyone on the team can be crucial to the smooth running of the project. How so?

Imagine that a customer has just hired the company you work for to develop the application they need. They set realistic deadlines, but as always they ask for agility in the deliveries. We know that deadlines are important, and no matter how realistic they are, unforeseen events can occur. The development team has a lot of facility and knowledge with a specific framework (library) or programming language and they perfectly meet the needs to develop the application that your customer needs. Let's add another factor here. The team's project experience and time in software development is not very large. But then the team decides to move on to other frameworks and languages that they are not fully adapted to or that will take time to study. But new challenges are always welcome, aren't they? In the beginning everything seems to go well and the team seems to deal smoothly with the particularities of the language and framework. After a while, the Front-end developers start having problems working with something specific from the library. Back-end developers start to rack their brains to understand why some exception handling doesn't work as they would like. The demands start to grow, some tests fall behind, the deadline is at the limit and the application starts to get messy. Where can we apply KISS in this story?

It is clear that the decisions made by the team were not the simplest! The team had knowledge in X language and Y framework. But the final decision was to adopt technologies and programming languages that they were not used to and had never applied in real projects.

There is no problem to know, study and work with new technologies. But the problem of the situation was that there was already a short deadline to deliver the application. It was not the ideal moment to go for something new since no one on the team had mastered the chosen technologies.

If this example was not clear. Here's another one.

The development team has just received a large demand. Upon calm analysis, they realize that there is a need to verify a specific business rule in the application that is essential for the user to be able to use the application. Some members of the team start consulting companies that already perform this verification and have a solution ready. But others on the team say: "We don't need to consume a third party service (API). Let's create our own service that will be responsible for all the verifications we need, and then we can save money. It shouldn't be that complex... "

So instead of searching the market for a company that already has the solution ready, that works and that many companies use, they start creating one from scratch and allocate some developers from the main project to develop the architecture and logic of the service. The whole team is dedicated and they succeed! Locally (localhost ๐Ÿ˜„) everything works great. Then the testing phase begins and the service goes to a homologation environment, so the QA can test following the requirements and scenarios that the client determined. But what happens is what no one imagined. Some checks work and some don't. Probably the developers followed a 'vicious' path when testing on their machines. Then they discover that with the LGPD already in force, they need to make adaptations in the code and servers to preserve the data that will be persisted. There is no margin for error, because if this verification is not done correctly, your client will suffer heavy losses. The deadline for delivery begins to run out, but the main application is not complete. So a lot of research is done and they hire the service of a third party company that already has the solution ready and is compliant with the General Law of Data Protection. The project is delivered with a delay that could have been avoided.

Again the problem here was not keeping things simple! The purpose might have been to save money, but don't you agree that there are solutions that are affordable and work very well? Some are even free depending on the demand. Creating a solution from scratch that really works can take time that does not exist at the moment. Especially if you do not have a large team of developers with solid experience.

I hope it is clear from these examples. Decisions don't have to be taken the hard way! We don't need to have a complex architecture from the start and work with 301 different technologies. Applications evolve based on the demands and numbers of users that make use of it. So it is no use working with a non-relational database thinking that you will gain performance if your database has little data stored and your application has only 10 users consuming the data. Go for simplicity!

Simplicity in code... a big challenge

Achieving simplicity in code is what everyone wants, but achieving this goal is not always easy. Mainly because code changes all the time and is changed by many people. So today what seems clear to someone who is developing a feature, tomorrow to another programmer may seem confusing if we are not careful to make it as simple as possible. This is very much related to the readability of the code that we write. Part of good code comes from readability: this means how you express, through the code, the ideas that are in your head and in the business rules. This is not an easy task! But we can overcome this challenge by keeping a few things in mind:

Readability, reusability, and maintainability are important, but we must be careful. When you start applying premature abstraction too much to components and classes before you fully understand the functionality, you introduce complexity. When you introduce complexity, the degree of readability will decrease. The warning is not to start with a generic solution when creating features. Start simple! The first time will never be the best solution you create. Trying to apply premature abstractions to components that have not even been started yet, can be a big mistake..

It is important to remember that there are certain code smells that are a barrier to readability. For example a long argument list in a method. When you see a long argument list, it is a sign that something has not been done right. It could be that the method is doing too much, or that you are passing properties instead of the code object.

public void DoStuff(string email, string userName, string country, string city, string street, string complement, string phone)
{
    //.....
}

Instead, why don't we build an object that will contain all these attributes?

public void DoStuff(User user, Address address)
{
    //.....
}

Many say that less code is more. Less code is not necessarily better, but it generally leaves less room for error. On the other hand, if you need hundreds of lines of code for a simple feature, you have probably overengineered it. There are many ways to increase complexity. Basically, any of the following structures affect our ability to understand and reason about our code.

if, else, else-if, ternary, switch, for, foreach, while/do-while, try-catch, nesting, flow breaks.

This is why code reviews are important, others can look at your code, read it and give feedback on some decisions that have been made. Also the code review serves as a way for everyone to share knowledge with each other and to help those who are new to the project.

Keep your code stupidly simple

It is not hard to find lines of code that could have been simplified or written with purpose. Keeping code readable, simple to understand, and easy to maintain should be every developer's obligation. But we know that in practice it is not quite like this. Take a look at these very simple examples:

if (FirstCondition) {
  // ...
} if (SecondCondition) { 
  //...
}

Is there any logical need to put the second condition this way? Obviously not. Would that keep the code simple? Also no! It might mislead others. The next developer who reads it might think: "Should there actually be an else here?"

It is good to always remember that code is clearer when each statement has its own line. The code above is about separate conditions, but it may not be clear with code organized like this! Take a look at this same corrected code:

if (FirstCondition) 
{
  // ...
}

if (SecondCondition) 
{
  //...
}

// Or else if

if (FirstCondition)
{
  // ...
} else if (SecondCondition) {
  //...
}

See other examples now with Boolean expressions:

public void Exemplo(bool x, bool y)
{
    var z = true;
    if (z)
    {
        Do();
    }

    if (x && z)
    {
         Do();
    }

    if (y || !z) // In this case Z will always be false
    {
       Do();
    }

Can you see the problem in the code above? The variable Z is always true. So what is the need to check it? It would be a different case if it was being declared as argument in a method (function) in the first condition. And since it will always be true there is no need to check it together with X or Y. This code is also a Code Smell. It is certainly not the best way to keep our code simple. See the code fix:

public void Exemplo(bool x, bool y)
{
    var z = true;
    if (Metodo(z))
    {
       //...
    }

    if (x)
    {
        //...
    }

    if (y)
    {
        //...
    }

And what about inverting Boolean expressions, making the code complex instead of simple? Take a look at the code below:

if ( !(x == 5)) { ...}  
bool c = !(i < 2);

Did the developer who thought up the above code at any point worry about making the code simple for others to read? Certainly not! Avoid putting complexity into conditions that are simple:

if (x != 5) 
{
// ... 
}
bool c = (i >= 2);

It is certainly easier to understand written like this. Now we have some simple to read code!

Let's look at another code example, now analyzing a case using the switch operator:

switch (variable)
{
  case 0:
    ExecuteTask();
    break;
  default:
    ExecuteTask();
    break;
}

Apparently there is nothing wrong with this code and it is not that hard to understand. We can actually simplify it by using if-else. Switch statements and expressions are useful when there are many different cases, here we have only two conditions to be checked. So we can replace and simplify with this code:

if (variable == 0)
{
  ExecuteTask();
}
else
{
  ExecuteAnotherTask();
}

The goal is to keep the code simple!

Simple code has fewer bugs

There will always be bugs in software. So we always have to try to minimize to 0 the chances of these bugs getting into production. We find many ways to try to do this (testing, code reviews, pair programming, etc.) and I believe that working towards simplicity is another one of these ways. There are different ways in which simple code can lead to fewer bugs:

  • Simple code usually results in clarity; if there is clarity when reading the code we have fewer bugs;
  • Simple code is easier to understand and therefore leads to more efficient code reviews;
  • It is easier to write reliable tests for simple code; better tests prevent us from bugs;

Also, bugs introduced by simple code are much easier to deal with. Since objective and clear code is easier to understand, it is also easier to find the wrong things in it. Complex code, on the other hand, is not only harder to understand, but tends to be touched less often. This results in a lack of familiarity with the code and makes debugging a pain.

If we have code that is simple it is more visible to find the bug in it, even better to have the tests!

Can you identify the bug below:

switch (i) {
  case 1:
    do_something(1); break;
  case 2:
    do_something(2); break;
  case 3:
    do_something(1); break;
  case 4:
    do_something(4); break;
  default:
    break;
}

The cases were generated by copying case 1. Under case 3, the values were not changed as appropriate for the case. Code reuse is good but this form of code copying has its dangers!

Conclusion

Developing software is not a simple task. There are many choices to make, and they may or may not affect the progress of the project in the future. Applying the KISS principle both in architectural decisions and in the code itself can make your day-to-day life much easier. Remember Martin Fowler's quote in the classic book Refactoring - Improving the Design of Existing Code:

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

If you read until here, thank you so much! See you next time! ๐Ÿ˜‰

ย