Why I Don't Teach SOLID
qualityisspeed.blogspot.com
qualityisspeed.blogspot.com
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).
When I arrived at my first internship, I killed a project or two though grotesque over-engineering.
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 working with "enterprise" systems for ~20 years and I swear that most of the extension points I've seen in custom-built systems have never been used to extend anything and simply complicate the original system.
Of course, a lot of products (particularly EPR and CRM) are extensible - sometimes quite elegantly, sometimes with Lovecraftian horrors, but at least they actually get used.
You start with spaghetti code which just works (hopefully), after a while you are taught how to do things 'properly', resist to it a bit and suddenly you have an epiphany - design patterns should be everywhere, etc. After over-engineering some projects, with loads of UML diagrams and stuff, you step back and stick to simplicity.
And I believe it's a good curve. You learn why some certain things (like overengineering) doesn't work, hopefully it happens early, and learning from trial and error is much more valuable compared to when someones teaches you how to do things 'properly'.
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.
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.
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.
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.
So let's change it to SOLID-C. SOLID principals minus the Complexity. If we can achieve that, everyone will succeed.
Yet here we are belabored by whole frameworks to achieve it.
But that's only a band-aid. It's my experience that they still won't truly understand why they can't just call A from B and they certainly don't understand how to get B in A through an injection.
function app(injected) {
return injected.doSomething();
}
function main() {
var injectable = new Injectable();
var result = app(injectable);
console.log(result);
}
Everything else is around obscuring that core pattern or making it "convenient" by providing some kind of naming abstraction. That at least makes a little bit of sense in Javascript, like above, since you need the stringy names to simulate types... but in any language with nominal typing you're just killing yourself with unnecessary complexity.But I don't see how the relationship of contravariant functors to OO-style subclasses is as clear as the DI <-> function abstraction example (functions are just functions in OO or FP)
So let's say that Apple is a subtype of Fruit. That means that there exists a natural coercion function f : Apple => Fruit. It also means that if we have a predicate Delicious that we define for both Fruit and Apple then the answer Delicious[Apple] must be exactly Delicious[Fruit].comap(f). If they don't align then your coercion function is somehow wrong and not implementing subtyping properly.
So then the company solves the problem by having a higher standard for hiring new developers. Eventually this becomes unsupportable because finding developers who can pick and use an in-house framework is costly and often impossible.
Keep it simple overrides make it SOLID.
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.
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.
Not true if there are laws specifying the relationship between the methods. But way more often true than you'd expect from reading a (good) java codebase.
I saw some Java 8 recently. The automatic conversion from a lambda to an interface with one (compatible) method was interesting, but yeah, it points to the problem that what you sometimes really want is just a function.
Interestingly, this fact has been known to procedural as well as functional programming proponents for decades (one of the few aspects on which they agree).
> sometimes really want is just a function
summed up nicely here: http://blog.ploeh.dk/2014/03/10/solid-the-next-step-is-funct...
Sure it's hard to automate it. It's one of those subjective, experience-based factors that keep humans like me in a job. I never thought that it was a rule that can be mechanically applied.
The Liskov Substitution Principle is applicable wherever inheritance is used, and failing to follow it anywhere when using inheritance pretty much guarantees bugs will eventually emerge. If its "most applicable to deep inheritance hierarchies", its because the cost of finding and fixing the source of the bugs resulting from violating the LSP is greatest in such hierarchies.
Most people try to at least ad hoc'edly treat "inherits from" as "is subtype of". Many typed languages even encode this directly. This is a total lie unless LSP is followed, however. LSP doesn't do much more than actually define what the necessary and sufficient properties of the "is subtype of" are.
"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."
So, as with most things in life, it's all about balance: Readability vs Maintainability.
There is no replacement for careful thought (including foresight) and pragmatic design.
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.
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 more 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?
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. :)
There's normally section at the start like
container.RegisterType<IMessageQueue, MSMQMessageQueue>
container.RegisterType<IGeocoder, GoogleGeocoder>
Modern containers don't even use annotations, they just scan the constructor parameters.
I've been on one project that manually wired everything, it was crazy. such a waste of time.
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.
Or the over-application of SRP ends up with 40 classes that are tiny slices of something that could easily be 1 class.
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.
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.
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.
Right. DRY runs into limits when you use languages with limited expressiveness -- and, particularly, Java-like class-oriented languages where classes are not, themselves, first-class are problematic here.
The problem isn't with the DRY principle, its with a language that doesn't really let you follow it, because certain things cannot be effectively abstracted out into reusable library code and require boilerplate. Most of these things are not problems with, e.g., Lisps.
This is where the Lisp macros start to shine - you can DRY up the code structure itself. The need for that doesn't come that often (I'm personally in the camp of avoiding macros until you're really sure they're the best tool for the job) but when you are really starting to get sick of that repetitiveness that obscure the intention behind your code, macros are really godsend.
"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]
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).
> ...you don't risk breaking any existing functionality but
> then why not just use a test suite?
The examples fail to make explicit that there are two programmers: One is building and distributing a library, the second is building an application using that library.The library programmer can easily distribute the test suite so that the application programmer can run the tests, but that doesn't change the fact that if the library programmer changes an object's interface, it breaks the application programmer's code. By committing to keep the old object's interface intact, the library programmer is giving the application programmer time to migrate their code to the new objects.
For applications I'm not sure open/closed makes as much sense - http://codeofrob.com/entries/my-relationship-with-solid---th...
> For applications I'm not sure open/closed makes as
> much sense.
I agree with you. If the same programmer (or team) is maintaining "both sides" of an object's interface, i.e. both implementing the behavior of the object AND consuming the object in their application code, I think we can assume that they'll know that if they change one side they'll need to immediately change the other.I'm not sure I agree with Rob Ashton's points, though. In his blog post, Rob trivializes the utility of third-party libraries:
> * These [libraries] are either replicable in a few
> hours work, or easily extended via a pull request.
This is simply not the case with any truly useful library. Useful libraries often represent years of careful design work and debugging. (Think networking libraries, UI frameworks, etc.)He also underestimates the amount of time it takes to continuously change application code to keep up with breaking changes from third-party libraries:
> * These [libraries] can be forked for your
> project and changes merged from upstream with
> little effort.
Again, if the library distributes a breaking change, it may require many, many hours of code changes and re-testing to make sure everything's still working properly. Hours that could be spent building new features. For that kind of tradeoff there'd better be a damn good reason for the change: Improved security or performance or ease of use.I must say that's my least favorite argument as to why something is important...
http://qualityisspeed.blogspot.com/2014/09/beyond-solid-depe...
Was way better than this one.
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.
You can get two highly skilled, well renowned software developers and they can completely disagree.
How are you meant to objectively evaluate code quality of developers then?
Suggested reading: http://thecodelesscode.com/case/154
My worst experiences encountering other people's code have been the code that followed the best practices, and defensively shielded itself from criticism. Layers of interfaces and injections and abstractions and separation of concerns yielding hundreds to hundreds of thousands of artifacts for the simplest task, always sold on the notion that it was ready for the future, but in reality would never be adapted to the future.
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".