HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

As we try to keep pace in the frantic Internet era, it's easy to become enraptured by the latest technologies - JavaBeans, CORBA, Swing and so on. However, in the rush to add the latest buzzword to our resumes or marketing brochures, we too often forget the basics of object-oriented programming. The basics were the reason OOP was developed and what first attracted us to this paradigm. How many of us have had the dismaying experience of coming across "object-oriented code" that, although it might implement a "cross-platform, event-driven, multithreaded, multitiered" application, had all the elegance and organization of spaghetti code written in unstructured Basic.

The basic principles underlying the object paradigm haven't changed since its inception. Although not all experts agree, the most common definition of an object-oriented language is that it's required to support four distinct concepts - encapsulation, data abstraction, polymorphism and inheritance. In this article we'll describe how attention to these basics has paid big dividends for us in creating the HotScheme interpreter. But first, let's review what each of these concepts means.

  • Encapsulation: Generally, most aspects of a class should be invisible to objects outside the class. By encapsulating member variables and methods (i.e., making them private or protected), a design can more precisely control their usage and therefore ensure the correctness of that usage.
  • Data abstraction: Class A, which uses class B, should be able to interact with a narrowly defined interface to B. This interface should present A with a view of B's usage, not its implementation. For example, a user of a symbol table class doesn't need to know if it's implemented as a hashtable, an array or some other concrete data structure. Users only need to know how to place a symbol in the table and how to get its value back out.

  • Polymorphism: Having defined an interface as above, objects that implement that interface should generally be interchangeable. If class A needs an object of class "SymbolTable" passed to its constructor, then all descendants of SymbolTable should also be acceptable parameters.
  • Inheritance: A class should be able to inherit as much of its behavior as possible from more primitive classes with the same signature, in order to maximize code reuse.
How These Principles Helped
When we began working on HotScheme, we didn't know where we would take the project. To test our ideas about how to build the interpreter engine (see "Design Patterns in a Java Interpreter," JDJ Vol. 4, issue 1), we started out using a simple character terminal I/O. This was the interface we had seen in most of the Lisp interpreters we had dealt with. We opened a terminal window, output a prompt and read the user's input as it occurred. As soon as we saw a closing parenthesis that matched the first opening one, we parsed and ran the user's code.

It would have been "easiest" to simply call System.out.println() and System.in.read() everywhere we needed to do I/O. However, this would have been an example of what Larry Wall, the creator of Perl, refers to as "false laziness." We would have killed off some of the work in version one only to have the ghost of that work haunt us, with an effort many times the initial savings, in versions two and three.

Fortunately, we make every effort we can to practice true laziness. Therefore, we sent all I/O through an object we called "LispTerminal." We defined a simple but sufficient interface to the class, using the time-tested metaphor of a character terminal as our model - an example of data abstraction. The class needed just three public methods - read(), print() and unread().

For the convenience of the users of the class (us!), we added a println() method (see Listing 1). We created a single, simple descendant of LispTerminal, CharLispTerminal, which sent its I/O to System.out and System.in. These actual I/O channels were hidden from users of the class through encapsulation (see Listing 2).

We decided to pass an instance of this class to methods that used it, rather than using a member variable, so that a new terminal type could be plugged in at any point while running. Here is a case of a general preference: when a private method needs to access a member variable of its own class, we like to pass it as a parameter to the method rather than access it directly. This may seem a curious waste of CPU cycles, but we've found that this practice offers in return a great deal of flexibility in using these functions, and even aids in the occasional repartitioning of a class hierarchy: if the method is directly accessing a class member, it's not easy to move it to another class.

Having completed our "proof-of-concept" cut at the interpreter, we decided we should be able to run HotScheme as an applet. This would allow an institution wanting to use HotScheme to place a single copy of the interpreter on its Web server where any student with a browser could access it. Running as an applet entailed restraints on what we could do. Besides the usual limitations imposed for security, we also had to worry about whether the JDK features we employed were generally supported in browsers.

