In 1987, Barbara Jane introduced a principle that henceforth became known as the Liskov Substitution Principle (sorry Jeanette). The upshot of the principle is this:
If you have a type of something (Dog) and a more specific subtype of that type (Beagle), then you should be able to substitute the subtype everywhere you have the type and not meaningfully change any of the properties. So, if I’m writing a program about dogs, I should be able to substitute in Beagle everywhere I have Dog and basically have things stay the same. This is a good programming principle for coding to abstractions and not violating the contract those abstractions imply, which is why the Liskov Substitution Principle is the “L” in SOLID.
An example that gets thrown around in talking about this principle is, if you have a Square class, and you have a Rectangle class, which one should be the type and which one should be the subtype? Geometrically speaking, a Square is a more specific type of Rectangle, and you’d think that Rectangle should be the type and Square should be the subtype.
“Ah, but hold!” say the Liskov nerds. “That way lay disaster and ruin, for the Rectangle will have Width and Height that can be set separately, while the Square’s Width and Height must change the other when one is set. Thou canst no longer substitute a Square for a Rectangle and get the same expected behavior, for the Height and Width are independent in the type, but co-dependent in the subtype.”
Now, developers will look at that and immediately think of many ways using composition and generics to sidestep this problem altogether. Eliminate the inheritance hierarchy while sharing structure or functionality. But Barbara Jane didn’t have some of those solutions available in 1987, and even if she did and we decided to keep to this proposed inheritance structure, what are we trading away, and are we actually violating LSP?
We could make the Square and Rectangle immutable, and that would solve the problem. The problem isn’t the inheritance model, per se, but the behavior of setting Height or Width. If you remove the setters, the problem goes away. This may be quite limiting to a program that had to deal with squares and rectangles, however.
If we reverse the hierarchy and make Square the type (maybe with a Side property) and the Rectangle a subtype (inheriting Side and adding… well, I don’t know what you’d call it… OpposingSide? OtherSide?), we might be doing the “right” thing from an LSP standpoint, but we are certainly sacrificing the clarity of the domain. We are no longer modeling truth – that is, that a Square is a Rectangle with an extra business rule, and the difficulty of naming the common property and the extra property a Rectangle would have just illustrates this.
Maybe that doesn’t bother you, but it bothers me.
If a domain model is supposed to be a ubiquitous language between a business process and software, then shenanigans like this violates that trust. We know the business is one way in reality, but we’re going to model it a different way for the sake of the integrity of a coding principle.
If you were writing software for a veterinary hospital, you wouldn’t make OneLeggedDog the type and Dog the subtype because the Dog has more legs. Clearly, the Dog is the base type, and OneLeggedDog is a more specific type.
Then there’s the issue of how much of a violation this actually is. I admit, if you substituted a Square for a Rectangle, and you did not know this substitution occurred, then it would be a surprise to find that changing the Height also changed the Width.
But this is actually expected behavior for a Square. In fact, it’s definitive behavior for a square. One could just as easily argue that, if the Square were the type and the Rectangle the subtype, this is an LSP violation because you would not assume a Square’s sides could be set independently. You would assume your type (Square) would constrain all the side lengths. Without care, the LSP principle could argue against virtually any kind of overriding, disrupting what you’d “expect” from a base type.
Yes, I know. You can do plenty of overriding without fiddling with expected postconditions, but do you see what I mean? The LSP can’t be the end of a discussion on how to model a domain.
Thankfully, modern OO languages give us lots of options to avoid this very thing. An IPolygon interface with a Sides collection comes to mind – allowing us to faithfully model the domain and keep the LSP intact. And if there’s a moral to my rambling story, maybe that’s it. That if you’re violating the LSP, perhaps that should spawn conversations about how you could more flexibly, yet accurately, represent a domain, and not be something you have to shoehorn conformity to in ways that don’t describe reality.