71

Coming from a math background, counterexamples are equally, if not more, helpful to me for understanding concepts than examples. I've seen many, many examples of when and how to use the SOLID principles, but I am wondering if there are some good examples of when enforcing these design principles is a bad idea.

More concretely: what are some examples where enforcing a given SOLID principle makes the code worse (for some reasonable definition of worse)?

15
  • 31
    I have a feeling this question will draw opinions rather than facts.
    – Rik D
    Commented Sep 12, 2023 at 8:20
  • 4
    Most code bases do not follow all SOLID principles. Especially O and L are often forgotten in face of requirements and optimizations.
    – Ccm
    Commented Sep 12, 2023 at 8:47
  • 6
    @Ccm: "O" and "L" maybe sometimes forgotten, but way more often they are just not required. The "OCP" makes most sense for reusable black-box libraries, but not every module is such a lib . And "L" is often not required because lots of programs don't require inheritance.or subtypes.
    – Doc Brown
    Commented Sep 12, 2023 at 13:29
  • 1
    @DocBrown Microsoft kind of has to stick to OCP, cause if they don't they would break existing code bases. On the other hand, sticking to OCP for too long will make your code base so large that only the likes of Microsoft can maintain it.
    – Ccm
    Commented Sep 12, 2023 at 17:44
  • 4
    I appreciate your take on code quality with this question, but it's very hard to develop the sensibilities around the Middle Way on this without getting burned at both extremes first. I've worked on spaghetti messes and grotesquely over-engineered code (and created my share of both) and it took a few knocks to the ego to really be able to strike a balance between "clean" and "simple". Commented Sep 13, 2023 at 15:44

8 Answers 8

161

My Principle of Applying Principles:

Principles, patterns, and practices are not final purposes. The good and proper application of each is therefore inspired and constrained by a superior, more final purpose.

You need to understand why you’re doing what you’re doing!

(The POAP is not exempt from the POAP.)

In other words, the default position should be not to apply a principle unless you understand reasonably well why you're applying it.1 Blind faith in anything tends to go wrong; software engineering principles are no exception. Mindless adherence is superstition. It generally just makes extra work and can produce results antithetical to the very intent of the principle. (And that's if it's even a good principle!)

So, I'm always delighted to see questions like this that make an effort to understand a principle's purposes.

My Suggestion

If you don't already have your head wrapped around the principles and their purposes, just create a cheat sheet for yourself to remind yourself what each principle should accomplish, how it might go wrong, and add any special notes for your particular software/business domain.

Something like this:

Principle Purpose Pitfalls Domain Specific Notes
Single responsibility Makes changes isolated, easy to test and reason about. Hyper-decomposition. You probably don't need a full class to set a page/screen title. This is your homework.
Open–closed Help prevent new features from breaking old ones. "Closing" a module you own, maintain, and can safely extend can lead to unnecessary complexity, inheritance chains, LOC, and added binary/bundle size. This is your homework.
Liskov substitution Helps prevent code that looks correct from being incorrect. Over-reliance on LSP as a signal for correctness. This is your homework.
Interface segregation Allows variable/function to accept to more types; Reduces boilerplate for using functions. Requiring the smallest possible interface may conflict with intent of LSP. This is your homework.
Dependency inversion Makes more of the system reusable, swappable, and testable. Over-inversion. Creating and "inverting" abstractions with no gain, adding complexity, making code harder to read and reason about. This is your homework.
Bonus: Don't Repeat Yourself Helps prevent inconsistent application of business rules throughout the system. Over-application: Code that looks the same might serve different purposes and behavior that should diverge become difficult to change. This is your homework.

My entries above are quick and dirty, and you're free and encouraged to disagree about the purposes and pitfalls. You should really research and reflect on each specific principle for yourself and how it applies to your particular system + domain.

Let me reiterate: How each of these applies specifically to a domain or architecture really depends on things only a domain expert would know -- e.g., What in this system is likely to change? What types of logic will be cross-cutting? Etc. So, do some thinking! Take notes! And share them with your team.


1. This answer generally assumes you don't have a tech lead to defer to or that you are the tech lead — and in either case are trying to make a judgement call. But, if you're reading this as a junior engineer or you're new to a team or product, etc., "a senior engineer told me to apply the principle in some way and I trust them [enough]" is a sane enough (though temporary) reason to just apply the principle. (While you grow into your role and take more ownership.)

Remember, even the POAP is subject to the POAP! And the point of the POAP is to keep you from doing silly, wasteful, or harmful things for the sake of a principle! (Or pattern. Or practice.) The POAP is not exempt! Fighting about principles is usually a violation of both the POAP's intent and probably the intent of the principle you'd be arguing about.

