70

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)?

Peter Mortensen
  • 1,045
  • 2
  • 12
  • 14
  • 31
    I have a feeling this question will draw opinions rather than facts. – Rik D Sep 12 '23 at 08: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 Sep 12 '23 at 08: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 Sep 12 '23 at 13:29
  • @DocBrown O is a good concept for a limited range of code: as you say, black box libraries. Even then, it has never worked long term. You always get into DLL hell or broken NPM dependencies. O is unnecessary and possibly bad for most software. L is more of an exercise in academic finger twiddling than is creating working code. And nobody understands S cause Bob Martin explained it horribly. Its main purpose is to create 100s of stupid questions on Stack Overflow. – user949300 Sep 12 '23 at 16:44
  • @user949300: the OCP works (mostly) wherever you see large library or framework vendors like Oracle or Microsoft, providing libs on which their users (we) do only have a very limited influence on the contained features. I don't change or recompile Microsofts's .NET framework, for me it is closed against modifications. Still it is extendible enough to let me develop lots of applications. – Doc Brown Sep 12 '23 at 17:11
  • 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 Sep 12 '23 at 17:44
  • 1
    SOLID should not be used, when, you don't fully understand its implications. And by the way, as nice as it sounds, it is really a bunch of Odd patterns clubbed together. There are plenty of good advise and rules around, beyond SOLID. – S.D. Sep 13 '23 at 07:18
  • 1
    I always took the 'S' (Single Responsibility Principle) to just be an extension of the good old Unix filter principle Do One Thing Well – Nick Keighley Sep 13 '23 at 12:09
  • 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". – Jared Smith Sep 13 '23 at 15:44
  • 1
    Parnas' take on S (and much earlier than S!!) is much better - not only does it make sense it is also actionable. "On the criteria to be used in decomposing systems into modules" (Parnas, 1972) – davidbak Sep 14 '23 at 14:52
  • @user949300 here's the thing: any piece of code your coworker wrote is easier for you to work with if you can treat it as a black box library, simply because you merely need to understand what it does and not how it does what it does. That's the whole point of the O. It does not matter if the end product is a black box library, what matters is that parts of it are supposed to be. That's modularity 101 – crizzis Sep 14 '23 at 21:50
  • Consider if a more useful set of counterexamples would be, e.g., "what bad things can happen if we don't follow SOLID principles?" – Daniel R. Collins Sep 14 '23 at 22:38
  • @crizzis - I somewhat disagree. While "O" code might help, there is plenty of good modular non-O code that is well written and well encapsulated. And there's probably some poorly written O code out there. – user949300 Sep 14 '23 at 23:14
  • 1
    @davidbak I wish I could upvote your comment more than once. Parnas described S infinitely better than Robert Martin's horrible confusing nearly indecipherable mangling. – user949300 Sep 14 '23 at 23:17
  • 1
    @user949300 Many of your objections are on point, but the Liskov substitution principle is is a sound and objective principle of good design wherever it is applicable. Furthermore, violations of it are often a sign of confusion among the developers. – sdenham Sep 15 '23 at 12:23

8 Answers8

159

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.

l0b0
  • 11,433
  • 2
  • 44
  • 48
