SOLID, KISS, DRY and other principles suck

iamprovidence
11 min readMay 27, 2024

--

There is an occult rumor among developers about a secret software principle that can solve all our issues. Turn out it’s not a single principle, but a bunch of them. However, are they actually helpful? Do they bring any value? Let’s see.

Today we will question DRY, KISS, YAGNI, Tell Don’t Ask, CQS and of course SOLID. You will see why I found them useless. And how all those are nothing more than just clean code, OOP and common sense.

⚠️ Warning alert ⚠️
The following article contains material that may be harmful, potentially distressing or traumatizing to some audiences. It is not recommended for children, women, pregnant children and individuals who may be emotionally vulnerable or can not bear any criticism. Please use discretion when deciding whether to read this article.

There are a lot of controversial cr💩p to say, so not to waste your time, let’s begin.

DRY

DRY stands for “DON’T REPEAT YOURSELF

The idea here is simple. You should not duplicate the code and reuse it as much as possible.

Even though the principle has good intentions, it should be kind of obvious for any developer, shouldn’t it?

I am about to tell you a secret: any program is just a set of instructions and if conditions. That’s it. Do you duplicate the same line 5 times? Write a loop. Do you duplicate the same instructions with different parameters? Write a function. Do you duplicate a set of functions? Write a class. Do you duplicate the same class for different types? Write generic. I think you got the idea 😒.

DRY does not tell you what is duplication and what is not. How DRY you can be? Is variable declaration also a duplication and should be reused?

Surely, I am exaggerating here. DRY may help inexperienced developers avoid the copy/paste approach. This way you don’t end up fixing the same bug in multiple places. However, it can also be as confusing as helpful.

I’ve been on a project where a developer tried to avoid code duplication in two similar functions. It resulted in being the most unmaintainable, unreliable, unpredictable, unreadable code I have ever seen.

Another time, we allowed the code to duplicate. In a year, two pieces of code that seemed to be similar had nothing in common.

DRY makes your code coupled. Sometimes it is fine. Sometimes it isn’t. For example, I allow some code duplication in Unit tests, because that is the essential idea of them to be isolated from each other.

Overly abstracted or DRY code can sometimes be resistant to change or impact performance. Sometimes, a slightly duplicated code path may be more flexible and performant than an overly abstracted one.

My verdict here would be next: kind of obvious, kind of useless.

KISS

KISS stands for “KEEP IT SIMPLE, STUPID”

It anticipates that most stuff works best if they are kept simple rather than made complicated.

So, keep it simple you say, ha? 🤔

It is not really descriptive enough about what exactly “simple” means.

Here are two functions doing the same. Could you tell me which of these functions is “simple”? The first? The second? Both? Maybe neither of them?

string FoundPerson(string[] people)
{
for (int i = 0; i < people.Length; i++)
{
if (people[i].Equals("Don")) return people[i];
if (people[i].Equals("John")) return people[i];
if (people[i].Equals("Kent")) return people[i];
}

return String.Empty;
}

string FoundPerson(string[] people)
{
List<string> candidates = new List<string>()
{
"Don",
"John",
"Kent",
};

for (int i = 0; i < people.Length; i++)
{
if (candidates.Contains(people[i]))
{
return people[i];
}
}

return String.Empty;
}

I know some developers like to overengineer stuff. This is an especially common disease among inexperienced devs, who just taking their first baby steps into their career. They are curious and willing to experiment.

For us, mastodons of programming, overengineering means making the system scalable. My code is complicated not because I want it to be this way, but because the system is complicated 😤. What do you expect me to do? Shoot a moon landing, without doing a moon landing?

The final judgment here is the same. Obvious, useless, pointless. Thank u, next.

YAGNI

YAGNI stands for “YOU AREN'T GOING TO NEED IT”.

Meaning, you should not add a logic that is not used right now, but for the future.

I mean… dah…

Why invest time in functionality that’s not needed?