Since we'd planned for change and extension from the start, we were able to add the GUI interface without changing the underlying interpreter. We descended another class, GUILispTerminal from LispTerminal, which had to perform some tricks that LispTerminal did not. Instead of continuously feeding input to the interpreter, GUILispTerminal forwards all input only when an "Evaluate" button is clicked.

Otherwise we'd have hit conflicts between the user's ability to interact with the GUI and the interpreter's operation. For example, if we were interpreting typing as it occurred, what would we do when the user clicked the "Trace On" button in the middle of typing in a command?

Another new issue is that we now had the possibility of hitting an end-of-input condition. In character mode the interpreter simply kept trying to read a character until the user quit the application. This wouldn't work in GUI mode because we'd eventually reach the end of our input buffer. We wanted to grab control back from the interpreter and collect another batch of input. We added a single new call, eof(), and had it always return false in the base class but true in GUILispTerminal whenever the end of the buffer had been reached. A true return from eof() would break out of the interpreter's endless read-evaluate-print loop.

Since we were no longer using simple character I/O, we had to devise a replacement for our use of a PushbackInputStream in CharLispTerminal so we could "unread" a character in GUI mode as easily as we could in character mode. We had the GUI put all keystrokes into a buffer internal to GUILispTerminal. Unreading a keystroke became as simple as decrementing an index into that StringBuffer:

public void unread(int c) { if(pos > 0) pos--; }

To implement the above, we had to add a new call for use from the GUI, which we called setBuffer(). This change was transparent to the interpreter itself, however, as the call isn't needed from its vantage point. See Listing 3 for the implementation of GUILispTerminal.

Our next adventure with the LispTerminal family came when we realized that the ability to load a package of Scheme code from a URL would be useful. A site hosting HotScheme could then supply users with packages of Scheme code as libraries, sample programs and student exercises.

Our new class, URLLispTerminal, shared the need for buffering with GUILispTerminal, so we created a new common ancestor for them, BufferedLispTerminal, and moved the methods and members that enabled buffering into that class (see Listing 4). The public methods that moved were eof(), read(), unread() and setBuffer(). Using inheritance, we were then able to make use of this code in both GUILispTerminal and URLLispTerminal.

The constructor for a URLLispTerminal takes a URL and another LispTerminal as arguments. Input is fetched from the URL, while all output is merely passed through to the contained terminal that was passed in the constructor. To load and run a file of Scheme code, all we had to do was pass a new instance of URLLispTerminal to our existing function, LispInterpreter.read_eval_print_loop(). This was done in the simple functor that implements the load command. The functor itself is essentially one line of code surrounded by some exception handling:

// the StringVal of the first arg is the URL,
// env.getTerm will return the current LispTerminal,
// and env is essentially the current symbol table
LispInterpreter.read_eval_print_loop(
new URLLispTerminal(
args.first().StringVal(), env.getTerm()),
env);

Although adding the ability to read code from a file based on user-typed input, adding the definitions in the file to the current environment, running the code and then returning control to the user might seem like a job that would involve changes thoughout the program, no code outside of the LispTerminal package and the Load functor changed.

We decided that we'd hit on a mechanism that would be generally useful, and abstracted it for all LispTerminals. In the base class we added two member variables, one to hold an input LispTerminal, the other an output LispTerminal. We redefined the base class implementations for reading and writing so that they'd first check the appropriate member. If it's not null, they delegate the I/O task to the contained object. This change was completely contained within the LispTerminal hierarchy. We'll illustrate the pattern with print() - read(), unread() and eof() are all similar:

public final void print(Object obj)
{
if(out_term != null) out_term.print(obj);
// polymorphism - the else clause will call the descendant's my_print()
else
my_print(obj);
}

