70

I like SOLID, and I try my best to use and apply it when I'm developing. But I can't help but feel as though the SOLID approach turns your code into 'framework' code - ie code you would design if you were creating a framework or library for other developers to use.

I've generally practiced 2 modes of programming - creating more or less exactly what is asked via requirements and KISS (typical programming), or creating very generic and reusable logic, services, etc that provide the flexibility other developers may need (framework programming).

If the user really just wants an application to do x and y things, does it make sense to follow SOLID and add in a whole bunch of entry points of abstraction, when you don't even know if that is even a valid problem to begin with? If you do add these entry points of abstraction, are you really fulfilling the users requirements, or are you creating a framework on top of your existing framework and tech stack to make future additions easier? In which case are you serving the interests of the customer, or of the developer?

This is something that seems common in the Java Enterprise world, where it feels as though you're designing your own framework on top of J2EE or Spring so that it's a better UX for the developer, instead of focusing on UX for the user?

  • 12
    The problem with most short programming rules of thumb is that they are subject to interpretation, edge cases, and sometimes the definitions of words in such rules are unclear on closer inspection. They can essentially mean a wide variety of things to different people. Having some non-ideological pragmatism usually allows one to make wiser decisions. – Mark Rogers Sep 27 '18 at 14:44
  • 1
    You make it sound like following SOLID principles somehow implies a big investment, a lot of extra work. It does not, it is practically free. And it will likely save you or someone else a big investment in the future because it makes your code easier to maintain and extend. You go on asking questions like "should we do our homework or make the customer happy?" These are not trade offs. – Martin Maat Sep 27 '18 at 19:20
  • 1
    @MartinMaat I think that the more extreme forms of SOLID do imply big investment. Ie. Enterprise software. Outside of enterprise software, you would have very little reason to abstract your ORM, technology stack, or database because there's a very good chance you are going to stick with your chosen stack. In the same sense, by tying yourself to a particular framework, database, or ORM, you are breaking SOLID principles because you are coupled to your stack. That level of flexibility from SOLID is not required at most jobs. – Igneous01 Sep 27 '18 at 22:26
  • 1
    See also the Inner Platform Effect. – Maxpm Sep 28 '18 at 3:40
  • 1
    Turning most code into something like a framework doesn’t sound at all terrible. It only becomes terrible if it becomes over-engineered. But frameworks can be minimal and opinionated. I’m not sure if this would be an unavoidable consequence of following SOLID but it’s definitely a possible consequence and one that, I think, you should embrace. – Konrad Rudolph Sep 28 '18 at 13:59
84

Your observation is correct, the SOLID principles are IMHO made with reusable libraries or framework code in mind. When you just follow all of them blindly, without asking if it makes sense or not, you are risking to overgeneralize and invest a lot more effort into your system than probably necessary.

This is a trade-off, and it needs some experience to make the right decisions about when to generalize and when not. A possible approach to this is to stick to the YAGNI principle - do not make your code SOLID "just in case" - or, to use your words: do not

provide the flexibility other developers may need

instead, provide the flexibility other developers actually need as soon as they need it, but not earlier.

So whenever you have one function or class in your code you are not sure if it could be reused, don't put it into your framework right now. Wait until you have an actual case for reusage and refactor to "SOLID enough for that case". Don't implement more configurability (following the OCP), or entry points of abstraction (using the DIP) into such a class as you really need for the actual reusage case. Add the next flexibility when the next requirement for reusage is actually there.

Of course, this way of working will always require some amount of refactoring at the existing, working code base. That is why automatic tests are important here. So making your code SOLID enough right from the start to have it unit-testable is not a waste-of-time, and doing so does not contradict YAGNI. Automatic tests are a valid case for "code reusage", since the code in stake is used from production code as well as from tests. But keep in mind, just add the flexibility you actually need for making the tests work, no less, not more.

