CUPID – the back story

“If you had to offer some principles for modern software development, which would you choose?”

At a recent Extreme Tuesday Club (XTC) virtual meet-up, we were discussing whether the SOLID principles are outdated. A while ago I gave a tongue-in-cheek talk on the topic, so ahead of the meet-up one of the organisers asked what principles I would replace SOLID with since I disagreed with them. I have been thinking about this for some time and I proposed five of my own, which form the acronym CUPID.

This article isn’t about those principles, that will be my next post. This is about why I think we need them. I want to share the back story and explain why I’ve never really bought into SOLID. To do that, let’s talk about the talk.

Why every single element of SOLID is wrong

PubConf was invented as a kind of afterparty to the NDC conferences. As the name suggests, it takes place in a pub. Several speakers give an Ignite-style talk – 20 slides, 15 seconds per slide, auto-advancing – and the audience roars, stamps, claps and thunders its approval accordingly. The winner gets something and everyone has a great time.

A few years ago I was invited to speak at a PubConf event in London. I like the challenge of a constrained talk. This one had to be drunk-people funny and Ignite-shaped. I had been thinking about Robert C. Martin’s SOLID principles, and in the spirit of “it depends” I thought it would be fun to see whether I could refute each principle with a straight face. I also wanted to propose an alternative in each case.

Now some talks write themselves: I figured I could use one slide to introduce each principle, one to challenge it, one to pitch an alternative, five times. That’s 15 slides, with 45 seconds per principle. Top-and-tail it, and there were my 20 slides!

As I wrote the talk I noticed two things. First, it was much easier to refute each principle than I thought (apart from Liskov’s Substitution Principle, so I had to tackle that a different way). Second, the alternative kept turning out to be the same thing: Write simple code. It is easy to challenge that with “What does ‘simple’ even mean?” but I had a good working definition for that so I wasn’t too worried.

After the conference I put the slides up on SpeakerDeck and a whole load of people I have never met started attacking first the premise of the talk, then the detail of slides from a talk they never heard me give, then me personally.

Since I’ve never written it up, here is roughly how the talk went. Bear in mind that for each principle, I had 15 seconds to introduce it, 15 seconds to challenge it, and 15 seconds to propose an alternative. Ready? Go!

Single Responsibility Principle

The Single Responsibility Principle says that code should only do one thing. Another framing is that it should have “one reason to change”. I called this the “Pointlessly Vague Principle”. What is one thing anyway? Is ETL – Extract-Transform-Load – one thing (a DataProcessor) or three things? Any non-trivial code can have any number of reasons to change, which may or may not include the one you had in mind, so again this doesn’t make much sense to me.

Instead I suggested to write simple code using the heuristic that it “Fits In My Head”. What does that mean? You can only reason about something if it fits in your head. Conversely, if something doesn’t fit in your head, you can’t reason about it. Code should fit in your head at any level of granularity, whether it is at method/function level, class/module level, components made up of classes, or entire distributed applications.

You might ask “Whose head?” For the purpose of the heuristic I assume the owner of the head can read and write idiomatic code in whichever languages are in play, and that they are familiar with the problem domain. If they need more esoteric knowledge than that, for instance knowing which of the many undocumented internal systems we need to integrate with to get any work done, then that should be made explicit in the code so that it will fit in their head.

At each scale there should be enough conceptual integrity that you can grasp “the whole” at that level. If you can’t, then that is a heuristic to strive for in your restructuring activities. Sometimes you can bundle several things together and they still fit in your head. The bundling even makes them easier to reason about than if they are artificially split out because someone insisted on Single Responsibility. In other cases, it makes sense to decompose a single responsibility artificially into several steps just to make each one easier to reason about.

Open-Closed Principle

This is the idea that code should be open for extension, i.e. easy to extend without changing, and closed for modification, i.e. you can trust what it does so you don’t need to go in and tinker with it.

This was sage advice in an age where code was:

  • expensive to change: Try making a small change and then compiling and linking a few million lines of C++ in the 1990s. I’ll wait.
  • risky to change, because we hadn’t figured out refactoring yet, never mind refactoring IDEs (outside of Smalltalk) or example-guided programming.
  • mostly additive: You would write some code, check it in (if you were down with the kids and using a version control system like RCS or SCCS), and then move on to the next file. You were translating the detailed functional spec into code, one lump at a time. Renaming things was uncommon; renaming files doubly so. CVS, which became the ubiquitous source control system, would literally forget the entire history of a file if you renamed it, it was such an uncommon activity. This is easy to overlook in an age of automated refactoring, and changeset-based version control.