I know, I know 😒. Some developers like to add stuff they don’t need. Others just feel pity and would rather have unused code in the project than remove something they spent hours writing. Third are just insecure they would not be able to write such complicated code again. There are plenty of reasons why unused code appears.

Guys 😭. Don’t be like that. I have just made a new principle for you, “REMOVE YOUR CODE” or in short RYC. Try it, you will like it 😉

While YAGNI helps prevent unnecessary work upfront, it can also make it more challenging to respond to change later on. If the codebase is not designed with future flexibility in mind, adding new features may require extensive refactoring or redesign, resulting in additional time and effort.

Tell Don’t Ask

The idea here is that you should tell an object to perform some logic, rather than asking its state to do an action yourself.

Here is a bad example of how stuff should not be done:

public class User
{
public string Name { get; set }
}

. . .

// ask
var user = new User();
var oldName = user.Name;
user.Name = "New user name";
RaiseEvent(new UserNameChanged(oldName, user.Name));

You can see User class does not have any behavior and the calling code should figure out on its own what should be done.

According to this principle, we should write like this:

public class User
{
private string _name;

public void ChangeName(string newName)
{
var oldName = _name;
_name = newName;

RaiseEvent(new UserNameChanged(oldName, newName));
}
}

. . .

// tell
var user = new User();
user.ChangeName("New user name");

Isn’t that what the whole OOP is about?🤔😁

CQS

CQS stands for “Command-query separation”. This one is actually not that bad.

It says the function should either be a command that executes an action or a query that returns data, but not both.

When it is applied to your architecture, you will get a well-known CQRS. But at its core, it is just about function being pure, not having side effects, and doing only one single thing.

While pure functions are great, you will find a lot of in-build API violating that rule for the sake of convenience: cache.GetOrAdd(), stack.Pop(), TryParse(), enumerator.MoveNext(), etc.

SOLID

And here we go 😤 A big daddy. Final boss. God of all principles. SOLID itself. We finally got to you.

I know I am touching saint stuff, which would make a lot of developers mad at me. So let’s try to be gentle here 😜.

SOLID sucks🙃.
Solid is nothing more, than a poor man’s OOP. Don’t believe it? Let’s see why.

S (Single responsibility principle)

There should never be more than one reason for a class to change.

In other words, every class should have a single responsibility.

What do you define by responsibility? There are obvious cases when your single class doing math calculations, payments, data storage, email notifications, and so on. Surely it has not been decomposed properly. But there are also not-so-obvious ones. The issue here, it is not clear where you define those boundaries and draw the line. This principle does not tell anything.

Let’s try to put some rules ourselves:

  • Starting from function. It should probably be doing one operation. It should either modify data or retrieve it. Got it!
  • Next goes classes. I guess they should contain related methods together, like data access, email sending, calculations, and so on.
  • Classes form module. Those should probably have only similar classes, like those working with databases are in one module, email sending goes to another, etc.

Phew 😮‍💨. We did it, did we? There is still lots of unsureness. Just as an example. We have two methods. They both get data from the database. One for users, while another for orders. How do we group such methods? By entity? By functionality? Everything that gets data goes in one class, everything that is deleted in another?

As you see, it does not tell you how exactly to do stuff, it just says something is wrong… or not. Different developers may understand this differently.

Applying the Single Responsibility Principle too much may lead to excessive class fragmentation up to the point where everything falls apart.

On the other hand, this principle is supposed to tell us how to design models, which is the whole point of OOP.

O (Open/Closed principle)

Software entities should be open for extension, but closed for modification. That is, such an entity can allow its behaviour to be extended without modifying its source code.

Or, design your code in a way, that adding a new feature means creating new classes and not editing existing code.

Once again, no clear guidelines. However, do you know what gives guidelines? Good, old design patterns, which is only possible because of OOP. Visitor allows adding new operations, strategy allows creating new algorithms, decorator about adding new behaviors to existing classes, you got me.