This is actually old wisdom. Long ago before the term SOLID got popular, someone told me before we try to write reusable code, we should write usable code. And I still think this is a good recommendation.

  • 23
    Extra points of contention: Wait until you have 3 use cases where you see the same logic before refactoring your code for reuse. If you start refactoring with 2 pieces, it's easy to end up in a situation where changing requirements or a new use case ends up breaking the abstraction you made. Also keep refactors limited to things with the same use-case: 2 components might have the same code but do entirely different things, and if you merge those components, you end up linking that logic, which can cause problems later down the line. – Nzall Sep 27 '18 at 9:23
  • 8
    I generally agree with this, but feel like it is too focused on "one-off" apps: You write the code, it works, fine. However, there's a lot of apps with "long time support". You might write some code and 2 years later, the business requirements change so you have to adjust the code. By that time, a lot of other code might depend on it - in that case, SOLID principles will make the change easier. – R. Schmitz Sep 27 '18 at 12:17
  • 3
    "Before we try to write reusable code, we should write usable code" - Very wise! – Graham Sep 27 '18 at 12:53
  • 10
    It’s probably worth noting that waiting until you have an actual use-case will make your SOLID code better, because working in hypotheticals is very difficult and you are likely to mis-guess what the future needs will be. Our project has a number of cases where things were designed to be SOLID and flexible for future needs... except the future needs turned out to be things no one thought of at the time, so we both needed to refactor and had extra flexibility we still didn’t need—which either had to be maintained in the face of the refactoring, or scrapped. – KRyan Sep 27 '18 at 13:19
  • 2
    also usually you will still need to write testable code, which usually means to have a first layer of abstraction in order to be able to switch from concrete implementation to test ones. – Walfrat Sep 27 '18 at 13:48
49

From my experience, when writing an app, you have three choices:

  1. Write code solely to fulfil the requirements,
  2. Write generic code that anticipates future requirements, as well as fulfilling the current requirements,
  3. Write code that only fulfils the current requirements, but in a way that's easy to change later to meet other needs.

In the first case, it's common to end up with tightly coupled code that lacks unit tests. Sure it's quick to write, but it's hard to test. And it's a right royal pain to change later when the requirements change.

In the second case, huge amounts of time is spent trying to anticipate future needs. And all too often those anticipated future requirements never materialise. This seems the scenario that you are describing. It's a waste of effort most of the time and results in unnecessarily complex code that's still hard to change when a requirement that wasn't anticipated for turns up.

The last case is the one to aim for in my view. Use TDD or similar techniques to test the code as you go and you'll end up with loosely coupled code, that's easy to modify yet still quick to write. And the thing is, by doing this, you naturally follow many of the SOLID principles: small classes and functions; interfaces and injected dependencies. And Mrs Liskov is generally kept happy too as simple classes with single responsibilities rarely fall foul of her substitution principle.