When we implemented our "plug-and-play" terminals, it occurred to us that we simply might have designed the interpreter to use separate I/O channels and done away with the terminal concept. This method would have some advantages. There'd be no need for a forwarding mechanism in LispTerminal or, indeed, any kind of terminal class at all. Everywhere we passed a LispTerminal, we would instead pass two parameters: an input sink and an output sink. However, we decided we liked the compactness of a single terminal object and the implication that both sides of I/O had to be handled as a unit. After all, in most cases the nature of the input will be tied to the nature of the output. Moreover, without the terminal object to keep track of the I/O delegation, some other mechanism would need to be created to do so. Having the LispTerminal class keeps this code all in one spot.

We're convinced that we can now easily create new terminals that combine socket, GUI, terminal and file I/O in any arbitrary combination desired. We could, for instance, create a terminal that teed its output, perhaps writing to both the screen and a file, or we could send code off to a fast server for processing.

Our design was far from ideal when we started coding. With perfect foresight, the addition of the delegation mechanism and the buffered class would have been unnecessary - we would have included them from day one. But only gods produce perfect designs. Because we followed the four principles of object-oriented design, none of the changes we had to make were onerous, and each took less than a day's worth of coding.

Our next step in the HotScheme project is to make SchemeObject a bean. Scheme differs from Java in that even the most primitive constructs in the language, such as "if" and "case," can be considered Scheme objects in their own right. In our implementation this is represented by having all of them descend from the base class SchemeObject. We intend to implement a JavaBeans interface to all of the language constructs in Scheme. This will permit the construction of visual editors for the language itself. Rather than presenting an abstract graphical model, then writing out a batch of text code quite different in structure from the graphical representation, it will be the actual code presenting itself graphically that will appear in the editor. Students will be able to pick "if," "else," "case," "+" and other language elements off a bean palette and connect them graphically. A bean will know what other types of beans and how many it can be connected to, and it can offer students help in creating a statement. For instance, if a student picks the "+" bean, the editor could indicate that all its input connections must evaluate to numbers. The student could attach numbers, symbols or functions to the bean only as input. (Unfortunately, we couldn't tell in advance if a particular symbol or function would result in a number - to do that we'd have to be able to run the program before the student finished constructing it!)

HotScheme is an open source project, and we encourage sites interested in using the tool and developers who wish to contribute to contact us. The Web site for the project, which includes two applet versions, JavaDocs, sample Scheme code and an archive of the source code is http://stgtech.com/HotScheme/.

Author Bios
Gene Callahan is president of St. George Technologies, where he designs Internet projects. He has written articles for Computer Language, Software Development and Web Techniques, among others. He can be reached at [email protected]

Brian Clark is a software engineer residing in Virginia. His current focus is on the application of design patterns on UI and middle-tier design using Java. He can be reached at [email protected]

	

Listing 1: 

abstract public class LispTerminal extends HotSchemeInternalRep 
{ 
    public LispTerminal() 
 { 
 } 
    publicLispTerminal(LispTerminal in, 
 LispTerminal out) 
 { 
     in_term  = in; 
     out_term = out; 
 } 
    public final void print(Object obj) 
    { 
    if(out_term != null) 
 out_term.print(obj); 
         else 
 my_print(obj); 
    } 
    abstract public void my_print(Object 
 obj); 

    public final void print(int i)  { 
 print(new String((new 
 Integer(i)).toString())); } 
    public final void print(long l)    { 
 print(new String((new 
 Long(l)).toString())); } 
    public final void println(Object obj) 
    { 
         if(out_term != null) 
 out_term.println(obj); 
        else  my_println(obj); 
    } 
    abstract public void my_println(Object 
 obj); 
    public int read() 
    { 
        if(in_term != null) return 
 in_term.read(); 
        else       return my_read(); 
    } 
    abstract public int my_read(); 
    public void unread(int c) 
    { 
        if(in_term != null) 
 in_term.unread(c); 
        else        my_unread(c); 
    } 
    abstract public void my_unread(int c); 
    public boolean eof() 
    { 
        if(in_term != null) return 
 in_term.eof(); 
        else         return my_eof(); 
    } 
    abstract public boolean my_eof(); 
    public SchemeToken getToken() 
    throws SchemeException 
    { 
        if(token_stack.empty()) 
         return new SchemeToken(this); 
        else return 
 (SchemeToken)token_stack.pop(); 
    } 
    public void pushToken(SchemeToken 
 token) 
    { 
        token_stack.push(token); 
    } 
// place for clients to push tokens back to: 
  private Stack token_stack = new Stack(); 
    private LispTerminal in_term  = null; 
    private LispTerminal out_term = null; 
} 
import java.io.*; 
import java.util.Stack; 

