Say you have an element container called SimpleList which exposes the following methods(apart from others of course):
1. add(element): Adds 'element' to the SimpleList
2. addAll(elements): Adds 'elements' to the SimpleList
These methods are extensible(can be overriden and hence extended).
They are implemented as follows in SimpleList.
# Contract: Will add 'element' to SimpleList or throw an
# exception in case of an error.
method add(element)
# Adds element to the underlying data store/array
# Contract: Will add 'elements' to SimpleList or throw an
# exception in case of an error. This method may throw an
# exception after adding 'some' of the elements from 'elements'.
method addAll(elements)
for element in elements
this.add(element)
Fair enough till now.
Up comes someone who decides they can do better and they extend SimpleList and create ABetterSimpleList!!
They are implemented as follows in ABetterSimpleList.
# Contract: Will add 'element' to ABetterSimpleList or throw
# an exception in case of an error.
method add(element)
# Adds element to an underlying cache since SimpleList
# may take a while to add() this element.
if cache.size() > 20:
this.addAdd(cache)
cache.clear()
# Contract: Will add 'elements' to ABetterSimpleList or throw
# an exception in case of an error. This method may throw an
# exception after adding 'some' of the elements from 'elements'.
method addAll(elements)
# Just call the base class' addAll method
All contracts have been satisfied, but can you spot the subtle bug?
Yes, there is an infinite recursion here!! Calling add() on ABetterSimpleList will add elements to the cache till the cache grows to 20 elements in length. Once that happens, it will call addAll() which will call the base class' addAll() which will call (what it thinks is) it's add() function, except that it had been overridden by us to call addAll()!!
Well, how do you solve this?? I don't have a concrete answer myself, but just scratching the surface has lead me to believe that there are 2 type of class extensions:
1. Extending for the base class (I call this framework extension)
2. Extending for users/subclasses (I call this normal extension)
In case [1], it is expected that the base class will consume your interfaces so you program for the base class. However, in case [2], your consumers are externals and not internals like your base class. Hence, you should now program for them.
An important side effect of this is that classes that are meant to be consumed by subclasses (case [2]) should NOT use their own overridable methods in their own implementation or else they CAN NOT give any guarantees to their consumers (subclasses in this case). However, framework extended classes (case [1]) SHOULD use their own public interfaces since that IS the purpose of them allowing method overriding on those interfaces. They are expected to be the consumers of their subclass' funtionality.
Examples of each type of class:
1. Framework Extension: Java's Runnable, C++'s streambuf
2. Normal Extension: Java's LinkedList, AbstractList. Typically anything that is layered falls into this category
No comments:
Post a Comment