Okay, this one is a bit easier to express:
If you declare something as taking a type, any instance of that type should be usable there.
Or, to put it another way, a Stock object is always a Stock object, it’s never a cow. The calculateValue() method should always calculate the value, never fire a nuclear missile. This is the Liskov Substitution principle, and is basically an injunction against creating objects that pretend to be one thing for convenience, when they’re actually something else.
There’s a very easy way to violate Liskov without noticing: check the type of an instance. Nearly always, if you’ve got an interface IPerson and you use “typeof” or “is”, you’ve written code that branches, usually an if statement. Now take a look at that statement again, and consider what happens when someone writes a new implementation of IPerson. Which side of the if statement does it fall? Answer is, it doesn’t matter, the next implementation might want either side. Yep, your code’s gonna break.
In this case, what’s happened is that you’ve basically broken encapsulation. If you move that decision into the implementing classes, either as a boolean property or a virtual method, you’ll solve the problem. (I’ll add that a boolean property is going to prove a lot more fragile than the virtual method, but it’s massively easier to achieve.)
The Bad News
Unfortunately from the Liskov Substitution Principle, it’s completely impossible to achieve. Every piece of code you ever write forms an implicit, stateful contract with its dependencies. Even if you are fully Liskov compliant right now, the next function someone writes may contain an implicit assumption that’s violated in a tiny proportion of cases. Truth is, types are not a constraint system and trying to pretend like they are can be positively dangerous.
Bertrand Meyer understood this problem and created Eiffel. Some of those ideas will make it into C#4. James Gosling understood the problem, but for some reason thought that constraining thrown exceptions was the best solution. The problem with Java exceptions actually helps us understand the problem with a slavish adherence to Liskov: premature constraints. The Java exception paradigm expects the interface designer to be able to anticipate all possible implementations of the interface, and punishes the implementor when the designer got it wrong.
A Sensible Approach
Well, design by contract is coming soon and will definitely enable us to improve our code quality, but what can we do about this now? First, there’s just the basic “use common sense” directive: don’t wilfully violate the behaviour that you’d expect of an implementation of an interface. Sometimes it’s unavoidable: a read write interface with an asynchronous implementation could behave quite differently from the synchronous implementation, and for good and valid reasons. What you can do is to implement standard unit tests for implementations of an interface. Here’s how you do it:
- Create an abstract test class with a method GetImplementation
- Make all of the tests use the interface
- Create multiple classes all of which override GetImplementation
Obviously, this creates a lot of tests, but it’s probably the best way to specify expected behaviour right now.
Finally, you owe it yourself to take time out and remind yourself that L stands for many other things too.
In general terms, yes, it’s the principles and approaches that matter. And that’s what SOLID’s all about. However, I think that we shouldn’t lose sight of the effect of tool-based thinking. I touched on this in a later post in this series, but I am wondering whether I should revisit the subject.Tool-based thinking is on my mind a lot at the moment: I’m actually practicing working without .NET, without strong types and most importantly, without Visual Studio at all and learning different approaches to solving problems.Moving back to your specific point, the problem with Java is that the definition of the abstract behaviour is all wrong. Exceptions rarely matter, but more subtle things, like not returning a null, usually do. As I said, Eiffel is a tool that gets this right. (I’m not recommending we all start using Eiffel, just that understanding its approach improves our mindset.) C# 4 actually has pretty good semantics for constraining an implementation to what the consumer/definer actually needs. Sadly, the syntax is absolutely appalling and the implementation is deliberately hobbled.
LikeLike