Rice University - Comp 212 - Intermediate Programming

Fall 2002

Lecture #13 - Implementation of Linear Recursive Structure Framework


0. Recall

Below is the UML class diagram showing the design of LRStruct, our framework for mutable linear recursive structure.

Keep in mind the following

public class DoSomethingWithLRS implements IAlgo { /* blah blah blah...*/} 

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.
   }

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...
}

 

The link below contains the source code for the complete LRStruct framework:

http://www.owlnet.rice.edu/~comp212/02-fall/dataStructures/list/lrs/

1. LRStuct

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 this LRStruct.
     */
    private ANode _head;

    /**
    * Initializes this LRStruct 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 this LRStruct
     */
    public final LRStruct insertFront(Object dat)	{
	return _head.insertFront(dat, this);
    }

    /**
     * Removes and returns this LRStruct's first.
     */
    public final Object removeFront()	{
	return _head.removeFront(this);
    }

    /**
    * Gets the first data element from this LRStruct
    */
    public final Object getFirst() {
	return _head.getFirst (this);
    }

    /**
     * Sets first data element to a new value.
     * @param dat replaces the existing first for this LRStruct.
     * @throws NoSuchElementException if this LRStruct is empty.
     * @return this LRStruct
     */
    public final LRStruct setFirst(Object dat) {
	return _head.setFirst (dat, this);
    }

    /**
     * Gets the rest of this LRStruct.
     * @throws NoSuchElementException if this LRStruct is empty.
     */
    public final LRStruct getRest() {
	return _head.getRest(this);
    }

    /**
     * Sets a new tail for this LRStruct.

     * post condition: getRest() now returns tail.
     * @throws NoSuchElementException if this LRStruct is empty.
     * @return this LRStruct
     */
    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 this LRStruct with a given head node.
     * @param node != null.
     */
    LRStruct(ANode node)  {
        _head = node;
    }

    /**
     * Changes the head node (i.e. state) of this LRStruct.
     * @param head replaces the existing state of this LRStruct.
     * @return this LRStruct
    */
    final LRStruct setHead (ANode head) {
	_head = head;
        return this;
    }

    /**
    * Gets the head node (i.e. state) of this LRStruct.
     * @return the head node.
     */
    final ANode getHead () {
        return _head;
    }
}


2. LRStruct Nodes as States

 

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 package-private constructor LRStruct( ANode n) { _head = n;} serves to initialize an LRStruct to a particular state.
It  is not absolutely necessary to define such a constructor.  

The call
new LRStruct(n);
can be replaced by a call to LRStruct() followed by a call to setHead(ANode n):
new LRStruct().setHead(n);

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.
Such a practice eliminates the prospect of  "forgetting" to initialize and/or improperly initializing an object.

 

 

Comment 2: We intentionally make the code for insertFront(...) for both EmptyNode and NENode look identical.
This begs the question, "why don't we just factor it up to the abstract ANode class, making its insertFront(...) method concrete, and thus eliminating code duplication in the concrete subclass?".

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.
Doing so is tantamount to saying that the super class is not an abstraction of the 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...
}

 

3. LRStruct Visitor

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:
  1. which object should be  responsible for determining whether or not the host's first is the last element?
  2. which object should be responsible for removing the last element?  

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.

  1. Ask the host's rest for help in determining whether or not the host is holding the last element.  When asking for help, the host will need to pass itself as a parameter to the helper task so that once the last element is discovered, it can then be removed.
  2. Help the preceding LRStruct remove its last element.  When helping the preceding (non-empty) LRStruct to remove an element, an empty LStruct simply tells the given LRStruct to remove its first, while a non-empty LRStruct will ask its rest for help to remove its last element passing itself as a parameter.

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.

 


dxnguyen@cs.rice.edu
Copyright 2002, Dung X. Nguyen - All rights reserved.