The only aspect of SOLID that doesn't really apply here is the open/closed principle. For libraries and frameworks, this is important. For a self-contained app, not so much. Really it's a case of writing code that follows "SLID": easy to write (and read), easy to test and easy to maintain.

  • one of my favorite answers on this site! – TheCatWhisperer Sep 27 '18 at 19:57
  • I'm not sure how you conclude that 1) is more difficult to test than 3). More difficult to make changes to, sure, but why can't you test? If anything, a single-minded piece of software is easier to test against the requirements than a more general one. – Mr Lister Sep 28 '18 at 15:14
  • @MrLister The two goe hand in hand, 1. is more difficult to test than 3. because the definition implies it isn't written "in a way that's easy to change later to meet other needs." – Mark Booth Sep 28 '18 at 17:33
  • 1
    +0; IMVHO you are misinterpretting (albeit in a common way) the way 'O' (open-closed) works. See e.g. codeblog.jonskeet.uk/2013/03/15/… - even in small codebases, it's more about having self-contained units of code (e.g. classes, modules, packages, whatever), that can be tested in isolation and added/removed as the need arises. One such example would be a pack of utility methods - regardless of the way you bundle them, they should be 'closed', i.e. be self-contained, and 'open', i.e. extendable in some way. – vaxquis Sep 29 '18 at 11:33
  • BTW, even Uncle Bob goes that way at one point: “What [open-closed] means is that you should strive to get your code into a position such that, when behavior changes in expected ways, you don’t have to make sweeping changes to all the modules of the system. Ideally, you will be able to add the new behavior by adding new code, and changing little or no old code.” <- this obviously applies even to small applications, if they are intended to be ever modified or fixed (and, IMVHO, that's usually the case, esp. when concerning the fixes chuckle) – vaxquis Sep 29 '18 at 11:35
8

The perspective you have can be skewed by personal experience. This is a slippery slope of facts that are individually correct, but the resulting inference isn't, even though it looks like correct at first glance.

  • Frameworks are larger in scope than small projects.
  • Bad practice is significantly harder to deal with in larger codebases.
  • Building a framework (on average) requires a more skilled developer than building a small project.
  • Better developers follow good practice (SOLID) more.
  • As a result, frameworks have a higher need for good practice and tend to be built by developers who are more closely experienced with good practice.

This means that when you interact with frameworks and smaller libraries, the good practice code you'll interact with will more commonly be found in the bigger frameworks.

This fallacy is very common, e.g every doctor I've been treated by was arrogant. Therefore I conclude that all doctors are arrogant. These fallacies always suffer from making a blanket inference based on personal experiences.

In your case, it's possible that you've predominantly experienced good practice in larger frameworks and not in smaller libraries. Your personal observation isn't wrong, but it's anecdotal evidence and not universally applicable.


2 modes of programming - creating more or less exactly what is asked via requirements and KISS (typical programming), or creating very generic and reusable logic, services, etc that provide the flexibility other developers may need (framework programming)

You're somewhat confirming this here. Think of what a framework is. It is not an application. It's a generalized "template" that others can use to make all sorts of application. Logically, that means that a framework is built in much more abstracted logic in order to be useable by everyone.

Framework builders are incapable of taking shortcuts, because they don't even know what the requirements of the subsequent applications are. Building a framework inherently incentivizes them to make their code usable for others.

Application builders, however, have the ability to compromise on logical efficiency because they are focused on delivering a product. Their main goal is not the workings of the code but rather the experience of the user.

For a framework, the end user is another developer, who will be interacting with your code. The quality of your code matters to your end user.
For an application, the end user is a non-developer, who won't be interacting with your code. The quality of your code is of no importance to them.

This is exactly why the architects of a development team often act as the enforcers of good practice. They are one step removed from delivering the product, which means they tend to look at the code objectively, rather than focusing on the delivery of the application itself.


If you do add these entry points of abstraction, are you really fulfilling the users requirements, or are you creating a framework on top of your existing framework and tech stack to make future additions easier? In which case are you serving the interests of the customer, or of the developer?

This is an interesting point, and it's (in my experience) the main reason why people still try to justify avoiding good practice.

To summarize the below points: Skipping good practice can only be justified if your requirements (as currently known) are immutable, and there will never be any change/addition to the codebase. Spoiler alert: That is rarely ever the case.
For example, when I write a 5 minute console application to process a particular file, I don't use good practice. Because I'm only going to use the application today, and it doesn't need to be updated in the future (it'd be easier to write a different application should I need one again).

Let's say you can shoddily build an application in 4 weeks, and you can properly build it in 6 weeks. At first sight, shoddily building it seems better. The customer gets their application quicker, and the company has to spend less time on developer wages. Win/win, right?

However, this is a decision made without thinking ahead. Because of the quality of the codebase, making a major change to the shoddily built one will take 2 weeks, while making the same changes to the properly built one takes 1 week. There may be many of these changes coming up in the future.

Furthermore, there is a tendency for changes to unexpectedly require more work than you initially thought in shoddily built codebases, thus likely pushing your development time to three weeks instead of two.

And then there's also the tendency to waste time hunting for bugs. This is often the case in projects where logging has been ignored due to time constraints or sheer unwillingness to implement it because you absentmindedly work under the assumption that the end product will work as expected.

It doesn't even need to be a major update. At my current employer, I've seen several projects that were built quick and dirty, and when the tiniest bug/change needed to be made due to a miscommunication in the requirements, that lead to a chain reaction of needing to refactor module after module. Some of these projects ended up collapsing (and leaving behind an unmaintainable mess) before they even released their first version.

Shortcut decisions (quick and dirty programming) are only beneficial if you can conclusively guarantee that the requirements are exactly correct and will never need to change. In my experience, I've never come across a project where that is true.

