In Data Abstraction and Hierarchy, SIGPLAN Notices 23,5 (May, 1988) Barbara Liskov wrote;
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
and the Liskov substitution principle (LSP) was Born.
Applied diligently the LSP is one of the more powerful modeling techniques in an application designers bag of tricks. Unfortunately it’s also one of the most frustrating and difficult to grok because it seems so simple and intuitive.
When applying the principle it should be seen from the interface clients point of view. So “the behavior of P is unchanged”, doesn’t mean that the program has the same effect on the world, it means that the caller doesn’t have to condition its interaction with the subject based on whether they are o1 or o2. To the client both objects fulfill the contract and the client can ignore the differences between them.
Lets say I’m asked to provide a list interface (let’s pretend that java.util.List doesn’t exist for now). The client requires three operations; get an item at a specified index; put and item in a specified index and size that returns the greatest value for index. I create my interface and an implementation or two and deliver it to the world to great fanfare.
+------------------------------+
| List<T> |
+------------------------------+
| <T> get(int index) |
| <T> put(int index, <T> item) |
| int size() |
+------------------------------+
Another group requires a similar interface, the only difference being that they don’t want the list to be modifiable. Seeing as the original List interface was so successful I’m asked to create the new ImmutableList without the put operation.
+------------------------------+
| ImmutableList<T> |
+------------------------------+
| <T> get(int index) |
| int size() |
+------------------------------+
Several months later I’m reviewing my library and I notice that the two list interfaces share a common set of methods and I decide to redefine List to inherit from ImmutableList and unthinkingly violate the LSP.
+------------------------------+
| ImmutableList<T> |
+------------------------------+
| <T> get(int index) |
| int size() |
+------------------------------+
^
|
+------------------------------+
| List<T> |
+------------------------------+
| <T> put(int index, <T> item) |
+------------------------------+
You see from the standpoint of an ImmutableList client a List isn’t an ImmutableList as it can be modified. The inheritance relationship allows a List to be used wherever an ImmutableList is specified. Thus a client expecting that the list will not change may function incorrectly if when a List instance is supplied.
So how does this relate to Hibernate? Hibernate allows you to specify persist your java model with minimal impact on your code (this is grossly simplified view but will allow me to illustrate my point). It manages the relationships between objects and can be configured to either load an object graph completely, lazily or a combination of both. This is achieved by substituting the referenced classes with Hibernate proxies. These proxies will then load an objects details as required. Hibernate also allows you to detach and reattach and object from it’s data source. When an object is detached attempting to access parts of the object graph that haven’t been loaded results in an exception being generated.
Not so long ago I was involved with a project that used Hibernate to persist it’s state. The model was relatively small but the data set was very large. The data was organized into hierarchies. A node could have many children, these children could also have children (think massive XML document). A single node could literally have millions of descendants. Eager loading the complete graph for any node simply wasn’t an option and as the objects needed to be sent over the wire lazy loading was out. The team had developed a number of queries that would load part of the tree to a specified depth to suite the callers purpose. The problem was that the caller had to know which query had been used to load the object and adjust their behavior accordingly, violating the LSP. The team had effectively created derivative classes that behaved differently. This manifested itself in subtle (and not so subtle) defects, the solution to which was often to add additional queries to service the specific client needs.
To be fair the problem wasn’t really hibernates fault, but by the way it was being used (give someone enough rope …). Developers must be constantly vigilant to ensure their im implementation choices don’t introduce subtle design flaws.
Recent Comments