Below is the UML class diagram showing the design of LRStruct, our framework for mutable linear recursive structure.
Keep in mind the following:
When a client programs with LRStruct, he/she only sees the public methods of LRStruct and IAlgo.
To operate on an LRStruct, the client writes appropriate IAlgo classes. For example,
public class DoSomethingWithLRS implements IAlgo { /* blah blah blah...*/}
The framework dictates the overall design of an algorithm on LRStruct: there must be some code for emptyCase and some code for nonEmptyCase. For example,
public class DoSomethingWithLRS implements IAlgo {
// fields and constructor code...
public Object emptyCase(LRStruct host, Object inp) {
// some concrete code here...
return some Object; // may be null.
}
public Object nonEmptyCase(LRStruct host, Object inp) {
// some concrete code here...
return some Object; // may be null.
}
}
To perform an algorithm on an LRStruct, the client must "ask" the LStruct to "execute" the algorithm and passes to it all the input required by the algorithm. For example,
LRStruct myList = new LRStruct(); // an empty list
// insert a few Objects into myList by making repeated calls to myList.insertFront(/*whatever*/}
// Now call on myList to perform DoSomethingWithLRS:
Object result = myList.execute(new DoSomething(/* argument list */), new AccPair(-2.75, 77));Here AccPair is a user-defined class defined as
public class AccPair {
private double _acc;
private int _degree;
public AccPair(double a, int d) {
_acc = a;
_degree = d;
}
// Other accessor methods: settors and gettors...
}
Since the input parameter of IAlgo is an Object, it can be anything from a single data object to a "bunch" of data objects. To pass a bunch of data objects to IAlgo, the client may have to "wrap" them up in some ad-hoc user-defined class.
The link below contains the source code for the complete LRStruct framework:
http://www.owlnet.rice.edu/~comp212/02-fall/dataStructures/list/lrs/
The code for LRStruct follows the state design pattern and is almost trivial. LRStruc is the invariant part of the system. It remains the same for all applications using LRStruct. On the other hand, the different kinds of operations you want to perform on an LRStruct vary from application to application. They form the variant part of the system. Their behaviors are abstractly encapsulated in an interface called IAlgo. This is an example of what we call "behavior abstraction", which is what an OO design should be striving for. Within this union of all possible algorithms on LRStruct, IAlgo is invariant! In any system, an abstract class (or interface) should never change.
package lrs; /** * Mutable linear recursive structure. * Visitor Pattern: Serves as a host capable of executing algorithms which are * visitors. * @author Dung X. Nguyen Copyright 2001 - All rights reserved. * @since 10/09/01 */ public class LRStruct { /** * The state of of thisLRStruct
. */ private ANode _head; /** * Initializes thisLRStruct
to the empty state. */ public LRStruct() { _head = EmptyNode.Singleton; } /** * Returns "()" if empty, otherwise returns the contents of this *LRStruct
enclosed in parentheses. */ public String toString() { return _head.toString(this); } /** * Inserts dat to the front of this LRStruct. * post condition:getFirst()
now returns dat. * @param dat data to be inserted. * @return thisLRStruct
*/ public final LRStruct insertFront(Object dat) { return _head.insertFront(dat, this); } /** * Removes and returns thisLRStruct
's first. */ public final Object removeFront() { return _head.removeFront(this); } /** * Gets the first data element from thisLRStruct
*/ public final Object getFirst() { return _head.getFirst (this); } /** * Sets first data element to a new value. * @param dat replaces the existing first for thisLRStruct
. * @throws NoSuchElementException if thisLRStruct
is empty. * @return thisLRStruct
*/ public final LRStruct setFirst(Object dat) { return _head.setFirst (dat, this); } /** * Gets the rest of thisLRStruct
. * @throws NoSuchElementException if thisLRStruct
is empty. */ public final LRStruct getRest() { return _head.getRest(this); } /** * Sets a new tail for thisLRStruct
. * post condition:getRest()
now returns tail. * @throws NoSuchElementException if thisLRStruct
is empty. * @return thisLRStruct
*/ public final LRStruct setRest(LRStruct tail) { return _head.setRest(tail, this); } /** * Hook method to execute an algorithm with a given input and return * an appropriate output object. * @param algo an algorithm (!= null) that operates on this LRStruct. * @param inp input object needed by visitor algo. * @return output object resulting from the execution of algo. */ public final Object execute(IAlgo algo, Object inp) { return _head.execute(algo, inp, this); } /* Package access only: */ /** * Initiazes thisLRStruct
with a given head node. * @param node != null. */ LRStruct(ANode node) { _head = node; } /** * Changes the head node (i.e. state) of thisLRStruct
. * @param head replaces the existing state of thisLRStruct
. * @return thisLRStruct
*/ final LRStruct setHead (ANode head) { _head = head; return this; } /** * Gets the head node (i.e. state) of thisLRStruct
. * @return the head node. */ final ANode getHead () { return _head; } }
Code |
Illustration |
class EmptyNode extends ANode { /** * The owner becomes non-empty and * has dat as its first element. */ LRStruct insertFront(Object dat, LRStruct owner) { return owner.setHead(new NENode(dat, new LRStruct(this))); } |
|
|
|
|
|
class NENode extends ANode { private Object _dat; private LRStruct _tail; /** * Initializes this NENode to contain dat and a given tail list. * @param dat the data object to be stored in this NENode. * @param tail the LRStruct tail of this NENode. */ NENode(Object dat, LRStruct tail) { _dat = dat; _tail = tail; } /** * Inserts a data object at the front of the LRStruct owner. * @param dat the object to be inserted at the front. * @param owner the LRS referencing this NENode. */ LRStruct insertFront(Object dat, LRStruct owner) { return owner.setHead(new NENode(dat, new LRStruct(this))); /* Details: LRStruct coOwner = new LRStruct (this); NENode newNode = new NENode (dat, coOwner); return owner.setHead (newNode); "old" style: owner._head = newNode - cannot be done here. */ } } |
|
|
|
|
|
Comment 1: Proper initialization of an object is crucial
in program correctness. The call However, it is a good programming practice to have an object
completely initialized when it is defined, and have all initialization
code done in one place.
|
|
Comment 2: We intentionally make the code for insertFront(...)
for both EmptyNode and NENode look identical. Doing so would make the abstract class ANode "aware" of a specific concrete variant, NENode, and prevent us from hiding (and perhaps changing) the details of implementation from external client code, and thus violating the principle of information hiding. As a general OO programming principle, never make a super class aware
of any of its subclasses.
|
|
class NENode extends ANode { Object removeFront(LRStruct owner) { owner.setHead(_tail.getHead()); // owner._head = _tail._head return _dat; } // other methods elided... } |
|
Let us examine the code for lrs.ANode.
It overrides the toString() method by making use of an anonymous inner class which in turns calls on another anonymous inner class!
The use of anonymous inner classes here allows us to program a concrete method for ANode without referring to any concrete named implementation of IAlgo. This is crucial in keeping ANode independent of any concrete implementations.
abstract class ANode { /** * Uses anonymous visitor class to compute a String representation. */ String toString(LRStruct owner) { return (String)owner.execute (new IAlgo() { public Object emptyCase(LRStruct host, Object inp) { return "()"; } public Object nonEmptyCase(LRStruct host, Object inp) { return "(" + host.getFirst() + host.getRest().execute(new IAlgo() { public Object emptyCase(LRStruct h, Object i) { return ")"; } public Object nonEmptyCase(LRStruct h, Object i) { return " " + h.getFirst() + h.getRest().execute (this, null); } } , null); } } , null); } // Other methods elided... }
Exercise: Write an algorithm to remove the last element from an LRStruct.
Below are two different ways of thinking that lead to two different ways of solving the problem.
Non Object-Oriented Thinking (NOOT) |
Object-Oriented Thinking (OOT) |
Any LRStruct algorithm must consider the case when the LRStruct is empty and the case when the LRStruct is not empty. | As dictated by the framework, any algorithm on LRStruct is a
concrete IAlgo visitor with two methods: one to deal with the empty case
and one to take care of the non-empty case.
|
If the LRStruct is empty, then throw an exception as specified. | When the LRStruct host is empty, throw an exception
as specified.
|
If the LRStruct is not empty,
traverse the LRStruct until the empty LRStruct
is reached, at which point ask the preceding LRStruct
to remove its first.
The above algorithm seems straightforward and simple enough. It involves imposing an external control structure to traverse an LRStruct and stop at some appropriate point. As illustrated in a subsequent section, such an algorithm is usually implemented in terms of a loop whose exit condition involves checking an LRStruct for emptiness! |
When the LRStruct host is not empty, it has a first and a
rest, which is itself an LRStruct. In order to remove the last
element from such a host, we need to answer two questions:
The host's rest knows whether or not itself is empty, and thus can determine whether or not the host's first is actually the last element. Therefore the responsibility for determining whether or not the host is holding the last element should be delegated to the host's rest. Though the host's rest knows whether or not the host holds the last element, it cannot remove that element. The only object capable of removing this element is the host itself. So knowing when to remove does not necessarily means capable of the task of removal. In general, knowing when some action must take place does not mean capable of carrying out that action. These are two distinct responsibilities that need to be distributed to two cooperating objects. The above OO thought process leads to the idea of decomposing the task of removing the last element into two subtasks.
Note that the above algorithm involves no notion of list traversal at all. It entails decomposing the original task into smaller subtasks ("divide-and-conquer") and is only expressed in terms of which objects should be doing what and what pieces of information must be passed to each object's method in order to carry out each of the subtasks. OOP calls for thinking in terms of problem abstraction, task decomposition, and delegation of responsibilities. It is a paradigm shift that emphasizes design and promotes a different way of thinking about formulating and solving problems. |
Below is an implementation of the procedural non-oo algorithm.
Non Object-Oriented Algorithm (NOOA) |
Non Object-Oriented Coding (NOOC) |
Algorithm to check for emptiness |
public class IsEmpty implements IAlgo { // Singleton public Object emptyCase(LRStruct host, Object inp) { return Boolean.TRUE; } public Object nonEmptyCase(LRStruct host, Object inp) { return Boolean.FALSE; } } |
If the LRStruct is empty, throw an exception.
If the LRStruct is not empty, traverse the LRStruct until the empty LRStruct is reached, at which point ask the preceding LRStruct to remove its first. |
/** * Static method of some class. */ public static void removeLast(LRStruct list) { if (((Boolean)list.execute(IsEmpty.Singleton), null)).booleanValue()) { throw new java.util.NoSuchElementException("Empty list has no data."); } LRStruct next = list.getRest(); while (!((Boolean)next.execute(IsEmpty.Singleton), null)).booleanValue()) { list = next; next = list.getRest(); } list.removeFront(); } |
Comment:
Though the high level description of the algorithm seems simple enough, its translation into code is not that simple. It is not apparent that the code is a faithful (and correct) translation of the algorithm statement because it involves a lot of tedious book-keeping and flow control programming constructs. |
Now consider how the OO algorithm is implemented.
Object-Oriented Algorithm (OOA) |
Object-Oriented Coding (OOC) |
An algorithm to remove the last element from an LRStruct is a concrete IAlgo visitor. |
public class RemLast implements IAlgo { // Singleton |
When the LRStruct host is empty, throw an exception as specified. The code on the right simply expresses such an action. |
public Object emptyCase(LRStruct host, Object inp) { throw new java.util.NoSuchElementException("Empty list has no data."); } |
When the LRStruct host is not empty, ask the host's
rest for help in determining whether or not the host is holding the last
element, passing the host as a parameter just in case this is true and the
last element needs to be removed. |
public Object nonEmptyCase(LRStruct host, Object inp) { return host.getRest().execute(RemLastHelp.Singleton, host); } } |
The algorithm for a host LRStruct to help remove the last element from the preceding LRStruct, p, is a concrete IAlgo visitor whose input parameter is p. |
public class RemLastHelp implements IAlgo { // Singleton |
The empty LRStruct tells the preceding LRStruct to remove its first. |
/** * @param inp the LRStruct preceding host. */ public Object emptyCase(LRStruct host, Object inp) { return ((LRStruct)inp).removeFront(); } |
A non-empty LRStruct asks its rest for help to remove its last element passing itself as a parameter. |
/** * The last element of the (non-empty) host is the * last element of inp, the LRStruct preceding host. */ public Object nonEmptyCase(LRStruct host, Object inp) { return host.getRest().execute(this, host); } } |
Comment:
The implementation code for the OO algorithm directly maps to the abstract high-level description of the algorithm and can be easily proved to be correct. It is "declarative" in nature. It does not
involve any conditional to specify when to perform certain task. It
simply states what needs to be done for each state of the host LRStruct.
Polymorphism is put to use to direct all flow control reducing code
complexity. |
The RemLastHelp defined in the above will only perform correctly if it is passed the appropriate parameter: the LRStruct that precedes the host. Though it is called once, inside of RemLast.nonEmptyCase(...), by the original LRStruct to :"help" it determine whether or not it is holding the last element and remove that element if this is the case, we have to go through the standard process of defining a class for it. We can (and should) rewrite RemLastHelp as a local anonymous inner class that is instantiated "on-the-fly" inside of the RemLast.nonEmtpyCase(...) as follows.
public class RemLast implements IAlgo { // other methods elided... public Object nonEmptyCase(LRStruct host, Object inp) { return host.getRest().execute(new IAlgo() { // Anonymous inner class! public Object emptyCase(LRStruct h, Object p) { return ((LRStruct)p).removeFront(); } public Object nonEmptyCase(LRStruct h, Object p) { return h.getRest().execute(this, h); } }, host); } }
In the above, note how the parameters of the anonymous inner class are renamed in order to avoid masking the parameter names in the outer object.