Nowadays, the equivalent advice if you need code to do something else is: Change the code to make it do something else! It sounds trite, but we think of code as malleable now like clay, where in the Olden Days the metaphor was more like building blocks. There was no feedback loop between The Spec and The Code like we have with automated examples.

In this case I railed against the “Cruft Accretion Principle”. Code is not an “asset” to be carefully shrink-wrapped and preserved, but a cost, a debt. All code is cost. So if I can take a big pile of existing cost and replace it with a smaller more specific cost, then I’m winning at code! Write simple code that is easy to change, and you have code that is both open and closed, however you need it.

Liskov Substitution Principle

This is just the Principle of Least Surprise applied to code substitution, and as such is pretty sensible. If I tell you something is a valid subtype of the thing you have, then you should be able to assume it will act the same in any sense that you care about.

However, the language LSP uses of “subtypes”, coupled with the way most developers conflate subtypes with subclasses, and the vagaries of “desirable properties”, means that it tends to evoke the class-based inheritance language of “is-a” and “has-a“, and its corresponding 1980s entity modelling.

In the spirit of using a butter knife as a screwdriver, many objects can “act-like-a” or “sometimes-be-used-as-a” or “pass-off-as-a-if-you-squint“. In this context what we really want is small, simple types that we can compose into whatever more complex structures we need, and to make our peace with all the nuances that go along with that. My advice, quelle surprise, is to “write simple code” that is easy to reason about.

Interface Segregation Principle

This really is fish-in-a-barrel as principles go. For some reason, this one caused the most controversy, but to me it is the easiest to debunk. While researching this talk I discovered that this pattern came about when Robert C. Martin was wrangling a God object in the middle of some printing software at Xerox. Everything was happening in a class called Job. His approach to simplifying it was to find each place where it was used, figure out which methods “went together” and put those in an intermediate interface. This had several immediate benefits:

  • Collecting related methods into different interfaces showed all the different responsibilities the Job class was performing.
  • Giving each interface an intention-revealing name made the code easier to reason about than just passing a Job object around.
  • It created the option to break the Job class out into smaller classes fronted by each interface. (Arguably they didn’t need the interface any more now.)

All of this makes sense, it’s just that it isn’t a principle, it is a pattern. A principle is something that is generally good advice in any context: Seek first to understand, then to be understood; Be excellent to each other.

A pattern is a strategy that works in a given context (God class) that has benefits (smaller components) and trade-offs (more separate things to manage). The principle would have been about not getting into that mess in the first place!

Thus I argued that if this were a principle at all, it was the “Stable Door Principle”. If you had small, role-based classes in the first place, you wouldn’t be in the position of trying to decompose a huge, tangled mess.

Sure, we may find ourselves in that context from time to time, and when we do, interface segregation is a perfectly cromulent strategy for slicing your way towards sanity, along with building a suite of characterisation tests and all of the other advice in Mike Feathers’ brilliant Working Effectively With Legacy Code.

Dependency Inversion Principle

While there is nothing fundamentally wrong with DIP, I don’t think it is an overstatement to say that our obsession with dependency inversion has single-handedly caused billions of dollars in irretrievable sunk cost and waste over the last couple of decades.

The real principle here is option inversion. A dependency is only interesting when there might be multiple ways of providing it, and you only need to invert the relationship when you believe the wiring is important enough to become a separate concern. That’s quite a high bar, and mostly all you ever need is a main method.

If instead you subscribe to the idea that all dependencies should be inverted all the time, you end up with J2EE, OSGi, Spring, or any other “declarative assembly” framework where the structuring of the components is itself a twisty maze of config. J2EE deserves a special mention for deciding that each type of dependency inversion – EJBs, servlets, web domains, remote service location, even the configuration configuration – should be owned by different roles.

In the wild, there are entire shadow codebases where each class is backed by exactly one interface, which only exists to satisfy a wiring framework or to inject a mock or stub for automated testing theatre. The promise of “you can just swap out the database” evaporates as soon as you try to, well, swap out the database.

Most dependencies don’t need inverting, because most dependencies aren’t options, they are just the way we are going to do it this time. So my – by now entirely unsurprising – suggestion is to write simple code, by focusing on use rather than reuse.

“If you don’t like them, I have others”

When I look at SOLID, I see a mix of things that were once good advice, patterns that apply in a context, and advice that is easy to misapply. I wouldn’t offer any of it as context-free advice to new programmers. So what would I do instead? I thought there might be a one-to-one correspondence for each of the SOLID principles and patterns, since there is nothing inherently bad or wrong with any of them, but as the saying goes, “If I were going to Dublin, I wouldn’t start from here.”

