DRY, KISS, and YAGNI to avoid Over-engineering Trap
Refactor and architect your code based on DRY, KISS, and YAGNI principles, beware of over-engineering trap that leads to unnecessary complexity
Throughout my career, I have seen many smart software engineers/developers fall into the trap of over-engineering without even realizing it.
Let's see why software developers tend to over-engineering and what the symptoms are.
Factors that Contribute to Over-engineering
These are the factors that contribute to over-engineering in my opinion:
Lack of understanding of requirements: Developers anticipate problems that may never arise, leading to unnecessary complexity in the system. In other words, a solution is looking for a problem to solve.
Long-term requirements are hard to predict. It requires some level of domain knowledge. If you're lacking it (most developers do), you should just focus on creating solutions that fix the current or short-term requirements.
Too much freedom: Developers have too much freedom to design and do whatever they wish to do. Architecting is fun, you feel like you're a God in some way. This leads to developer egos that he/she merely just wants to demonstrate their skills and knowledge for the sake of doing it.
Overuse/misuse of software design patterns is a very good example, which again makes the system unnecessarily complex and hard to understand. It creates problems rather than solving them.
Over-engineering Symptoms
Over-engineering is often not obvious. The easiest way is to look at the following symptoms, which indicate your system may be suffering from over-engineering.
Increased development time: In theory, refactoring/architecting should reduce development time, not increase it. This usually happens when developers try to design a generic system with few layers of abstraction, which causes the system overly complicated to work with.
Reduced usability: If developers find themselves having to duplicate the code instead of reusing the existing code, it maybe indicates the system is lacking flexibility, difficult to modify or extend.
All these over-engineering symptoms are very similar to bad code symptoms. These symptoms are usually caused by an overly complex system, which may be avoided by following DRY, KISS, and YAGNI software development principles.
Don't Repeat Yourself (DRY)
This is "Refactor 101" in my opinion. If you find yourself needing to have duplicated code, don't do it. Refactor existing classes and functions to be reused instead.
This is not only specific to duplicated code, but also different ways of doing the same thing. If there are multiple solutions to a problem, try to consolidate the solutions into one.
This seems simple, but in practice, the code that you want to refactor could be owned by somebody else / team and not within your control. What will you do in this case? Repeat yourself? You know what the right answer is. :)
Keep It Simple, Stupid (KISS)
Making a simple system simple is easy, but making a complex system simple is extremely hard. First things first, don't make a simple system hard at least. Using proper naming and comment is a good start.
The very important mindset for a software developer is you can never get things right in one go. Thus, refactoring is a continuous process. You keep refactoring your code until a junior/fresh-grade software developer understands it.
Yes, you heard it right. The measure of success for simplicity is everyone can understand it without spending much effort.
You Ain't Gonna Need it (YAGNI)
I once thought having a solution to look for a problem is awesome. It shows how smart and proactive I am. Well, if you have a simple solution with simple effort, then maybe it is fine.
The bottom line is, you're implementing solutions that never be used at the expense of software complexity and effort.
In my experience, this is not merely a software engineering role. A simple requirement leads to a simple solution. Instead, you should negotiate the software requirements to make your solution simple.
Over-engineering Examples
This is my experience dealing with over-engineering in my career. I explain it briefly, so no full context is given. Therefore, the opinions and thoughts expressed here reflect only my views and may not be true.
C++ Template / Generics
I worked with a software engineer who was obsessed with C++ template metaprogramming. All his code was written in C++ template. For those who don't know, C++ template is similar to Generics in Kotlin.
It is generic, but it hurts readability. No one can understand his code except himself. So every time we wanted to make a change, we needed to request to get his approval. A colleague of mine told me this is how you can secure your job!
Key learning: Use generic programming carefully. If it doesn't help readability (from other software developers' perspectives), don't use it.
Composition Over Inheritance
Composition over inheritance has been a trend for software development good practices. It allows more flexibility and reusability.
A software architect in my company liked this idea, so he converted almost all class methods to components. You don't modify/override methods anymore, you create components to replace them instead.
There are a few issues with this approach:
99% of the components are not shared/reused: This defeats the whole purpose of reusability. A good example of YAGNI.
Components' relationship is a mess: This is a drawback of composition, figuring out the relationship between components is hard, especially if you have many of them.
Prone to duplicated code: The system is very flexible. Thus, anyone can just replace something with something else, which leads to multiple solutions to solve a single problem. A good example of violating DRY.
Key learning: In my opinion, this is overdone. Componentization/composition should be done at a higher level (e.g. feature-based) instead of individual methods in a class.
Interface Segregation Principle
This is one of the famous SOLID principles. In short, this principle advises you to avoid designing overly large interfaces, which leads to clients being forced to depend on methods that they do not use.
Well, this sounds great. So a colleague of mine broke all the classes into smaller classes and every single class implements an interface that it needs. It went from 5 classes to 30 classes now.
As you probably know, the drawback is no one can understand the code anymore. It requires some extra effort at least. The bottom line is what customer problems have been solved? Unfortunately, it is none. Without this refactoring, the software still runs perfectly fine.
Key learning: Refactoring and architecting are fun, but do it carefully and don't do it for the sake of doing it. You know this when your code doesn't solve the actual customer problem and requires extra effort to understand it.
Conclusion
I like the "Code Complexity vs Experience" graph below. It pretty much reflects the current majority of software engineering industries, which complicate things rather than simplifying them.
Source image: flaviocopes
If you find SOLID and design patterns are hard to understand, forget about it. Focus on DRY, KISS and YAGNI instead to write clean code.
One thing to keep in mind is, clean code is subjective, there are no clear rules or definitions. Your code is clean, but it may be dirty to me.
So to me, clean code is:
When your code is simple, understandable and maintainable by most of your colleagues, your code is clean!
DRY, KISS and YAGNI are good principles to start with, but we as a human will never be able to do it right in one go. So you must refactor your code from time to time, it is a continuous process. See clean code lesson 1 by uncle Bob.