When I started coding, I just hacked stuff out any which way. This code was hard to extend, because it had no structure.<p>When I arrived at university, I tried to use all the "appropriate" design patterns and software engineering techniques. This code was hard to extend, because it had 60 separate extension points, all of which were in the wrong place (and none of which had ever been used).<p>When I arrived at my first internship, I killed a project or two though grotesque over-engineering.<p>Today, I'm just happy if the code is simple, readable, and does one thing well, and if it has enough unit tests to prevent bit rot. If I add an extra layer of abstraction, I do it because it makes the code simpler, or because it eliminates duplication.
I've been the elephant in the application architecture room for years. I was opposed to Dependency Injection for years for the primary reason of Complexity, Readability, and Teachability.<p>As a consultant, I have the opportunity to traverse many different development environments managed by diversely capable development teams. This experience has led me to the conclusion that there are many more entry-level, mid-level, and worker-bee developers than senior programmers and architects.<p>So when architects design new systems, you'll see a lot of highly complex, loosely-coupled code that's simply unreadable, and no amount of "knowledge-transfer" will bridge the gap with the wider audience of developers who are not as skilled.<p>You end up with those mid-level developers altering code in ways that break the original intent with the primary concern of being productive and completing tasks. I can't tell you how many times I've had to unravel shoddy code baked on top of or into an otherwise "normal" architecture.<p>This is why I eventually postulated that complexity trumps loosely-coupled architectures. Our "customer" as architects are those mid-level developers. We need to build frameworks and code bases that _anyone_ can maintain and enhance.<p>So let's change it to SOLID-C. SOLID principals minus the Complexity. If we can achieve that, everyone will succeed.
If you learn only 1 thing from SOLID, it would be the Single Responsibility Principle (SRP). Honestly, this is over 80% of the value of SOLID.<p>The Interface Segregation Principle is a special case of SRP, and Open-Closed Principle and Liskov Substitution Principle are most applicable to deep inheritance hierarchies, which are rarer than they were. SRP pushes you towards "composition over inheritance" which is also good.<p>Yes, using a lot of interfaces and an IoC container does push you towards a particular style, but it's not that hard to read once you know it.
It's hard not to empathize with the author. However, I think Sandi Metz put it well in Practical Object-Oriented Design In Ruby when she said:<p>"Concrete code is easy to understand but costly to extend. Abstract code may initially seem more obscure but, once understood, is far easier to change."<p>So, as with most things in life, it's all about balance: Readability vs Maintainability.
I believe a lot of these principles came out of the observation that some good codebases followed them, and this was taken as a sign that <i>all</i> good codebases should; it's a sort of "if X is good, then not doing X is bad" type of fallacy. If you try to analyse them in detail they all have an element of subjectivity and vagueness (e.g. "what is a 'responsibility'?" ) that tends to encourage <i>over</i>abstraction and unnecessary, misguided extensibility. Blind, dogmatic application of a set of principles with little reasoning behind them is basically cargo-cult-programming in disguise. Trying to follow the indirections in such a codebase where SOLID has been applied liberally, which is particularly troublesome when debugging, does <i>not</i> make it any easier to maintain or extend.<p>There is no replacement for careful thought (including foresight) and pragmatic design.
I think a big issue is that a lot of these rules need to be revised as language features evolve. A lot of productive paradigms are no longer "pure" code.<p>I see a lot of comments kindof poo-pooing annotations, but one of the better Java devs I know is convinced that annotations are the solution to code readability/overengineering in Java -- in essence, custom annotation processors can replace both the need for interfaces and abstract classes.<p>One of the biggest problems I see is simply that the schism-ing of modern design paradigms means that debugging tools have to play catchup and therefore make code seem a bit less linear. But the reality is, through IoC/AOP/annotations, developers are often reducing the number or interfaces to traverse and making the code more readable, while at the same time actually making it <i>more</i> generic (your class doesn't have to conform to so many standards if you can tack the annotations on whatever fields/methods you want). Should someone be introducing a proxy layer for every class just in case they need to fit it into a more advanced design in the future? Or would it be easier to just code more literally while language/container designers work on a more seamless replacement method?<p>In a way, it does just seem like some of these new techniques are a hacky way of forcing FP into OOP. Lots of different design paradigms playing nicely within the same VMs, ecosystems is a nice problem to have though. :)
I came to the conclusion a while ago that IOC containers are a real "two problems" solution. I still heavily use constructor injection in my code, but I wire the constructors by hand. Keeps you honest and actually forces you to think through abstractions more clearly.
In general I think that a lot of these design "principles" are just mislabeled. A better label is generally "rough hewn guideline to use in a first pass at design". Many of them contradict each other, or even themselves, especially when rotely applied beyond sensibility.<p>Take for instance DRY - if you follow it too far, you end up with InterfaceFactoryFactoryFoo. And of course all those FactoryFactories start to look like violations of DRY anyway.<p>Or the over-application of SRP ends up with 40 classes that are tiny slices of something that could easily be 1 class.<p>Amusingly both are the result of myopic application, going fractal if you will, on the principle, rather than setting a decent "scope" for the application of the ideas.<p>Further as you go through design and implementation, you find places where the design abstractions were wrong, and the "single thing" or "unrepeated task" is violated, in the large (rather than in the tiny) and you have to accept it or do some refactoring. Such is life.<p>None of these things takes away the value of DRY or SOLID or any of the other design principles - it's just that there is a very hard orthogonal problem of "proper scoping" for these principles.
I always struggled to understand the appeal of the open/closed principle.<p>"The idea was that once completed, the implementation of a class could only be modified to correct errors; new or changed features would require that a different class be created."[1]<p>This sounds a lot like bolt-on coding, always adding code rather than assimilating new features into a codebase. This doesn't seem like a sustainable strategy at all. Yes you don't risk breaking any existing functionality but then why not just use a test suite? The major problem though is that instead of grouping associated functionality into concepts (OO) that are easy to reason about, you are arbitrarily packaging up functionality based upon the time of it's implementation... (subclassing to extend).<p>[1] <a href="http://en.wikipedia.org/wiki/Open/closed_principle" rel="nofollow">http://en.wikipedia.org/wiki/Open/closed_principle</a>
I personally thought the follow up to this article. Available at:<p><a href="http://qualityisspeed.blogspot.com/2014/09/beyond-solid-dependency-elimination.html" rel="nofollow">http://qualityisspeed.blogspot.com/2014/09/beyond-solid-depe...</a><p>Was way better than this one.
I would add "don't repeat yourself" as another not-so-good rule of thumb. In one of my past projects one of the most serious problems we faced was too much generalisation on early phase of development when requirements at this time were quite straight forward, but when we meet with real need of using "advantages" of our generalisation and avoidance of repeating things... well, it didn't work smoothly and even maintanance of over-engineered libraries were harder.<p>Of course of I am not saying that we should copy-paste everything, but adding a lot of layers of abstractions also doesn't seem neat.
Personally, I find that taking a very aggressive approach to the Liskov substitution principle is the most helpful rule of thumb. I follow it to the point of practically never using inheritance. If you are using inheritance to modify behavior, then you are probably ignoring LSP and making your code "unintelligible".
I think this is about finding a balance between pragmatism and perfectionism. We should strive to find the simplest, cleanest solution to problems and continually refactor to keep complexity at bay. The SOLID guidelines, practically applied, may help in achieving that goal, so they are worth having in your toolbox.
I think this just highlights the ridiculousness of software development.<p>You can get two highly skilled, well renowned software developers and they can completely disagree.<p>How are you meant to objectively evaluate code quality of developers then?
Reading this article and its sequel, I feel like the author is handwaving one key bit. I absolutely agree with his position on dependency elimination as a primary goal, but by saying "Oh, a class that operates on a dependency is hard, so let's not write those", and "We're not going to deal with interfaces" he's punting on the entire problem -- handwaving the hard bits and then ignoring the fact that they exist.<p>At some point, your code does have dependencies. The entire purpose of an interface is to be able to specify what your dependencies are -- to be able to say "This is the smallest thing I need in order to be able to work". When untangling dependencies, adding that bit in there makes it very clear what the seams are -- where you can say "I depend on something that does Foo -- Feel free to replace it", rather than "I depend on this thing that comes entangled in its own network of dependencies".