So, given what I have learned about software development over the last 30 years, are there any principles that I would offer instead? And could they form a pithy acronym? The answer is in yes, and I will outline them in the next article.

Editorial note: I reserve the right to censor any comments that I do not think are constructive or moving the conversation forward.

33 comments

  1. everythingfunctional · · Reply

    There is a meta-principle at work here that I try to make explicit when I’m teaching. Every tip/trick/technique/principle/pattern I’m about to teach you has tradeoffs. Nothing is so dangerous as an idea when it is the only one you have. Couple that with YAGNI, and I think that covers the theme of this post.

  2. I don’t fundamentally disagree with your thesis, but i sort of have the same problem with it that i have with the idea that we can improve security by training users. If we could rely on developers of varying skill levels to translate complex (either because of actual complexity, poor definition or rapid mutation) requirements into simple code it would have started happening by now.

    Either way, looking forward to seeing your principles.

  3. […] CUPID – the back story (Daniel Terhorst-North) […]

  4. Grzegorz Gałęzowski · · Reply

    What do you think about Uncle Bob’s take on your recent presentation about SOLID? I mean the one from his blog: https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html

    1. Robert was responding to some slides he saw, with no other context for the talk, and he chose not to contact me while writing his post, so what you are reading is at least two levels indirected.

      As with anything I think be gets some things right and some things wrong in his response. He says “We don’t mix X with Y” where I often do, to start with, while it still fits in my head. To separate out e.g. logic from presentation prematurely often leads to unnecessary complexity and in retrospect, the wrong abstractions. Maybe I want lots of little chunks of UI+logic. So I let the garden get a bit messy, to paraphrase Ward Cunningham, and then I refactor towards what emerges. Likewise with his take on my take on OCP and SRP.

      He makes a logical fallacy with my take on ISP. I say you shouldn’t have unnecessary interfaces in the first place, not that you shouldn’t keep necessary interfaces small. Likewise, his assertion that “It is hard to imagine an architecture that does not make significant use of [DIP]” says more to me about his imagination than what I consider good software design. That’s not a dig, that is an acknowledgement that his four or five decades of programming experience have given him a paradigm of deeply held beliefs which works for him, and that I happen to no longer share them.

      1. Grzegorz Gałęzowski · ·

        Thank you for responding. The egoistic part of me wants something like the Chicago vs London confrontational video series that Robert C. Martin did with Sandro Mancuso. I learned a lot from that one, so “SOLID vs CUPID” with you two is something I’d gladly pay to watch. For now, I suppose, that’l have to stay in my dreams.

  5. Alperen Belgic · · Reply

    There are multiple interpretations of SOLID, this actually somehow explains it is not that solid. But, for Dependency Inversion, the reason I disagree is that, if you don’t plug the dependency from outside, you can’t make it testable, the responsibility of the class becomes vague, and also some parts of the interactions with the outside world aren’t visible to its consumers. All of these causes less maintainable code over time, in my opinion.

    1. The idea that a dependency has to be injected to make it testable may be a limiting belief. Is it always true? Why can’t I test the component as a whole, with its batteries included? If I can’t then perhaps the component is doing too much, or perhaps I’m not being creative enough with my testing.

      1. What puzzles me is that you say “The promise of “you can just swap out the database” evaporates as soon as you try to, well, swap out the database.” but I find that’s exactly what I’m swapping out! I find that it’s exactly the database that I need to swap out for my tests (much easier to provide an in-memory sqlite for tests than it is to e.g. run queries against a DynamoDB that only exists in cloud anyway).

        I agree with component testing, “batteries included”. I believe that the “testing pyramid” should not be a pyramid at all (you should have a few integration tests, a big bunch of component tests, and a few unit tests). But how do you do component testing without swapping out the DB?

  6. “most dependencies aren’t options, they are just the way we are going to do it this time.”
    This seems almost always like the first point of questioning whenever a new dependency is needed (which in itself contends with open close principle).
    DI/IOC is a sensible modern development principle that allows for efficient compartmentalizing of suitable components (through library seperation, numbers, etc), but very often it gets used through “this is just how it’s done” mentality also.
    I enjoyed this blog and also the boldness of challenging the status quo!
    Looking forward to the CUPID post!

  7. Kevlin Henney did a talk a about this some time ago. It seems you agree, but perhaps with slightly different arguments. Found two versions:
    https://www.youtube.com/watch?v=tMW08JkFrBA and https://vimeo.com/157708450

  8. John Carter · · Reply

    https://www.artima.com/articles/the-c-style-sweet-spot#part3

    Bjarne Stroustrup: My rule of thumb is that you should have a real class with an interface and a hidden representation if and only if you can consider an invariant for the class.

    And that is the hard, mathematical, overarching principle. LSP, when expressed in terms of the class invariants is obvious… get it wrong, you obviously have a bug.

    SRP should be thought of in terms of “Can you decompose the class (and it’s invariant) into subclasses (and the invariant into subexpressions)?”

    OCP again… think about it in terms of what is happening to the invariant?

    In any real world code there are invariants all over the place, if you don’t know what they are… you are writing bugs all over the place. Pretty much by definition in fact.

    Think of a class invariant as defining the “valid region”, the set of all possible states an object can be in, for which every public method works correctly.

    ie. Step out of the valid region… you guarantee that you have a bug (by definition)… ie. Some public method, for some valid parameters, will fail.

    It’s as simple and as hard as that.

    Stop thinking of SRP, LSP, OCP, Law of Demeter as wishy washy “design guidelines” and start thinking in terms of building bug free programs out of bug free classes.

    As stated (except for LSP) they are wishy washy. But underneath is the hard steel of “class invariant doesn’t hold” === “you have hit a bug”.

    Code is brittle and inflexible because people write code that “works by accident” for the handful cases they tested, instead of all valid uses.

  9. […] >> CUPID – The Back Story [dannorth.net] […]

  10. […] CUPID – the back story – Daniel Terhorst-North […]

  11. […] >> CUPID – The Back Story [dannorth.net] […]

  12. I totally agree!!

  13. Ignazio Calò · · Reply

    I agree and sadly I see only “seasoned” developer share this vision.
    Jung devs are usually “SOLID or it is crap” while more experienced engineer knows that everything has a price and -as everything in life- you can only try to pick up the thing with the less negative impact.

  14. Might be worth mentioning the “stable” is what a horse is kept in (aka a “barn” in some places), unless the “door” was left open and the horse ran off – thus following the principle means you’re not uselessly closing the stable door after the horse has already bolted.

  15. Principles are heuristics and not universal laws. So they are “wrong” by definition, that means it’s easy to find counter examples. The reason why we use them is that is extremely hard – if not impossible – to come up with rules that are universally applicable and of practical use at the same time. I tried to clarify this a few years ago…

    https://sebastiankuebeck.wordpress.com/2017/09/17/solid-principles-and-the-arts-of-finding-the-beach/

  16. John Fahey · · Reply

    Your dependency inversion argument “A dependency is only interesting when there might be multiple ways of providing it” is flawed.

    The reason I use DI isn’t because I might want a different implementation later, multiple implentations now, or because of unit testing. I do it because it creates a seam between my business logic and some dependency (and all it’s dependencies), and allows me to write non-deterministic business logic that is separated from code that just integrates with these dependencies. In doing so it becomes more self-documenting, e.g. “I depend on something that can read the contents of a file” rather than “I depend on something that can do anything imaginable to a filesystem”.

    So it’s not primarily for making testable code, or supporting multiple implementations, or because I might want to change the database in the future, but these are positive effects resulting from this approach. It’s a bit of a correlation vs causation thing which I think is why many people miss the point.

    For example I was changing a junior dev’s code from using a SQL database to a NoSQL database. They had code for reading a file, processing the file, and writing the result to the database, all entwined in one big procedural lump. It was a mess. If it had been written using DI then I would have been able to leave a large amount of it alone, and find what needed to change more easily. No guarantees the interface wouldn’t change but it would have greatly reduced the pain involved.

    When I was finished, the code for reading, writing, and processing file contents was in different classes, and the logic for processing the file contents (our business logic) had no compile-time dependency on the others. Is that not what you would have done?

    1. Jonathan · · Reply

      I probably wouldn’t have split it into 3 classes, especially if you don’t have a compelling case for reusing at least one of those classes in an unrelated context, but that’s because it sounds like you split a single responsibility across three classes. That’s the problem with SRP, though, it doesn’t help guide us to reasonably good models of what our code is doing, certainly not better than the principle of keeping the code simple enough at each level of granularity to hold the whole thing in your mind.

      FWIW, splitting the procedural code into functions that will be called in sequence makes more sense to me given your limited description, although the language and tools used make a big difference.

      Then again, if you’re doing ETL work, why not use an actual ETL tool? Pentaho wasn’t bad the last time I looked and could move data to and from most places, while SSIS was even a bit easier as long as you’re going to an MS SQL DB. With a tool like those you might end up writing basically no code and have something even the business people can wrap their minds around. You may be able to save money on the front end by not using the professional-grade tools, but doing so makes for maintenance nightmares later if you have professional-grade quantities ETL work to do.

  17. […] lol, Why every single element of SOLID is wrong. […]

  18. I have given a talk occasionally for several years on SOLID principles and some patterns, as I believe they are good to understand (and I was surprised by how many had not even heard of them). But I agree with everything you say. I always caveat my talk with YAGNI. These are not rules. If you know them, they can help you write better code, but if you follow them slavishly you run the risk of turning simple code into an over-engineered mess. In fact, if you’re lucky enough to be using something like Python, question whether you should be using classes at all. I try to tell everyone who attends to go read John Ousterhout for a counterpoint.

  19. Stephen Williams · · Reply

    I’m a bit confused by the section on Dependency Inversion and some of the comments to this article. Are we getting Dependency Inversion and Dependency Injection mixed up? They are not the same thing. My Spring Boot application uses Dependency Injection to wire components together and Dependency Inversion to ensure application layer code depends on application layer defined abstractions (ports) rather than lower-level infrastructure layer code (adapters).

  20. […] CUPID the back story | Dan North & Associates […]

  21. Christopher Baker · · Reply

    I mostly agree with what you are saying here, however, to me the SOLID priciples are about simplifying code, similar to what you are stating here as the antedote to SOLID, however, some, if not all, were largely based on coding in the 70s and 80s, if you go back to their origins.

    I do, however, find it a little ironic that your your discussion around the ISP example of the job god object and it’s solution, which was to carve it up into smaller more manageable classes (and interfaces).

    By breaking up a horrendous god object, did that not make these classes small enough for a single task which, essentially, meant they only had a single responsiblity? Thus agreeing with the 1st principle by proxy?

    Great article and always great to have an open discussion.

  22. It seems that your LSP alternative is satisfied by “duck-typing” – is that right?

    (FWIW, I agree; SOLID can be rather inflexible – but, then again, it’s right there in the name.)

  23. […] CUPID the Backstory (Daniel Terhorst-North) […]

  24. Articles that claim software concepts are “all wrong,” black or white, come across as click-bait to me. Not much is black & white in the realm of discussions around software design.

    I continue to find value in the five class design principles. Less-experienced developers understand them just fine when we talk about them in context–i.e. when we face a challenge in code. Their frustration with the impact of changes, or the difficulty of making a change, can often be directly described as SOLID violations.

    I don’t view SOLID as sole canon, however. We have a good number of (incomplete?) perspectives on design, and they all provide some value, particularly the more we seek to learn and talk about design. Beck’s simple design, SOLID, GRASP, Fowler’s code smells, other concepts (design patterns perhaps, DRY, LoD, etc.), and now CUPID. (I myself started to write a book on the “four C’s” of design.) I can quibble with the perspectives in each and every one of them–including your writeups on SOLID and my four C’s–but I don’t think I’d dare say any one was flat-out wrong. They all also tend to support each other–there is indeed a fairly universal notion of small modules / small classes being a good thing, for example. Seems like this meshes with your comment about the theme of this post.

    Also, I’ve never once considered unleashing SOLID on a novice developer to figure out on their own. (I don’t think I’ve ever met someone who suggested this was a good idea, either.) As with most design concepts, this is all best ferreted out & learned while coding and with folks who can explain the implications of unprincipled choices… or let them feel it firsthand later.

    I ended up summarizing all the counter-proposals to SOLID here in my head as “just write simple code.” The closest I saw here for a definition of simple was “code should fit in your head at any level of granularity;” this isn’t a bad definition. But I know many seasoned developers who would buck against this definition, arguing that lots of small functions are harder for them to reason about, and that top-to-bottom methods are simpler. And many of them would claim “but I can fit all of this in my head,” and they’d be right.

    McConnell famously claimed that methods of up to 200 lines are just fine, and that (mostly 1980s) research backed up this contention. But I view anything that promotes–or gives a pass to–increasing function size / class size as a general rule as potentially damaging; we know how people are once they’re given an inch.

    In any case, I look forward to reading about CUPID.

  25. So what are CUPID principles?

  26. […] 2) S.O.L.I.D principles of C# programming are used extensively. Here is an interesting talk and slide deck, with explanation which illustrates what no longer works with them and offers C.U.P.I.D principles as an option instead. Read more. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: