How to deal with overengeenering?

Deep dive into KISS and YAGNI principles

How to deal with overengeenering?

Posted by Paweł Głogowski on June 6, 2020

In my previous article on overengineering. I have explained its origins and provided some general tips on how to avoid it. However, the principles such as KISS and YAGN that I mentioned are quite complex, and it pays to take a deep dive into them. So, in this article, I will focus on those two principles and give some advice on how to handle them.

YAGNI - Why developers wants to foresee future?

Speaking from my experience, I have seen many times how developers try to outsmart the business and foresee the future by implementing a ton of unnecessary features that will "for sure" be used someday.

Usually, that's because for developers, the original task often seems to be simple, and they think like that: "I already make some changes in this area of application so I will sneak some additional features into it to save some time in the future." This is a very bad practice because we usually end up with unnecessary code in the codebase, and the foreseen future might never come up. Usually, after 1-2 years, you might decide to delete that code after hours lost on maintaining it.

At first glance, it does not seem to be dangerous enough to become a source of worry. However, if we end up with more and more unused pieces of code in the application, then it easily becomes very confusing to new developers. Also, someone needs to maintain all of this code, even the bits that aren't being used. This might end up costing a significant amount of time (and money) to your team.

So as a rule of thumb remember this:

"Always implement things when you actually need them, never when you just foresee that you need them."

It might sound very simple, but it's that kind of advice that is easy to remember, but hard to follow. I will try to provide some real-life examples that break the YAGNI rules.

Creating abstractions having just one well-defined case

Let's say that we have to implement a functionality to send some data to an external destination. For now, it is just one destination. Usually, good developers will think that additional destinations will eventually be the case for the application, so it's worth introducing some kind of abstraction. Let's take a look at following pieces of code:

public abstract class ExternalSystemSender {

    public void send() {
        Records toSend = prepareDataToSend();
        commonMethodToSend(toSend);
    }

    private void commonMethodToSend(Records data) {
        //Sending logic
    }

    protected abstract Records prepareDataToSend();
}                        
public class ConcreteExternalSystemSender extends ExternalSystemSender {
    @Override
    protected Records prepareDataToSend() {
        return new Records();
    }
}                  

This can be dangerous from two perspectives. First, you write unnecessary code and also make assumptions about further destination systems. So you may require a huge refactor even if other destinations finally come to life.

"Never introduce an interface or abstract class for only one implementation. Create an abstraction only when you actually need it."

Abuse of strategy pattern

In general, the strategy pattern is a great option to deal with algorithms based on various criteria. However, some developers abuse it by trying to use it everywhere to replace if or switch statements. If the logic is pretty simple and there is just one if - then there is no need to rebuild it and use strategy patterns. Let's take a look at the following example:

public interface PrintingStrategy {
    void print(Object object);
}                                                                 
public class BluePrintingStrategy implements PrintingStrategy {
    @Override
    public void print(Object object) {
        System.out.print("" + object.toString() + "");
    }
}         
public class RedPrintingStrategy implements PrintingStrategy {
    @Override
    public void print(Object object) {
        System.out.print("" + object.toString() + "");
    }
}                                           
class PrintingStrategyFactory {

    static PrintingStrategy createPrinting(String color) {
        if("RED".equals(color)) {
            return new RedPrintingStrategy();
        }
        if("BLUE".equals(color)) {
            return new BluePrintingStrategy();
        }    
        throw new RuntimeException("Color not supported");
    }
}                                           
class PrintingClass {
    void print(String inColor, Object toPrint) {
        PrintingStrategy strategy = PrintingStrategyFactory.createPrinting(inColor);

        strategy.print(toPrint);
    }
}                                      

It's 5 classes just to implement something that can be done within ten lines of code. And in that case, it seems like we don't actually need it (YAGNI).

KISS - what does it mean?

What is complexity?

While YAGNI seems pretty straightforward, the KISS principle is much more problematic and misleading. Most of the time, that's because for many people, "simple" may sound like something incomplete or primitive. Also, each developer will claim that their solution is "as simple as possible" and not too complex. However, I will try to explain what the main concepts around simplicity are. Let's start by defining what complexity actually is.

In computer science, you can find two types of complexity - essential and accidental. Essential complexity is something that we cannot really cope with. Basically, it refers to the domain and the actual problem we are trying to solve. It's pretty obvious that writing a new Facebook or Twitter app will be much more complex than a simple web page with just one form to send a message.

On the other hand, there is accidental complexity - a complexity that is not related to a problem and originates in bad technical decisions, wrongly implemented design patterns, and so on. Let's try to go deep into some common bad architecture designs that make your system complex.

Lasagna architecture anti pattern

Layered architecture was very popular in the early days of programming and served as a great concept to separate presentation view from business logic. Then the layers came to the rescue. The ability to develop parts of the application independently was a huge change. However, times have changed, and new technologies have come up. It turns out that now the number of layers is growing, and it's harder to follow the layering rules and write an application that isn't too complex. It leads to a situation in which to fix some simple issues, you need to make appropriate changes in most layers, including unit tests, which might be painful and time-consuming.

Also, developers tend to have strict rules about layers - for example, that a layer:

  • Depends on the layers beneath it
  • Is independent of the layers on top of it, having no knowledge of the layers using it.

Layers are a good way of implementing encapsulation, data separation, and so on. However, try not to abuse them, and in case you need to use some layer just to proxy it to the layer beneath it - go ahead and use that layer directly.

“Use just the layers we need, the tiers we need, and nothing more!”

Why do developers want to outsmart everyone?

I experienced a few situations in my career when I started to contribute to an existing project and noticed that there are a lot of 'cool' patterns all over the place. I saw complicated CQRM systems, event-driven systems, factories, decorators, etc., created just to solve some really simple CRUD application.

This is very often the result of isolating developers from business people. They usually don't know where the business is all about and what is valuable to the client. They can't see the application from this perspective, and, as a result, they feel that delivering business value can be as satisfying as exploring new technologies and toys to solve even the simple problems. I have seen a ton of situations, where a group of really talented developers could not deliver anything valuable to the users because they were too focused on technical improvements.

If you're a developer, my advice to you is: try to understand what is valuable to your client and get out of your comfort zone. Understanding what the priorities of the client are will be beneficial for you. Then you could focus on implementing those features and solving real problems rather than building complex systems full of unnecessary patterns and designs that serve your ego rather than users.

On the other hand, if you managing development teams try not to be just a proxy between the client and the developers. Encourage your developers to understand the core business needs and let them join some non-technical meetings with managers and users. Try to change their priorities to regularly deliver something valuable to users rather than introducing new technical improvements.

Summary

Managing complexity in IT projects is a really difficult process. In this article, I highlighted just some hints, but it's a really complex topic that cannot be captured in a single article. The most important part is to change the attitude of developers - once they are aware of what is important, they will find solutions that serve clients and not just themselves.

Let`s work together!

+48 792 014 952