Investing the extra time in good practice is investing in the future. Future bugs and changes will be so much easier when the existing codebase is built on good practice. It will already be paying dividends after only two or three changes are made.

  • 1
    This is a good answer, but I should clarify that I'm not saying we abandon good practices, but what level of 'good practices' do we pursue? Is it good practice to abstract your ORM in every project because you 'might' need to swap it out for another one later? I don't think so, there are certain levels of coupling I am willing to accept (ie. I'm tied to the framework, language, ORM, database that was chosen). If we follow SOLID to it's extremist degree, are we really just implementing our own framework on top of the stack chosen? – Igneous01 Sep 27 '18 at 22:32
  • You are denying OP's experience as "fallacy". Not constructive. – max630 Sep 28 '18 at 5:10
  • @max630 I'm not denying it. I spent a good part of the answer explaining why OP's observations are valid. – Flater Sep 28 '18 at 5:35
  • 1
    @Igneous01 SOLID isn't a framework. SOLID is an abstraction, which is more commonly encountered in a framework. When implementing any kind of abstraction (including SOLID), there is always a line of reasonability. You can't just abstract for abstraction's sake, you'd spend ages coming up with code that's so overgeneralized it's hard to follow. Only abstract what you reasonably suspect will be useful to you in the future. However, don't fall into the trap of assuming you're tied to e.g. your current database server. You never know what new database will be released tomorrow. – Flater Sep 28 '18 at 5:47
  • @Igneous01 In other words, you have the right idea by not wanting to abstract everything, but I do get the feeling you're leaning ever so slightly too far in that direction. It's very common for developers to assume that the current requirements are set in stone, and to then make architectural decisions based on that (wishful) assumption. – Flater Sep 28 '18 at 5:49
7

How does SOLID turn simple code into framework code? I'm not a stan for SOLID by any means but it's really not obvious what you mean here.

  • KISS is the essence of the Single Responsibility Principle.
  • There’s nothing in the Open/Closed Principle (at least as I understand it—see Jon Skeet) that goes against writing code to do one thing well. (In fact the more tightly focused the code is, the more important the “closed” part becomes.)
  • The Liskov Substitution Principle doesn’t say you have to let people subclass your classes. It says that if you subclass your classes, your subclasses should fulfill the contract of their superclasses. That’s just good OO design. (And if you don't have any subclasses, it doesn't apply.)
  • KISS is also the essence of the Interface Segregation Principle.
  • The Dependency Inversion Principle is the only one I can see remotely applying, but I think it's widely misunderstood and overblown. It doesn't mean you have to inject everything with Guice or Spring. It just means that you should abstract where appropriate and not depend on implementation details.

I admit I don't think in SOLID terms myself, because I came up through the Gang of Four and Josh Bloch schools of programming, not the Bob Martin school. But I really think that if you think “SOLID” = “adding more layers to the tech stack”, you’re reading it wrong.


P.S. Don't sell the benefits of “better UX for the developer” short. Code spends most of its life in maintenance. A Developer Is You.

  • 1
    Regarding SRP - one could argue that any class with a constructor violates SRP, because you can offload that responsibility to a factory. Regarding OCP - this is really a framework level problem, because once you publish an interface for consumption to outside use, you cannot modify it. If the interface is only used inside your project, then it's possible to change the contract, because you have the power to change the contract within your own code. Regarding ISP - one could argue that an interface should be defined for each individual action (thus preserving SRP), and concerns outside users. – Igneous01 Sep 28 '18 at 3:02
  • 3
    1) one could, but I doubt anyone worth listening ever has. 2) you might be surprised how quickly one project can grow to a size that freely modifying internal interfaces becomes a bad idea. 3) see both 1) and 2). Suffice to say I think you’re reading too much into all three principles. But comments aren’t really the place to address these arguments; I suggest you pose each of them as a separate question and see what kind of answers you get. – David Moles Sep 28 '18 at 3:19
  • 4
    @Igneous01 Using that logic, you might as well abandon getters and setters, as you can make a separate class for every variable setter and one for each getter. IE: class A{ int X; int Y; } class A_setX{ f(A a, int N) { a.X = N; }} class A_getX{ int f(A a) { return X; }} class A_setY ... etc. I think you are looking at it from a too meta point of view with your factory claim. Initialization is not an aspect of the domain problem. – Aaron Sep 28 '18 at 17:36
  • @Aaron This. People can use SOLID to make bad arguments, but that doesn’t mean doing bad things = “following SOLID”. – David Moles Sep 28 '18 at 17:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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