SOLID Code isn't by Mark Seemann
Recently I had an interesting conversation with a developer at my current client, about how the SOLID principles would impact their code base. The client wants to write SOLID code - who doesn't? It's a beautiful acronym that fully demonstrates the power of catchy terminology.
However, when you start to outline what it actually means people become uneasy. At the point where the discussion became interesting, I had already sketched my view on encapsulation. However, the client's current code base is designed around validation at the perimeter. Most of the classes in the Domain Model are actually internal and implicitly trust input.
We were actually discussing Test-Driven Development, and I had already told them that they should only test against the public API of their code base. The discussion went something like this (I'm hoping I'm not making my ‘opponent' sound dumb, because the real developer I talked to was anything but):
Client: "That would mean that each and every class we expose must validate input!"
Me: "Yes…?"
Client: "That would be a lot of extra work."
Me: "Would it? Why is that?"
Client: "The input that we deal with consist of complex data structures, and we must validate that all values are present and correct."
Me: "Assume that input is SOLID as well. This would mean that each input instance can be assumed to be in a valid state because that would be its own responsibility. Given that, what would validation really mean?"
Client: "I'm not sure I understand what you mean…"
Me: "Assuming that the input instance is a self-validating reference type, what could possibly go wrong?"
Client: "The instance might be null…"
Me: "Yes. Anything else?"
Client: "Not that I can think of…"
Me: "Me neither. This means that while you must add more code to implement proper encapsulation, it's really trivial code. It's just some Guard Clauses."
Client: "But isn't it still gold plating?"
Me: "Not really, because we are designing for change in the general sense. We know that we can't predict specific change, but I can guarantee you that change requests will occur. Instead of trying to predict specific changes and design variability in those specific places, we simply put interfaces around everything because the cost of doing so is really low. This means that when change does happen, we already have Seams in the right places."
Client: "How does SOLID help with that?"
Me: "A result of the Single Responsibility Principle is that each self-encapsulated class becomes really small, and there will be a lot of them."
Client: "Lots of classes… I'm not sure I'm comfortable with that. Doesn't it make it much harder to find what you need?"
Me: "I don't think so. Each class is very small, so although you have many of them, understanding what each one does is easy. In my experience this is a lot easier than trying to figure out what a big class with thousands of lines of code does. When you have few big classes, your object model might look something like this:"
"There's a few objects and they kind of fit together to form the overall picture. However, if you need to change something, you'll need to substantially change the shape of each of those objects. That's a lot of work, and this is why such an object design isn't particularly adaptable to change.
"With SOLID, on the other hand, you have lots of small-grained objects which you can easily re-arrange to match new requirements:"
And that's when it hit me: SOLID code isn't really solid at all. I'm not a material scientist, but to me a solid indicates a rigid structure. In essence a structure where the particles are tightly locked to each other and can't easily move about.
However, when thinking about SOLID code, it actually helps to think about it more like a liquid (although perhaps a rather viscous one). Each class has much more room to maneuver because it is small and fits together with other classes in many different ways. It's clear that when you push an analogy too far, it breaks apart.
Still, a closing anecdote is appropriate...
My (then) three-year old son one day handed me a handful of Duplo bricks and asked me to build him a dragon. If you've ever tried to build anything out of Duplo you'll know that the ‘resolution' of the bricks is rather coarse-grained. Given that ‘a handful' for a three-year old isn't a lot of bricks, this was quite a challenge. Fortunately, I had an appreciative audience with quite a bit of imagination, so I was able to put the few bricks together in a way that satisfied my son.
Still, building a dragon of comparable size out of Lego bricks is much easier because the bricks have a much finer ‘resolution'. SOLID code is more comparable to Lego than Duplo.
Comments
http://bit.ly/ik3dCW
Functional units of small size are key to evolvable code. This makes for a fine grained "code sand" which can be brought into ever changing shapes.
The question however is: What is to become smaller? Classes or methods?
I´d say it´s classes that need to be limited in size, maybe 50 to 100 LOC.
Classes are to be kept small because they are the "blueprints" of the smallest stateful runtime units of code which can be recombined: objects.
However this leads to two problems:
1. It´s difficult to think of a decomposition of the very common noun-classes into smaller classes. How to break up a PaymentService class into many smaller classes?
2. Small classes would lead to an explosion of dependencies between all of these classes. They would be hard to manage (even with dependency injection).
That´s two reasons why most developers will probably resists decreasing the size of their classes. They´ll just keep methods small - but the effect of this will be limited. There´s no limit to the size of a class; 1,000 LOC class can consist of 100 methods each 10 lines long.
But the two problems go away if SOLID is applied in combination with a different view on object orientation.
We just need to switch from nouns to verbs when thinking about domain logic classes (not data classes).
If classes become functions (or maybe better behaviors) then there is no decomposition problem anymore. Behaviors can be as small as needed, maybe just 1 LOC.
Also behaviors conceptually don´t have any dependencies on each other (but maybe to an environment); sitting is not dependend on running even though running might "be done" after sitting.
"I certainly wouldn't want to drive a Lego car or walk across a Lego bridge"
ROTFL
Great quote! I've always liked the principles behind solid, but have always been a little turned off by some of the zealotry associated with it. Like most everything in life it's great "in moderation".
Thanks for your article.
You are describing a philosophy
which is almost standard practice
in Smalltalk since the 80's..
As you know, albeit desired, it is
however not always possible to
make small classes.
Recommended reading:
(also for OO in general)
"Smalltalk, Objects and Design"
by: Chamond Liu
ISBN: 1-8847777-27-9
Manning Publications Co, 1996.
Kind Regards
Ted
..btw I am sure that there must be
a photo of yours whereon you look a bit happier?
It's my experience that there is lots of discussion around the concept of abstraction and too little discussion around the use and practice of abstraction.
I interview many software development candidates, and I always explore what I consider foundational concepts - one is abstraction. I find most developers / programmers / software engineers (/ whatever), cannot adequately communicate HOW they use abstraction to enable their code - not on an architectural level, not on a modular level, not on a class level, and not even on a simple functional level. This is a real indication of the state of software design and it is alarming to me.
It's my belief that designing abstraction in software engineering is the most critical tool in software construction, but it is the least discussed.
So since there is no real expression of abstraction (except for inheritance) in code, it´s neglected or left to personal style. Sad.
I’m not convinced that the cost is indeed "really low", for two reasons.
1) Interfaces, before TDD became popular, imparted information to the maintainer of a type because they were used to indicate, for instance, that a given type was to be treated like a collection that could be enumerated or a type that could be compared with other types for the purposes of, say, sorting. When added reflexively as a means of inserting a level of indirection, the interface no longer imparts any information.
2) When navigating around a large code base where nearly all types are accessed via a level of indirection (courtesy of ubiquitous interfaces) my IDE – Visual Studio 2010 – struggles to answer common questions like "what code will be executed when SomeType.ReadHeader() is called?". Hitting F12 doesn’t take me to the definition of the method but rather takes me to the definition of the interface which, because of the point above, is of no value. ReSharper can sometimes find the method with a more advanced search but not always. The upshot of this is that code making heavy use of interfaces becomes much harder to statically analyse.
Agreed. That point is manifest in @Dave's subsequent post. So, how would the lack of direct language support for abstraction - other than inheritance - be solved? And how would code tracing / debugging interfaces and their implementations be handled?
My gut tells me this is one reason (of several) that scripting-like languages have gained popularity in the recent past, and that we are being pointed in a more real-time, interpreted language direction due to the necessary dynamic nature of these constructs. Hm... {thinking}
Love a blog that makes me think for more than a few seconds.
"So, how would the lack of direct language support for abstraction - other than inheritance - be solved?"
and I don´t know if we should hope for languages to offer support soon. We´ve to live with what we have: Java, C#, C++, Ruby etc. (Scripting languages to me don´t offer better support for abstraction.)
Instead we need to figure out how to use whatever features these languages offer in a way to express abstraction - and I don´t mean inheritance ;-)
And that leads to a separation of mental model from technical features. We need to think in a certain way about our solutions - and then translate our mental models to language reality.
Object orientation tried to unify mental model and code reality. But as all those maintenance nightmare projects show, this has not really worked out as nicely as expected. The combination of OO method (OOAD) and OO programming has not lived up to our expectations.
Since we´re pretty much stuck with OO languages we need to replace the other part of the pair, the method, I´d say.
Very interesting article...