15
  • 6
    I agree with this answer, understanding the spirit of the principle is essential to being able to apply it correctly - 100% correct. However, I want to patch a small gap in the phrasing of the first section that some people might misinterpret: when you join a team who is already using these principles in the codebase you will now be working in, it would be counterproductive for you to default to not applying the principle while working in the codebase. There may be a need here to do as the Romans do and at the same time work at understanding why the Romans do what they do.
    – Flater
    Commented Sep 12, 2023 at 22:37
  • @Flater A reasonable caveat. But, I'm not sure how to add it to my answer with the right nuance yet. Sometimes, when you join a team, the expectation is that you do push back on things that make no sense. I'm just not sure how many special cases and nuances I need to try to weave into this.
    – svidgen
    Commented Sep 12, 2023 at 22:40
  • 2
    It's worth noting that the POAP is not exempt from the POAP. If you don't understand what you're pushing back against, don't push back? Or, if the best you can come up with for a "purpose" in each row is, "keeping peace on the team" or "I trust my SME" ... fair enough for now. How would you advise putting the right nuance into the answer?
    – svidgen
    Commented Sep 12, 2023 at 22:41
  • 2
    Not disagreeing with either this answer (which is very good!) or the comments - but there are many things considered "best practices" in this industry which are designed to handle the case where you mainly have junior programmers and where even the "senior" programmers aren't trusted (by their managers) to think for themselves. (Possibly for very good reason!) Take a look at Sonar warnings for Java + the many companies which insist every such Sonar warning be fixed according to the warning regardless of whether it makes sense. Because you aren't qualified to judge! Even if you are!
    – davidbak
    Commented Sep 15, 2023 at 1:54
  • 1
    @crizzis My stance is founded on the idea that the principles essentially achieve good ends. So, it's not saying so much, "don't worry about these principles", as it is, "go learn when to use these principles, because they're important". It's not much different than teaching a classroom about the Chain Rule. You don't just apply the rule to everything you see. You learn when to apply it.
    – svidgen
    Commented Sep 15, 2023 at 14:15
16

Even when a certain situation does not require those principles, following SOLID to a certain degree does not automatically lead to "worse code" (at least, it does not automatically make devs write horrible code). Sure, overuse of SOLID can lead to a certain amount of overengineering. However, for some situations and some programs, the real reason for not sticking to SOLID is that it is simply not worth the extra effort.

SOLID aims for developing software under constant long-term maintenance and constant evolvement, usually over years, by a multi-person team. Those principles try to keep the cost of change low (or at least constant), especially when a software system grows over time. But not every piece of software has such a life-cycle. Here are some examples:

  1. Take, for example, the yearly business report of a mid-size company, implemented in Excel, made up by 1000 lines of VBA code and a bunch of formulas. Maybe its author is lucky and can reuse that Excel macros next year again, but likely there will be huge parts have to be rewritten either, so it won't pay-off to invest too much effort into making the code SOLID. Or maybe there will be a new developer tasked with it, with a completely different skill set, who offers to create the report with completely different tools - it may be most effective here to dump the old report and let the new guy use the tools they know best.

  2. Lets say your company is introducing a new ERP system, and it has to be filled initially from other, older systems. You will need some Perl or Python scripts for this task once, maybe a program of a certain size. But regardless of the size, after the migration is done, the scripts can be dumped. There is obviously no value in putting too much invest into SOLID code here.

  3. You are working for a company designing computer games. They have a core framework which follows the SOLID principles and which is reused over years. Still every game will have a not-too-small individual part which is newly written for each game. Since a typical game will only produce income over a time of one or two years at maximum, they will only offer updates for this time interval after the initial release, and only for the worst bugs, then the maintenance period ends. Next game they produce must be significantly different to keep potential players happy, so lots of parts must be rewritten or newly invented either - so there is not much benefit from SOLID.

Don't get me wrong, that does not mean SOLID is completely useless for such software. But to keep a program SOLID, you will need trained devs, a certain amount of automated test for allowing refactorings and regular code reviews. There is a lot software where keeping this level of quality simply isn't needed.