Listing 2: 

public class CharLispTerminal extends LispTerminal 
{ 
    public CharLispTerminal(InputStream 
 in, PrintStream out) 
    { 
    input  = new PushbackInputStream(in); 
    output = out; 
    } 
    public void my_print(Object obj) 
    { 
    output.print(obj); output.flush(); 
    } 
    public void my_println(Object obj) 
    { 
    output.println(obj); 
    } 
    public int my_read() 
    { 
    int c = 0; 
    try 
    { 
    c = input.read(); 
    } 
    catch(java.io.IOException e) 
    { 
            ; 
        } 
        return c; 
    } 
    public void my_unread(int c) 
    { 
        try 
        { 
            input.unread(c); 
        } 
        catch(java.io.IOException e) 
        { 
            ; 
        } 
    } 
    public boolean my_eof() 
    { 
    return false; 
    } 
    private PushbackInputStream input; 
    private PrintStream output; 
} 
import java.util.Vector; 

Listing 3: 

class GUILispTerminal extends BufferedLispTerminal 
{ 
 private Vector  m_vOutput; 
 private boolean debug = false; 
    public GUILispTerminal() 
    { 
  m_vOutput = new Vector(5); 
 } 
 private void print(String s) 
 { 
  m_vOutput.addElement(s); 
  if(debug) 
  { 
System.out.println("GUILispTerminal::print Vector is " + m_vOutput); 
System.out.println("GUILispTerminal::print " + s); 
  } 
 } 
 public void setInput(String s) 
 { 
 setBuffer(s); 
  if(debug) System.out.println("GUILispTer- 
 minal::setInput " + s); //debug 
 } 
 public Vector getOutput() 
 { 
 if(debug) System.out.println("GUILispTer- 
 minal::getOutput " + m_vOutput); //debug 

  Vector vTemp = (Vector)m_vOutput.clone(); 
  m_vOutput.setSize(0); 
  return(vTemp); 
 } 
    public void my_print(Object obj) 
    { 
  print(obj.toString()); 
    } 
    public void my_println(Object obj) 
    { 
  print(obj.toString() + "\n"); 
 } 
} 

Listing 4: 

abstract public class BufferedLispTerminal extends LispTerminal 
{ 
    public BufferedLispTerminal() 
 { 
     super(); 
        CommandBuf = new StringBuffer(""); 
 } 
    public BufferedLispTerminal(LispTerminal in, LispTerminal out) 
 { 
     super(in, out); 
        CommandBuf = new StringBuffer(""); 
 } 
    public int my_read() 
    { 
  if(pos < CommandBuf.length()) 
   return((int)CommandBuf.charAt(pos++)); 
  else 
      return(0); 
    } 
    public void my_unread(int c) 
    { 
  if(pos > 0) pos--; 
    } 
    public boolean my_eof() 
    { 
        if(pos < CommandBuf.length()) 
        { 
// if everything else is spaces, return true, else false 
            for(int i = pos; i < CommandBuf.length(); i++) 
            { 
              if(!Character.isWhitespace(CommandBuf.charAt(i))) 
                    return false; 
            } 
            CommandBuf.setLength(pos);  // we won't look at those spaces again 
        } 
        return true; 
    } 
    protected void setBuffer(String s) 
    { 
        pos = 0; 
        CommandBuf.setLength(0); 
        CommandBuf.append(s); 
    } 
    private StringBuffer CommandBuf; 
   private int pos = 0; 
} 


 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.