1 An example of similar methods
2 Abstracting Over Methods with Similar Structure
3 Using Classes that Capture Methods
4 Parameterizing Methods Passed as Arguments
5 Summary

Method Abstraction

Kathi Fisler

Credits: This lecture derives from Felleisen et al’s How to Design Classes.

Writing clean, reusable code is a key component of software design. Reusable code arises from abstracting over common patterns between code fragments, so that later fragments can be expressed in terms of more general constructs. So far, we’ve studied abstracting over shared fields and methods with abstract classes, and abstracting over similar code sans types via generics. Today, we turn to a different problem: abstracting over similar code sans functions. An example will make this clearer.

1 An example of similar methods

Last week, we were talking about menu items. Imagine that we had a list of menu items (i.e., a menu) and two different methods for extracting a subset of those items: one that selected inexpensive items and one that selected items featuring garlic. We could write these functions as follows:

  interface IMenu {

    // select items under $10

    IMenu selectLowPrice();

    // select items with "garlic" in the name

    IMenu selectHasGarlic();

  }

  

  class Mt implements IMenu {

    Mt (){}

  

    public IMenu selectLowPrice() {

      return this;

    }

  

    public IMenu selectHasGarlic() {

      return this;

    }

  }

  

  class Cons implements IMenu {

    MenuItem first;

    IMenu rest;

  

    Cons (MenuItem first, IMenu rest){

      this.first = first;

      this.rest = rest;

    }

  

    public IMenu selectLowPrice() {

      IMenu r = this.rest.selectLowPrice();

      if (this.first.hasLowPrice())

        return new Cons(this.first, r);

      else

        return r;

    }

  

    public IMenu selectHasGarlic() {

      IMenu r = this.rest.selectHasGarlic();

      if (this.first.hasGarlic())

        return new Cons(this.first, r);

      else

        return r;

    }

  }

  

  class MenuItem {

    String name;

    int price;

  

    MenuItem(String name, int price) {

      this.name = name;

      this.price = price;

    }

  

    boolean hasLowPrice() {

      return this.price < 10;

    }

  

    boolean hasGarlic() {

      return this.name.startsWith("Garlic");

    }

  }

Notice how selectLowPrice and selectHasGarlic are identical except for the method called on the first element of the list? We would like to write the common code once, configuring the different methods via a parameter.

Had we been in Racket, this abstraction would have been easy. We would write a single selectBy method and provide the name of the selection function as a parameter, something like:

  public IMenu selectBy(?? selectFunc) {

    IMenu r = this.rest.selectBy(selectFunc);

    if (this.first.selectFunc())

      return new Cons(this.first, r);

    else

      return r;

  }

Unfortunately, this isn’t valid Java code, because Java does not support passing methods as arguments (in technical terms, methods are not first-class in Java). This is the right spirit, however, so the question is how we achieve this result within Java’s capabilities.

2 Abstracting Over Methods with Similar Structure

What are we allowed to pass an arguments in Java? We have said repeatedly that in Java, everything is an object. We can pass objects. So if we can create objects that act like functions, we can implement this design.

How do we make an object "act like a function"? Objects contain methods, which are functions. We can certainly make the selectFunc parameter be an object with a method in it that we want to call. This is fine, as long as we have a consistent naming scheme for that method (so we know how to access it). For example, what if we said every selectFunc object had to have a method named choose? Then we could write our desired code as:

  public IMenu selectBy(?? selectFunc) {

    IMenu r = this.rest.selectBy(selectFunc);

    if (selectFunc.choose(this.first))

      return new Cons(this.first, r);

    else

      return r;

  }

Setting aside that we still need a type for selectFunc, this approach would solve our problem.

What can we use as the type for selectFunc? Well, all we really care about is that it provide a choose method that consumes a MenuItem and produces a boolean (note that this is the same contract on each of hasLowPrice and hasGarlic). This is an ideal job for an interface:

  interface IPred {

    // determines whether an item matches selection criterion

    boolean choose(MenuItem m);

  }

  

  public IMenu selectBy(IPred selectFunc) {

    IMenu r = this.rest.selectBy(selectFunc);

    if (selectFunc.choose(this.first))

      return new Cons(this.first, r);

    else

      return r;

  }

We have used the name IPred here as shorthand for "predicate", which is a function that returns a boolean. What objects implement IPred though? Remember that our goal was to use objects to mimic functions passed as arguments. So we just need to create a class for each selection criterion, as follows:

  class HasLowPrice implements IPred {

    HasLowPrice(){}

  

    boolean choose(MenuItem m) {

      return m.price < 10;

    }

  }

  

  class HasGarlic implements IPred {

    HasGarlic(){}

  

    boolean choose(MenuItem m) {

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

    }

  }

3 Using Classes that Capture Methods

Now that we’ve seen how to create the classes that represent functions, we need to know how to use them. The easiest way to demonstrate this is through test cases in the Examples class:

  class Examples {

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

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

    MenuItem pasta = new MenuItem("pasta", 11);

    IMenu M1 = new Cons(salad,

                        new Cons(bread,

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

    IMenu M2 = new Cons(bread, new Mt());

    IMenu M3 = new Cons(salad, M2);

  

    boolean testGarlic (Tester t) {

      return t.checkExpect{M.selectBy(new HasGarlic()), M2};

    }

  

    boolean testPrice (Tester t) {

      return t.checkExpect{M.selectBy(new HasLowPrice()), M2};

    }

  }

So in order to tailor our selection according the HasGarlic method, we pass a HasGarlic object into selectBy; we handle HasLowPrice similarly.

4 Parameterizing Methods Passed as Arguments

What if we wanted to allow a user of our menu library to customize the limit on the price? In other words, we want to replace the constant 10 in HasLowPrice with a parameter. Here’s the current HasLowPrice class again:

  class HasLowPrice implements IPred {

    HasLowPrice(){}

  

    boolean choose(MenuItem m) {

      return m.price < 10;

    }

  }

We can’t change the type on choose (that’s required by IPred). We can, however, have the constructor for this class take the parameterized limit as an argument though, and store it in a field:

  class HasLowPrice implements IPred {

    int high;

  

    HasLowPrice(int high){

      this.high = high;

    }

  

    boolean choose(MenuItem m) {

      return m.price < high;

    }

  }

For those of you who had 1102, this shows how to mimic closures in Java: whereas Racket automatically saves closed-over values in lambdas, in Java we create the closure manually by passing in free variables as constructor arguments.

5 Summary

To summarize, we get the effect of functions as arguments in Java by:
  1. creating a class for each function we want to pass as an argument,

  2. picking a consistent name for the method that will provide the actual function,

  3. creating an interface that requires that consistent name (and its input/output types, and

  4. having the class for each function implement the interface that specifies the naming convention.

Those of you with prior Java experience may be wondering why we are going through this exercise. After all, Java provides built-in iterators on lists (which we will talk about later in the term). Yes, it does. In many cases, the built-in iterators will give you what you need. We cover this though because sometimes you will need to write your own data structure (for which Java does not provide iterators). This way, you see how iterators are built so you can build them for yourself as needed.