6
  • 2
    About the last paragraph; it kinda (perhaps inadvertently) suggests SOLID creates the necessity for automated [unit] tests. I think it's the other way around. In order to be able to use automated [unit] tests, you have to apply some of the solid principles. Not every software needs that level of quality (automated [unit] testing), and many that do can't get it because the codebase, or the parts of it that beg for automated testing, don't follow at least the S, the L and the D. Commented Sep 13, 2023 at 13:51
  • @user253751: my wording did not express what I had in mind, of course. I reworded my first paragraph, better now?
    – Doc Brown
    Commented Sep 13, 2023 at 20:41
  • EnterpriseFizzBuzz …
    – jmoreno
    Commented Sep 14, 2023 at 10:40
  • The problem is, in a lot of cases 'developing software under constant long-term maintenance and constant evolvement, usually over years, by a multi-person team' is not originally intended nor anticipated at all by the person who makes the initial commit...
    – crizzis
    Commented Sep 14, 2023 at 22:02
  • @crizzis: well, the trick is to refactor and make the code more SOLID as soon as it becomes apparent that a software system might live longer than a few weeks or months.
    – Doc Brown
    Commented Sep 14, 2023 at 22:16
12

SOLID principles (and most of software engineering, really) are about human factors. The goal is to enable working on a codebase in the face of unforeseeable future requirements changes.

Therefore, one big exception is when your requirements will not change, and your environment is also constant and limited (such as the original lunar missions). Then it makes total sense to optimise the code until it fits the available space, while sacrificing the ability to easily change it for change requests that you know will not come.

0
6

All of these concepts in SOLID have disadvantages.

S - This one is mostly safe, unless you are restrained from an address or storage space perspective. Think embedded software.

O - Can come with a serious maintenance overhead depending on how religious you are.

L - Most OOP languages will help you stick to this one, but when it comes to complex software written in the likes of C, sticking to it is not the primary objective. An example is the windows SDK and the Handle type: if you feed a file handle to a function expecting a GPU handle you'll be in trouble.

I - If you use a framework or develop a framework you will stick to I at some point. Similar to S, it may go unused in some extreme situations. Overuse has the tendency to turn your code inside out.

