This series is about Clean Code, SOLID principles, and all kinds of other cool stuff I talk about in my Clean Code classes. | ||
---|---|---|
The Rectangle and the Square Part I | The Rectangle and the Square Part II |
Last time, in the first post in my new clean code series, we discussed how I torment my students with the ol’ Square and Rectangle trick in my Clean Code course, talking about Liskov Substitution Principle (LSP). At the end of the discussion, we got the audience to understand the issue, and go from “you’re doing it wrong” to “so what?”.
So let’s talk about the bigger issue at hand, because it’s not just about clean code.
Remember before clean code, where we just learned about OOP, inheritance and class derivation?
Inheritance is a feature of a programming language that conjures up a relationship – the derived class is “a kind of” the base class.
Well, it can be, but as we saw, it’s not up to the derived or base class to decide. It’s how the client code uses it, that decides if they are treated the same way. Behavior is not just method signature and implementation. It’s also the usage of these methods across the class – what we expect them to do when we call them in combination.
“A kind of” is not really a programming language feature, we decide about semantics. We frequently interpret this incorrectly, and create hierarchies of classes which we see as “a kind of”, but really are special cases that need to be treated differently.
Special relations
Ok, there must be another way. Let’s look at another solution to the problem.
Here’s another definition of a Rectangle, where the area calculation behaves consistently, based on the length of the diagonals and the angle between them:
public class Rectangle { protected double _diagonal; protected double _angle; public int GetDiagonal() { return _diagonal; } public int GetAngle() { return _angle; } public void SetDiagonal(double diagonal) { _diagonal = diagonal; } public void SetAngle(double angle) { _angle = angle; } }
For our area calculation purposes, a Square is equivalent to a Rectangle. It is literally and programtically, “a kind of” a Rectangle:
public class Square : Rectangle { }
The area calculation code is similar for both Square and Rectangle of course:
var shape = new Square(); shape.SetDiagonal(10.0); shape.SetAngle(25.6); Console.WriteLine(String.Format(“Area = {0}”, shape.GetDiagonal() ^ 2 * 0.5 * Sin(* shape.GetAngle())));
Because the Square is a Rectangle, there can be no discrepancy between their behavior and that means that LSP approves it – we can replace Square with Rectangle and everything works as expected.
Something still feels wrong about this design.
Clean code is about other people
In a day-to-day conversation, would you describe a rectangle by its diagonals and the angle between them? Most people don’t.
They will look at your Rectangle code and snicker. Some will not even trust you anymore, and you’ll spend lunch eating alone. Width and height are good for most humans, diagonals and sine language – not so much.
That’s because the “width and height” design is how most people think of properties of a rectangle, while the diagonals and angle design is fit for area calculation. The former is about designing from the inside out, regardless of use, while the latter is using the code from an external client with a special use in mind.
There’s a gap between what we build and how it’s used. Plus, it took me awhile to find the formula and prove to myself that it is correct, other people I suspect will require the same treatment.
This is where bugs come from.
We translate models all the time – the business, the code, the intended and actual behavior – and things get lost.
Ironically we’re thinking of “class inheritance” as a sure way to describe the same kind of types, while forgetting that it’s really not. We’re using one language, where it does not translate correctly to our understanding.
So what do we do?
- Use ubiquitous language. Same language, models, terms in the code, the design and the requirements.
- Use implementation inheritance less.
- Use interface “inheritance” instead. Interfaces have less baggage than implementation.
- If you do use inheritance, check if LSP still works. If not, you found a loop-hole. Either the types are not of the same kind, or there’s a translation problem.
- Review and review again. Other people can confirm or disprove what you’re thinking.
Remember that clean code is about communication with other people. Use their feedback to make sure.
0 Comments