Rice University - Comp 212 - Intermediate Programming

Fall 2002

Lecture #07 - NOOT vs. OOT - Using Helpers


1. Non Object-Oriented Thinking (NOOT) vs.  Object-Oriented Thinking (OOT)

In the previous  lecture, we did not have time to finish example 3: find and return the last element in a list.  We now resume our discussion.

This example introduces three new concepts: 

We will also use this example to illustrate two different ways of thinking that lead to two different ways of solving the same problem.

NOOT

OOT

Any IList algorithm must consider the case when the IList is empty  and the case when the IList is not empty. As dictated by the framework, any algorithm on IList is a concrete IListAlgo visitor with two methods: one to deal with the empty case (IEmptyList) and one to take care of the non-empty case (INEList).

 

If the IList is empty, then throw an exception since an empty list has no element at all. When the IList host is empty (IEmptyList), throw an exception since an empty list has no element at all.

 

If the IList is not empty, traverse the IList until the empty IList is reached, at which point return the first element of  the preceding IList . 

The above algorithm seems straightforward and simple enough. It involves imposing an external control structure to traverse an IList 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 IList for emptiness!

When the IList host is not empty (INEList), it has a first and a rest, which is itself an IList.  In order to find and return 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 returning 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 does not have access to that element.  The only object capable of accessing this element is the host itself.

So knowing when to return the last element does not necessarily means capable of the task of returning that last element.  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 worked out between the host and its rest.

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 its first as a parameter to the helper task so that once the last element is discovered, it can then be returned.
  2. Help the preceding IList remove its last element.  When helping the preceding (non-empty) IList to return the last element, an empty IList (IEmptyList) simply returns the input parameter (which is the first element of the preceding list), while a non-empty IList (INEList) will recursively ask its rest for help to return its last element by passing its first as a parameter.  This ought to work because the last element of an non-empty list is the last element of the preceding list.

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 requires checking for emptiness.
The boolean expresssion
  list instanceof IEmptyList
will return true if it is of type IEmptyList, false otherwise.
  
If the IList is empty (IEmptyList), throw an exception.

If the IList is not empty, traverse the IList until the empty IEmptyList is reached, at which point ask the preceding IList to return its first. 

/**
* Static method of some class.
*/
public static void removeLast(IList list) {
  if (list instanceof IEmptyList) {
    throw new IllegalArgumentException("Empty list has no data.");
  }
  IList next = ((INEList)list).getRest();  
  while (!(next instanceof IEmptyList)) {
    list = next;
    next = ((INEList)list).getRest();
  }
  return ((INEList)list).getFirst();
}
   
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.  It is error-prone because it involves a lot of tedious book-keeping and flow control.  For example, it is very easy to forget to type-cast list to INEList as shown in the above.

 

Now consider how the OO algorithm is implemented.

Object-Oriented Algorithm (OOA)

Object-Oriented Coding (OOC)

An algorithm to find and return the last element from an IList is a concrete IListAlgo visitor.
public class LastElement implements IListAlgo {
  // Singleton Design Pattern
    public static final LastElement Singleton = new LastElement();

    private LastElement() {
  }
When the IList host is empty, throw an exception.  The code on the right simply expresses such an action.
  public Object emptyCase(IEmptyList host, Object inp) {
    throw new IllegalArgumentException("Empty list has no data.");
  }

When the IList 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's first as a parameter just in case this is true and the last element needs to be returned.

  public Object nonEmptyCase(INEList host, Object inp) {
    return host.getRest().execute(HelpGetLast.Singleton, host.getFirst());
  }
}
The algorithm for a IList host to help return the last element from the preceding IList, p, is a concrete IListAlgo visitor whose input parameter is the first element of p.
public class HelpGetLast implements IListAlgo {
  // Singleton Design Pattern
    public static final HelpGetLast Singleton = new HelpGetLast();

    private HelpGGetLast() {
  }
The empty IEmptyList case marks the end of the list: simply returns the input parameter.
  /**
  * @param inp the first element of the preceding host list.
  */  
  public Object emptyCase(IEmptyList host, Object inp) {
    return inp;
  }
A  non-empty INEList case: recursively asks the INEList host's rest for help to return its last element passing the host's first element as a parameter.
  /**
  * The last element of the (non-empty) host is the
  * last element of the preceding host list.
  */
  public Object nonEmptyCase(INEList host, Object inp) {
    return host.getRest().execute(this, host.getFirst());
  }
}
   
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 type of the host IList.  Polymorphism is put to use to direct all flow control, reducing code complexity.

 

Singleton Design Pattern

In the above, only one instance of LastElement is needed to perform the task of finding the last element of a list.  There is no need to instantiate a LastElement object each time we want to find the last element of a list.  In order to create exactly one instance of LastElement and enforce the uniqueness of such an instance, we apply the Singleton Design Pattern, as shown in the above code.  The same holds true for HelpGetLast.

The Singleton Design Pattern is a coding pattern that is used to model and enforce the uniqueness of a particular type of object.  If a class does not have any non-static fields, it need not have multiple instances and should be implemented as a singleton to save time and space.

Anonymous Inner Class

The HelpGetLast defined in the above will only perform correctly if it is passed the appropriate parameter: the IList  that precedes the host.  Though it is called once, inside of LastElement.nonEmptyCase(...), by the original INEList to :"help" it determine whether or not it is holding the last element and return that element if this is the case, we have to go through the standard process of defining a class for it.  In Comp 210, Scheme helper functions are best created "on-the-fly" as "local" lambda expressions.  Is there an analogous way in Java to create helper visitor objects dynamically (i.e. on-the-fly) without having to give names to their classes?  The answer is yes: anonymous inner class!  We will study this type of classes later and make extensive use of them.  In the mean time, let's write some more IListAlgo.


2. More Examples:

In class exercises.


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