D - Overuse makes the code difficult to use without a specialized DI framework, which have the adverse side effect of poor startup times (which is why it is popular in web projects where startup time doesn't matter but its use is limited in other types of projects). Leads to leaky abstractions (see the Android SDK for overuse, especially the RecyclerView or anything BLE related). See the Microsoft XAML framework for a more balanced approach.

1
  • 9
    I would add that S is often misinterpreted to create a module that only does one tiny little thing.
    – user949300
    Commented Sep 12, 2023 at 14:45
5

I do not think I can add anything new, as there are already a couple of really good answers, so I will merely try to put it another way around.

The idea behind the SOLID principles is to have clean, structured code. Order instead of chaos. So one could rephrase your question to "is there a situation, in which chaos is preferred over order?". Except for some very specific situations (e.g. some reverse-engineering issues) the answer is usually "no, unless you exaggerate".

Think of clothes in your wardrobe. It is definitely easier if you have them segregated on different shelves, using boxes etc. instead of just one huge heap. Objectively seen it's just easier if t-shirts are separated from socks. One could go one step further and have thick, winter socks in a different box than thin, summer socks. And the latter further could be further divided into short and long socks, which will bring even more structure, but at certain cost (more boxes, less actual space for socks). If you follow that path, at certain point it will be too much and you would rather prefer some "chaos" (e.g. socks of different colours mixed together), that you know you are able to handle, than the overhead that keeping everything organised causes.

Exactly the same principle applies to software engineering. Somewhere in your code you might see a tiny class with two public methods, doing two things that can be seen as two separate responsibilities. Will splitting it into two (or more) classes, making sure they are injected correctly, that every consumer knows which is which etc. result in a code that is easier to read, maintain and less error-prone? Or is it better to leave it as is, at least for the time being?

0
5

The SOLID principles are also called the principles of object oriented programming. Since no other answer has mentioned it before, let me do it.

You don't apply SOLID to any other programming paradigm, like

  • imperative programming
  • structured programming
  • functional prorgamming
  • declarative programming
  • ...

That said, SoC (Separation of Concerns; probably Dijkstra in 1974) was there long before SRP was officially called like that (maybe 1990 by Rebecca Wirfs-Brock in the book "Designing Object-Oriented Software").

Also, you could implement with dependencies inverted, but it was not called DIP at that time ("The C programming language" second edition just says that "Users don't need to know the details"). In C that was implemented with function pointers. OO made that use of function pointers safe via the virtual function table and thus "built-in" into the language and DIP became a thing.

6
  • 3
    I would have thought S and D apply to any programming paradigm. I expect my functions, modules, files, packages to all be SRP so far as is practicable. Commented Sep 13, 2023 at 12:29
  • 1
    @NickKeighley: Regarding SRP, I think there are similar principles, like SoC, which existed long before SRP. Regarding DIP, I thought dependeny inversion is only possible since OO. Before, e.g. in C, all source code dependencies go into the same direction as the call dependencies, don't they? Commented Sep 13, 2023 at 13:46
  • @ThomasWeller Shell scripting allows for some kind of (crude?) DIP and LSP, because you can store the names of functions, procedures and programs in variables and running them, allowing you to swap the implementation at runtime, if they expect the same parameters. Commented Sep 13, 2023 at 14:03
  • Unix files spring to mind. Everything looks like a file but what a read or write actually does varies wildly. The FILE* from stdio is a simpler example. Commented Sep 13, 2023 at 15:02
  • @NickKeighley: granted, that's a fair point. With function pointers you can do DIP. However, I think that was done long before someone mentioned DIP (maybe 1994 in this paper). Those function pointers are unsafe. By imposing restrictions on that indirect transfer of control, OO removes that unsafeness. Commented Sep 13, 2023 at 15:16
3
  • S: Good for the most part, but be careful not to over-fragment your code and scatter functionality all over the place.
  • O: Applicable to enterprise OOP. Probably you should just avoid inheritance, in which case, this principle is not applicable
  • L: The Liskov Substitution Principle is so horribly unintuitive (contravariance especially) that developers have largely abandoned inheritance. Stick to treating everything as invariant and save yourself some headaches.
  • I: Kind of goes along with S. Separate functions when they do multiple things and you don't always need to do all of those things. In other words, don't over-abstract and hide things behind context objects and god functions.
  • D: Passing in dependencies is nice sometimes. Avoid DI frameworks though.
2

As you are mostly looking for examples where skipping aspects of SOLID may be OK, or where sticking to it too much might be problematic, let me suggest this:

The more your code base has the character of a library, the more you should really stick with SOLID. Especially if it is used by people who are not themselves developing the library.

On the other hand: the more your code base has the character of an "end product" which is not used by anything else, and not re-used on a code level, especially if it has the strong character of a one-off, it can be completely fine to skip some SOLID aspects.

Examples (possibly contrieved):

  • If your code is small enough that it fits in a single method in a single class, with a two-digit amount of lines, it is probably fine for it to mix all its responsibilities together. Think tooling, scripts, glue, etc.
  • While open-closed is still partly applicable in modern languages (i.e., you don't want to break a range of other classes due to a change in another one), this criterion can be very much relaxed in a modern languages like Ruby (fully object oriented, but with fully dynamic duck-typing). In such a language, it is very much possible to modify classes in all kinds of ways, either in their source-code or on-the-fly through inspection/monkey-patching/extending, without introducing the problems of old, where you had to recompile all dependencies on many changes to a class.
  • Liskov substitution is arguably irrelevant (or at least not applicable) in object-oriented languages which chose other mechanisms than "classic" strict interfaces. I.e., again Ruby with its duck-typing, super-classes, modules and so on and forth; or JavaScript which is object-oriented but uses prototypes instead of classes.
  • I cannot think of a way in which Interface Segregation would ever be a bad thing. If the language does have something like interfaces or something comparable, each individual interface should be as stand-alone as makes sense. Especially if it happens to be a language which forces the class implementing the interface to actually have methods for all of them. As before, a more dynamic language like Ruby does not enforce this in a technical manner (there is no real comparable construct there which would make a big interface have negative impact on a subclass), but as a programmer it would definitely be a code-smell to have large chunks of code throwing together all manner of unrelated responsibilities.
  • Dependency inversion is a perfect candidate for something where you can start out with direct dependencies as long as there is only one or a very very few possibilities, and refactor DI if and when you notice that the number of candidates is growing. More on refactoring later.
  • DRY is the same. Specifically, while most of the other SOLID aspects are grand architectural items, DRY seems to be of a markedly different ("low-level") category to me. This comes down to the nitty-gritty and in some part also the opinion/tastes of the developer(s), and maybe more a part of a DOD than a big "principle".

Careful: my answer assumes modern development in the context of principles like The Agile Manifesto or the 12 Factors. Both more or less mandate, amongst others, that you are relatively easily able to quickly and ruthlessly refactor your code. There is some overlap with SOLID of course (none of those principles are "bad" in any way). But as soon as you have refactoring in place - real refactoring, with complete automated tests, an architecture that avoids having many of the issues targeted by SOLID in the first place, a development team which embraces those things, etc. - it should be very much possible to approach many tasks in a more pragmatic fashion, until the code grows and you see that you do indeed need to introduce some SOLID (or other) principles that you consciously decided to not adapt so far.

Not the answer you're looking for? Browse other questions tagged or ask your own question.