Let's say I wanted to create a Java List<String>
(see spec) implementation that uses a complex subsystem, such as a database or file system, for its store so that it acts as a persistent collection rather than an in-memory one.
So here's a skeleton implementation:
class DbBackedList implements List<String> {
private DbBackedList() {}
/** Returns a list, possibly non-empty */
public static getList() { return new DbBackedList(); }
public String get(int index) {
return Db.getTable().getRow(i).asString(); // may throw DbExceptions!
}
// add(String), add(int, String), etc. ...
}
My problem lies with the fact that the underlying DB API may encounter connection errors that are not specified in the List interface that it should throw.
My problem is whether this violates Liskov's Substitution Principle (LSP).
In his paper on LSP, Bob Martin actually gives an example of a PersistentSet that violates LSP. The difference is that his newly-specified Exception
there is determined by the inserted value and so is strengthening the precondition. In my case the connection/read error is unpredictable and due to external factors and so is not technically a new precondition, merely an error of circumstance, perhaps like OutOfMemoryError which can occur even when unspecified.
In normal circumstances, the new Error/Exception might never be thrown. (The caller could catch if it is aware of the possibility, just as a memory-restricted Java program might specifically catch OOME.)
Is this therefore a valid argument for throwing an extra error and can I still claim to be a valid java.util.List
(or pick your SDK/language/collection in general) and not in violation of LSP?
Edit: This argument might be more palatable if you consider a FileBackedList
(more reliable "connection") rather than a DbBackedList
.
If this does indeed violate LSP and thus not practically usable, I have provided two less-palatable alternative solutions as answers that you can comment on, see below.
Footnote: Use Cases
In the simplest case, the goal is to provide a familiar interface for cases when (say) a database is just being used as a persistent list, and allow regular List operations such as search, subList and iteration.
Another, more adventurous, use-case is as a slot-in replacement for libraries that work with basic Lists, e.g if we have a third-party task queue that usually works with a plain List:
new TaskWorkQueue(new ArrayList<String>()).start()
which is susceptible to losing all it's queue in event of a crash, if we just replace this with:
new TaskWorkQueue(new DbBackedList()).start()
we get a instant persistence and the ability to share the tasks amongst more than one machine.
In either case, we could either handle connection/read exceptions that are thrown, perhaps retrying the connection/read first, or allow them to throw and crash the program (e.g. if we can't change the TaskWorkQueue code).
remove()
instead of throwingUnsupportedOperationException
), or sort the entire thing respectively.get
is callingDB.getTable()
, that implies that sorting is impacting the underlying Database. I'm a bit leery of the implications.List<E>
is "An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted." Choice B might not violate LSP, but it seems a bit odd. The answers are ignoringadd(int index, E element)
, since you didn't mention it.