svidgen
  • 14,669
  • 4
    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 Sep 12 '23 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 Sep 12 '23 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 Sep 12 '23 at 22:41
  • @Flater I added a footnote. Hopefully this addresses the concern! – svidgen Sep 12 '23 at 23:14
  • @svidgen: Footnote is perfectly clear :) – Flater Sep 12 '23 at 23:25
  • Another problem with Interface Segregation is that it can make construction and use of wrapper classes difficult, especially in cases where some tasks can be done using a simple interface, but could often be done better via other means. For example, given an Enumerable, one could find item 1,000 by constructing an enumerator, and advancing it 1,000 times, and reading the next item, but many classes that implement Enumerable could support reading the Nth item directly. – supercat Sep 14 '23 at 21:51
  • 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 Sep 15 '23 at 01:54
  • P.S. I said immediately above about programmers not being trusted by their managers "possibly for very good reason!". Yes, a lot of managers have been burnt by trusting their programmers to know how to design. But also a lot of managers were probably the kind of programmer who didn't think for themselves, just went with the "rules" not to mention whatever is trendy whether it fit or not ("eXtreme programming", microservices, pentagonal-hexagonal-septogonal architecture: I'm looking at you!). There are all types of managers (also "architects" and "principal engineers" too). – davidbak Sep 15 '23 at 01:56
  • @SimonGeard 'my comment reflects frustration around developers who aren't willing to think for themselves' - mine too, if you haven't noticed. Sometimes not willing and sometimes simply incapable. But, I think davidbak's comment just below mine summarizes my opinion better than I would have hoped to. TL;DR if you're a seasoned dev, you don't need all those rules any longer, they become instincts. But, until you do, you need to look for guidelines elsewhere – crizzis Sep 15 '23 at 04:18
  • @crizzis - yeah, the thing that newer developers need to learn is that they're only guidelines. Don't go looking for invariable rules... if you think your problem can be reduced to rules, then I can probably replace you with a shell script, and spend less time answering questions. – Simon Geard Sep 15 '23 at 05:53
  • I would also add that many code principles break down at the edges of your application and need to be relaxed. For example DDD or functional programming are principles that don't work well (depending on your language) in an HTTP handler or a database access object. The trick is to push such code right out to the edges of your application and keep it as small as possible. – Richiban Sep 15 '23 at 13:17
  • 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 Sep 15 '23 at 14:15
  • @Richiban That's one way to approach it. Another way to approach the "edges" in DDD is to consider that you actually have N products. One of those products is an HTTP handler library. Another is a data access library. And, they each have their own domains and UL's. – svidgen Sep 15 '23 at 14:17
  • I may have missed something, but what exactly is meant by "POAP" in this context? – jwdonahue Sep 21 '23 at 22:47
  • Principle of Applying Principles. (And other things that start with P, I guess.) – svidgen Sep 22 '23 at 04:51
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.

Doc Brown
  • 206,877
  • 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. – Tulains Córdova Sep 13 '23 at 13:51
  • 2
    Enforcing SOLID principles easily leads to making code worse. Have you never heard the joke about the AbstractWidgetFactoryManagerDelegateImpl? – user253751 Sep 13 '23 at 16:40
  • @user253751: my wording did not express what I had in mind, of course. I reworded my first paragraph, better now? – Doc Brown Sep 13 '23 at 20:41
  • EnterpriseFizzBuzz … – jmoreno Sep 14 '23 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 Sep 14 '23 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 Sep 14 '23 at 22:16
  • ...which is easier said than done. Once the code goes past a couple thousand lines, it becomes surprisingly resistant to change. My point is, whenever in doubt of whether to use SOLID or not, I would still lean towards using it. And the situation of your piece of software in a larger picture is not always so clear-cut. Your mileage may vary, of course – crizzis Sep 14 '23 at 22:25
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.

Kilian Foth
  • 109,273
  • 2
    That's a good answer! However I'd like to point out that it's only one of many possible exceptions. This does not mean everything that isn't a lunar mission must strictly follow SOLID. – user253751 Sep 13 '23 at 16:41
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.

Ccm
  • 1,162
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?

piotta
  • 51
  • 1
  • 4
    Better yet: are there any situations where creating too much order creates its own chaos? E.g. you put every piece of clothing in a box... and now you have a wardrobe full of loose unsorted boxes instead of a wardrobe full of loose unsorted clothing and you can't even see what's in each box and the pile is twice as tall. Some people really write software like this! – user253751 Sep 13 '23 at 16:43
4

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.

  • 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. – Nick Keighley Sep 13 '23 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? – Thomas Weller Sep 13 '23 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. – Tulains Córdova Sep 13 '23 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. – Nick Keighley Sep 13 '23 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. – Thomas Weller Sep 13 '23 at 15:16
  • @TulainsCórdova please don't write your shell scripts like that. The "cure" for that nonexistent problem you are worrying about is worse than the "disease" – user253751 Sep 14 '23 at 19:57
  • You can do SOLID in any of those paradigms, with various degees of success. Only L becomes very difficult without OOP. – Ccm Sep 15 '23 at 09:58
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.
Beefster
  • 265
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.

AnoE
  • 5,829