I watched Sean Chambers deliver an excellent talk about S.O.L.I.D. principles last weekend at Tallahassee CodeCamp. It motivated me to look a little deeper into the Liskov Substitution Principle, which states:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
We accept this as a truth and it’s what makes us hate the is keyword in C# so damn much. I wrote a post a while back that explains (or attempts to) what LSP and why it’s important, which is to say I won’t harp on the what and why here. I’ll spend your attention, rather, on how LSP works with exceptions.
The LSP article on Wikipedia provides clarification around exceptions and subclasses:
No new exceptions should be thrown by methods of the subtype, except
where those exceptions are themselves subtypes of exceptions thrown by
the methods of the supertype.
Here’s a contrived and fugly example in ruby:
class FileSystem
def save(path, text) File.open(path, 'w+') {|f| f.write(text) } end
end
class AmazonFileSystem < FileSystem
include 'amazon_library'
def save(path, contents) begin write_to_s3(path, contents) rescue AmazonWebServicesError.new('cannot write file'); end end
end
class AmazonWebServicesError < StandardError
def initialize(message) @message = message end
def message return @message end
end
def backup_system
file_name = 'book.txt'; my_important_text = 'hello world';
[AmazonFileSystem.new, FileSystem.new].each do |file_system| begin file_system.save(file_name) rescue AmazonFileError # VIOLATION! FOR SHAME! end end
end
A try-catch (or begin-rescue) is a conditional pattern and it’s my sense that when we use this with implementation inheritance we’ve made a particularly egregious and insidious violation of the LSP.
First off, we’re know things about the internals of our subclass in our consumer. We’ve broken encapsulation and increased coupling.
Secondly, our special AmazonWebServiceError only shares StandardError with what’s likely to be thrown by our FileSystem class. This means we can’t treat errors that are likely to fall out of Ruby’s IO core polymorphically. More conditional logic/branching, more cyclomatic complexity, more coupling, and these are bad things.
What can we do to stay on the good side of LSP when dealing with exceptions in inheritance hierarchies, you ask? Well, as the rule states, we can make exceptions subclass the exception the parent throws. For example, we could catch whatever error our fictitious amazon library throws and decorate it with a custom error that derives from the error File.write in ruby core throws. Another, perhaps, better option would be to hold off on coding defensively wherever you can and let the exception bubble up to a central part of your program that deals, exclusively, with these corner cases. That is, implement an exception shield wherever we don’t need compensating logic.
LSP and exceptions: another thing to pay attention to while on programming’s happy trail, friends.
I think Java’s “throws” clause it’s a nice to have in other languages.
I wrote about the LSP just yesterday. Maybe you want to take a look and see if there are inaccurate affirmations:
http://giorgiosironi.blogspot.com/2009/09/solid-part-3-liskov-substitution.html