Let’s just hope applying this principle in the wrong way won’t lead to over-engineering. Next one 😒.

L (Liskov substitution principle)

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

In human words, you should be able to use subclass everywhere where parent class is used.

For example, the code below does not apply this principle:

abstract class VehicleBase 
{
public abstract void Turn();
}

class Train : VehicleBase
{
public override void Turn()
{
throw new InvalidOperationException();
}
}

Because you can not place Train everywhere in your code where VehicleBase is used, otherwise it will throw an exception.

The correct design would be next:

abstract class VehicleBase 
{
. . .
}

abstract class FreeDirectionalVehicleBase : VehicleBase
{
public abstract void Turn();
}

class Train : VehicleBase
{
. . .
}

So, basically all that this principle says is to use inheritance properly… Yeeeah it’s not like that is what OOP is about 😬.

I (Interface segregation principle)

Clients should not be forced to depend upon interfaces that they do not use.

Or, don’t put all methods in a single interface.

So instead of having this:

interface IAnimal 
{
void Eat();
void Fly();
void Swim();
}

class Duck : IAnimal { . . . }

Just have separate interfaces, and implement them based on your needs:

interface IAnimal 
{
void Eat();
}

interface IFlyable
{
void Fly();
}

interface ISwimable
{
void Swim();
}

class Duck : IAnimal, IFlyable, ISwimable { . . . }
class Dog : IAnimal, ISwimable { . . . }
class Cat : IAnimal { . . . }

Sound familiar? It’s like the Liskov substitution principle has been described in other words.

Honestly, at this point, I am convinced that all those principles are just Single responsibility have been defined differently 😒.

D (Dependency inversion principle)

Depend upon abstractions, not implementation.

Developers usually will say something like:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions
  • Abstractions should not depend on details. Details should depend on abstractions

This just highlights that people do not understand what this principle actually means. Nobody can explain what a high-level or low-level module is. The difference between those. In which direction dependencies should be pointed out and why they are inverted.

In simpler terms, you have two types of dependencies:

  • code dependency, when one class (or module) depends on another
  • execution flow, when one class (or module) calls another

Usually those dependencies point out in the same direction and look like this:

But this principle allows you to invert dependencies, so you result i something like this:

Why do I have to explain every principle 😤? Big, red flag to me 🛑.

Long story short, it is possible because of interfaces, and interfaces are something that OOP brings 🙃.

But honestly, after being toxic all this time, I want to say that this principle is the only decent one. It actually brings something, has a good explanation (nobody understands though), affects your architecture, reduces coupling between modules, can break cyclic dependencies, and many more. If only used in the right hands 😉.

Conclusion

So what conclusion can we make? The article is coming to the end. I hope I did not hurt your feelings 😅 I genuinely don’t understand how those obvious stuff got so popular. See me in a bar drinking beer with my friends and I would make up to ten more principles for you 😂.

Overall, it is important to keep in mind that all those principles are guidelines, and not strict rules. Apply them in your practice but don’t get too addicted to those. They have good intentions, but are too abstract to be used and can be understood differently by different people.

And, please, don’t annoy your colleagues with them during the next PR review. This happens to me all the time. Somebody reviewing my code, mentions something about single responsibility, and oh dear, here we start 😤. You just joined the escape room without escape.

I will put all my efforts to make you suffer, to make you cry, to regret your existence. Next time, you would think ten times before bringing any of those principles.

Do you think I am joking?

Believe me, at the end of the day you will even question what the word ‘single’ even means. I will… Ahem 😤😤😤 Sorry for that, it’s just my untreated psychological trauma, flashbacks from Vietnam 😒. Let’s just finish with it until it does not become even worse.

💬 Let me know, in the comment section, what you think about this article. Have you enjoyed it? Do you agree with me or not? Does this article sucks? 🙂

Waiting for your feedback, racoons🦡

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence