I got myself into this book writing project in part because I went looking for a specific kind of design book and… didn’t find it. If you can’t find it, it’s now your job to create it, right?

Among the successful design aesthetics out there is SOLID (and that’s a link to a pdf of an article introducing many of the ideas, btw). What I’d like to do today is “critique” it, in a sense. Or perhaps, deconstruct and re-evaluate it, in light of the things I’ve been talking about on this blog for awhile. Why isn’t this the kind of stuff I was looking for?

What’s context??

One of the big concepts I can’t shut up about is system boundaries. The reason I can’t stop talking about them is they’re #1 on the list of considerations when it comes to that nebulous “context” and “trade-offs” thing people sometimes hand-wave about. If we’re going to talk shop about design, they’re going to come up constantly.

You can’t talk about what designs are good without considering whether you’re working on a system boundary or not. We must design system boundaries differently than just any other code.

So if you’ve got a list of things that constitute “good design,” and they do not give even the slightest consideration to system boundaries… Well, you start to see where I raise my eyebrows.

Let’s dig in.

S: “single responsibility”

“A module should have only one reason to change.”

This is the most confusingly stated principle in SOLID, in my opinion. I’m not sure many people have a good idea of what it means.

In my post on modularity, we look at what a module consists of. I ended with a note about what I missed, and perhaps should return to with a full post of it’s own… But I digress, here’s the short version:

We often think the important things about a module are what abstractions it exposes, and what dependencies it has. But the two most important questions about a module’s design are:

  1. What must this module NOT expose?
  2. What dependencies must this module NOT have?
  3. (Which creates an implied third:) What other modules must NOT depend upon this module?

More on this in a future post (probably), but this all goes back to an old design classic paper, “On the criteria to be used in decomposing systems into modules.” The underlying idea: find an assumption that might change, and encapsulate that assumption into a module. Then, when it does change, only that module will be affected. This makes changes easier.

The “single responsibility” principle is getting at this same idea, but I think it confuses things. There’s sometimes not a single “responsibility” by itself, and there’s really little reason why exactly one should be the goal. (Especially if we’re conflating, as OO advocates sometimes do, “module” with “class.” Good module designs often encompass multiple classes.) I kind of suspect this phrasing was chosen so it would fit the SOLID acronym.

But there’s potentially another reason for this phrasing, and that’s the common “god-object” dysfunction in object-oriented design. This is, pretty specifically, an OO dysfunction (there no functional programming equivalent that I know of). I suspect it stems a little from the “is-a” relationship idea. My User class is a user, right? Okay, so, authentication goes there because a user authenticates. Okay, so, sessions go there because users have sessions. Okay, so, sending messages go there because users send messages. And so on, to oblivion.

This leads people to break all of the good rules about designing modules, while doing things that seem completely natural and reasonable at the time. But then User knows about everything, everything knows about User, and you’ve got spaghetti.

This is, as I see it, what “single responsibility” is all about. It’s a principle named after a specific OO design dysfunction, but the actual “rules” are really about good module design.

O: “open–closed”

“A module should be open for extension but closed for modification.”

Last year, I used the expression problem to show the different choices we have in type design. When we introduce a type, we have a choice between three good options: data, objects, or abstract data types (ADTs). Each of these come with different trade-offs, different modes of extensibility and future maintainability, and are appropriate in different situations.

And SOLID is here to tell us pretty much “DATA BAD, ALWAYS USE OBJECT.”

Or at least, that’s definitely what I take away from reading its authors. As you can imagine, this is a principle I don’t like at all.

The examples used are always “if we use data, we can’t extend it with new variants, but if we use an interface, we can!” I hope that, having read my piece on type design, people immediately have a problem with that reasoning. After all, now the interface has a fixed list of methods, and that type cannot be extended with more methods without modifying the original module! Shoot! The example of being “open for extension” is actually “closed for extension!”

How was this missed? Well, partly, I think this particular type design trade-off is not as well known as it should be.

I also think part of the explanation is the rapid growth in the number of software engineers that used to be happening when this field was younger. When new programmers are constantly pouring in that fast, it’s hard to raise the average level of technical decision-making capability. If you waved a magic wand and “everyone” knew something, two years later they were a minority again.

Coupled with that, I think there was a disparity of impact with this design decision. Data is often a superior choice to objects, but usually it’s not earth-shattering. (As evidenced by the dreadful support for data in most of our programming languages.) But inappropriate use of data where an interface should have appeared can create a lot of problems. Screaming “always use objects” at a pile of inexperienced developers really may have increased the average quality of the designs they produced. Even if wrong 70% of the time, the 5% of the time it’s really the right choice might be overwhelming.

But I hope we can move past this old advice towards making the right decisions in the right circumstances.

But there’s another serious problem here, and that’s the implicit assumption that extensibility is desirable. One the one hand, we shouldn’t forget that ADTs are a legit design choice, and are deliberately (and desirably) not extensible by users (and as a result, become safely modifiable by maintainers). I even speculate that ADTs are probably more commonly the right type design choice than interfaces!

The bigger issue, though, is the thing I can’t shut up about. Let me quote myself on design goals:

Extensibility and re-usability are potential goals for system boundaries only.

Being “open for extension” is blanket advice that ignores all context. The open-closed principle tells us everything should be extensible, but extensibility is for system boundaries, and we really shouldn’t create system boundaries unnecessarily! Boundaries are expensive. Code that’s not on a boundary is cheap and easily modified. If it’s cheap to modify, it doesn’t need to be extensible, and making it extensible is usually a step backwards in design quality.

We’re much better off designing for easy modification wherever it’s cheap to do so. You even might say we should be “open to modification.”

L: Liskov substitution

“Subclasses should be substitutable for their base classes [without breaking behavior contracts].”

I don’t object to this one at all. Mostly, I object to the casualness with which this principle gets thrown around. I don’t think advocates of SOLID always fully understand the implications, because implementation inheritance routinely violates Liskov substitution (especially when taking variance seriously).

Confronting this reality leads one down the path of “preferring composition over inheritance,” all the way to banning implementation inheritance (almost?) entirely. That might be a good goal, but I’m not sure everyone understands it’s a consequence of this principle.

Almost entirely” because implementation inheritance can (more or less) safely be used when:

  1. Never exposed on a system boundary (even a soft one). Or,
  2. When used internally by an ADT, not an object. (This is sort of a restatement of the above, though.)

That is, inheritance is less dangerous when you fully control its use and can change all users at once as you please. You have a nice closed-world to play in. When such a convenient tool is laying around, you’re probably not going to resist using it in these cases when it’s reasonably non-destructive.

I: interface segregation

“Many client-specific interfaces are better than one general-purpose interface.”

To be honest, I don’t really understand this one very well. Or rather, I think it’s pretty vague and has a few interpretations, which means it’s not exactly one good principle.

One simple interpretation is that “god-interfaces” are just as potentially destructive as “god-objects.” If you’ve read my bit on modules, you can imagine a little bit why: interfaces can create new public dependencies in a module just as well as classes can. A better design is one that controls and limits their scope. So multiple interfaces that allow some public dependencies to be avoided when they’re not necessary can be quite a bit better than a single mega-interface. (I do think thinking in terms of public dependencies is critical here, though. Otherwise we might be left wondering why multiple smaller interfaces are supposedly “better” and when to actually break things up.)

Another interpretation, as best I can see one, concerns what we often see happen with REST APIs, or with plug-in systems, that need to evolve while supporting legacy code. This is a principle that exclusively applies to system boundaries. In this situation, we’re stuck with an interface we can’t change, because it’d break users. So instead of changing it, we introduce a new, “version 2” of the interface, and modifying our code to work regardless of which is actually used.

This allows all the legacy users to be unaffected, while allowing the design to (partially) evolve. It’s only partial, though. Often, having to support the exact behaviors available through the legacy interface makes the most-desired change infeasible. We’re often forced into some compromise.

So this isn’t so much a design principle as it is a tool for coping with hard system boundaries. It’s a tool we’re forced into using, not something we should ever be applying from the start. Except that if we’re to apply this tool, the design needs to be amenable to its use. But this mostly just boils down to being cognizant of what we’re exposing as a system boundary at all.

Another interpretation of this principle boils down to rehashing some of the ideas we talked about with “single responsibility” above, and what we see below with “dependency inversion.” So, I won’t bother.

But there is one thing I’d like to additionally point out: sometimes we’re just suffering from a lack of support for data in our languages. Consider a method that returns an object. On the one hand, we can ask “this method returns this class, but maybe it should be returning a more narrow interface instead?” Or… we might also ask “this method returns a class, but really what we’re after is just some data that happens to be encapsulated by that class. If this language supported data better, I might just return the data type I’m interested in instead of referencing the whole object.”

Sometimes we just want more focused types.

And as a final aside, I almost said “more specific types” in that last sentence, but that’d be wrong, wouldn’t it? After all, returning an interface type instead of a concrete class is “more focused” but technically “less specific” because it could be any class that implements the interface. Subtlety in word choice, that.

D: dependency inversion

“Depend upon abstractions. Do not depend upon concretions.”

This is one of those principles that I think is partly backwards, again. Like the “law” of Demeter, this rule, if followed, encourages you to think about how you’re using code. The correct thing to do here is think about how that code is structured, instead.

Your task is not to “not use concretions.” It’s to not expose the “concretions” that should not be used.

And again, we’re only talking about system boundaries, although in this case, even “soft” system boundaries (of other modules) are in scope, too. As I mentioned in the Demeter post, the real law here is that “anything exposed by a system boundary equally becomes a system boundary.” Don’t get caught unaware.

I also continue to object that many “concretions” are actually abstractions. Advocates of SOLID seem to take the view that only interfaces are abstractions, but any type is, including ADTs and data.

The “inversion” this principle mentions comes about as a result of the old “interfaces cutting dependencies” trick. When you’re trying to hide a dependency (because design demands that a module not depend on another one), an interface can sit in the middle.

So this principle is a bit of a muddle of a few different ideas, and I think thinking in terms of system boundaries (and having a reasonable notion of “module” besides just “class”) is a better approach.

End notes

Today’s post is way long enough, but I also think it might be suffering from a serious lack of examples. For today, oh well. Sorry. :)