1 Why "visit"?
2 Summary
2.1 Final Note: A Naming Variation

Method Abstraction over Class Hierarcies

Kathi Fisler

Yesterday, we learned how to parameterize code over methods. The basic idea is to create a class that contains the method argument, pass an object of that class as the actual parameter value, and extract the method argument from the object.

Let’s look at another example. Imagine that I wanted to further refine my menu items into categories such as Starter, Entree, and Dessert items. We do that with the following classes:

  class Dessert extends MenuItem {

    Dessert (String name, int price) {

      super(name, price);

    }

  }

  

  class Starter extends MenuItem {

    Starter (String name, int price) {

      super(name, price);

    }

  }

  

  class Entree extends MenuItem {

    Entree (String name, int price) {

      super(name, price);

    }

  }

Let’s use yesterday’s approach to select a list containing desserts whose name starts with "lemon" and entrees that cost more than $20. In particular, we want the following test case to work in our Examples class:

  class Examples {

    Starter bread = new Starter("garlic bread", 6);

    Starter salad = new Starter("salad", 5);

    Entree pasta = new Entree("pasta", 21);

    Dessert tart = new Dessert("lemon tart", 8);

    IMenu M1 = new Cons(salad,

                        new Cons(bread,

                                 new Cons(pasta, new Mt())));

    IMenu M4 = new Cons (tart, M1);

  

    boolean testEntDess (Tester t) {

      return t.checkExpect(M4.selectBy(new EntDessSelect()),

                           new Cons(tart,

                                    new Cons(pasta, new Mt())));

    }

  }

We start by creating another IPred class (here called EntDessSelect) that will do the selection for us:

class EntDessSelect implements IPred { public boolean choose(MenuItem m) { return ???; } }

How do we fill in this method? Our problem asks for different decisions based on the type of the MenuItem:

  • for Starters, always return false

  • for Entrees, determine whether price is over 20

  • for Dessert, determine whether it starts with "lemon"

Whatever we do here must have some way to ask for the type of the item so we can do the appropriate computation. What are our options?

Can we use instanceof? No. Only use instanceof if you can guarantee that you won’t cut off options to extend the class later. If we hardcode the known instances here, what happens if our menu gains new categories that we want to filter on with this same method? Or if we subdivide desserts into sweet and savory? Instanceof would work, but isn’t a good solution here.

What if we provided a different choose version for each MenuItem extension? Something like:

  class EntDessSelect implements IPred {

    EntDessSelect(){}

  

    public boolean choose(Dessert m) {

      return m.name.startsWith("lemon");

    }

  

    public boolean choose(Starter m) {

      return false;

    }

  

    public boolean choose(Entree m) {

      return m.price > 20;

    }

  }

While this may look similar to using instanceof, it supports extensibility better because we could extend the EntDessSelect class with a subclass that provides choose on a different type. For example,

  class Drink extends MenuItem {

    Drink(String name, int price) {

      super(name, price);

    }

  }

  

  class EntDessDrinkSelect extends EntDessSelect {

    public boolean choose (Drink m) {

      return m.name.equals("wine");

    }

  }

The class structure supports adding methods, but is less flexible for editing the contents of methods (as the instanceof approach would have you do).

Unfortunately, this approach doesn’t pass the type checker. Java complains that there is no choose method on MenuItem. Even though desserts, entrees, and starters are the only current variants of MenuItem, (a) Java doesn’t know that, and (b) wouldn’t enforce it if it did, as future extensions might add more variants.

What we really need to do here is acknowledge that EntDessSelect only knows how to process the current set of menu items, leaving the option to extend the set of known items later as needed. We do this by changing the interface that EntDessSelect implements. Instead of implementing the general IPred (which covers all MenuItem), we implement the following restricted interface:

  interface EntDesStaPred {

    public boolean choose(Dessert m);

    public boolean choose(Starter m);

    public boolean choose(Entree m);

  }

Now, we can provide a predicate against this interface that selects our desired items:

  class EntDessSelect implements EntDesStaPred {

    EntDessSelect(){}

  

    public boolean choose(Dessert m) {

      return m.name.startsWith("lemon");

    }

  

    public boolean choose(Starter m) {

      return false;

    }

  

    public boolean choose(Entree m) {

      return m.price > 20;

    }

  

  }

Now, our Examples class doesn’t compile, as EntDessSelect no longer implements IPred. Rather than mess up selectBy, for now let’s write another version of it that works against EntDessSelect. We’ll call the new version selectByV. Because we have changed the type of the input (relative to selectBy), we also change how we use the input in the if statement. Now, we assume that each menu item has a method called visit that takes our new predicate and calls the appropriate method for its class:

  class Cons implements IMenu {

    MenuItem first;

    IMenu rest;

  

    // constructor and selectBy omitted

  

    public IMenu selectByV (EntDesStaPred menuV) {

     IMenu r = this.rest.selectByV(menuV);

     if (this.first.visit(menuV))

        return new Cons(this.first, r);

      else

        return r;

    }

  }

The Dessert class with its visit method follows:

  class Dessert extends MenuItem {

    Dessert (String name, int price) {

      super(name, price);

    }

  

    public boolean visit(EntDesStaPred menuV) {

      return menuV.choose(this);

    }

  }

1 Why "visit"?

This code illustrates what is known as the visitor pattern, an OO coding style that is used when one has to pass a function over variants as an argument. The idea is that a "visitor" is a class that knows how to process each variant of a datatype (in this case, each known category of menu item). The selectByV code traverses the data structure and "visits" all of the classes within the variant. The "visitor" (in this case, the EntDessSelect class) knows what to do on each "visit".

2 Summary

This material can be a bit confusing and overwhelming, especially if you lose yourself in the details rather than the high-level goal. Our high-level goal is to pass a function as an argument. Functions often behave differently on different variants of a class. When that happens, we create a class that knows how to process each variant. We write the code for each variant in a separate method, but gather all of those methods up into a single "function as object" class (here EntDessSelect). This single "function as object" class is known as a visitor in OO terminology.

The rest of the code is just the details needed to get this pattern to work. First, make sure you understand the high-level concept. Then, focus on the details. We will do another example of this next class, to help you get more familiar with this idea.

2.1 Final Note: A Naming Variation

Some presentations of the visitor pattern use a different name for each method in the visitor. Had we done that here, our interface, visitor and visit methods would have looked like:

  interface EntDesStaPred {

    public boolean chooseDessert(Dessert m);

    public boolean chooseStarter(Starter m);

    public boolean chooseEntree(Entree m);

  }

  

  class EntDessSelect implements EntDesStaPred {

    EntDessSelect1(){}

  

    public boolean chooseDessert(Dessert m) {

      return m.name.startsWith("lemon");

    }

  

    public boolean chooseStarter(Starter m) {

      return false;

    }

  

    public boolean chooseEntree(Entree m) {

      return m.price > 20;

    }

  

  }

  

  class Dessert extends MenuItem {

    Dessert (String name, int price) {

      super(name, price);

    }

  

    public boolean visit(EntDesStaPred menuV) {

      return menuV.chooseDessert(this);

    }

  }

Either version is fine – follow whichever makes more sense to you.