1 An example of similar methods
2 Abstracting Over Methods with Similar Structure
2.1 A Type and Better Name for some Object
2.2 Creating IMatch Classes
2.3 Using select Matches
3 Parameterizing Methods Passed as Arguments
4 But Why Would Anyone Do This???
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 wanted two different methods for extracting a list of some of those items: one that selected inexpensive items and one that selected items featuring garlic. We could write these functions as follows:

  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");

    }

  }

  

  class MenuList {

    LinkedList<MenuItem> menu = new LinkedList<MenuItem>();

  

    // add an item to a menu

    public void addItem(MenuItem m) {

      menu.add(m);

    }

  

    // produce number of low-price items

    public LinkedList<MenuItem> selectLowPrice() {

      LinkedList<MenuItem> result = new LinkedList<MenuItem>();

      for (MenuItem m : menu) {

        if (m.hasLowPrice())

          result.add(m);

      }

      return result;

    }

  

    // produce MenuList of garlicy items

    public LinkedList<MenuItem> selectHasGarlic() {

      LinkedList<MenuItem> result = new LinkedList<MenuItem>();

      for (MenuItem m : menu) {

        if (m.hasGarlic())

          result.add(m);

      }

      return result;

    }

  }

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

For those of you who’ve seen Racket, this is just a simple use of filter. We would pass either the hasLowPrice or the hasGarlic function as an argument to filter. Assuming we wrote our own filtering function called selectBy, analogous code in Java might look something like:

  public LinkedList<MenuItem> selectMatches(??? shouldSelect) {

    LinkedList<MenuItem> result = new LinkedList<MenuItem>();

    for (MenuItem m : menu) {

      if (shouldSelect(m))

        result.add(m);

    }

    return result;

  }

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 shouldSelect parameter be an object with a method in it named shouldSelect. Then, our selectMatches code could just call that the shouldSelect method from within the object. In other words, we could write:

  public LinkedList<MenuItem> selectMatches(??? someObject) {

    LinkedList<MenuItem> result = new LinkedList<MenuItem>();

    for (MenuItem m : menu) {

      if (someObject.shouldSelect(m))

        result.add(m);

    }

    return result;

  }

This is valid Java code, minus the lack of a type (and a more descriptive name) for someObject. So we need to figure out how to fill in the ???, and how to use this method to select our low price and garlicy items again.

2.1 A Type and Better Name for someObject

What is someObject doing? It is trying to provide a way to capture whether a menu item matches some criterion (such as low price or a name with garlic). For starters, matcher is a better name for this object:

  public LinkedList<MenuItem> selectMatches(??? matcher) {

    LinkedList<MenuItem> result = new LinkedList<MenuItem>();

    for (MenuItem m : menu) {

      if (matcher.shouldSelect(m))

        result.add(m);

    }

    return result;

  }

What can we use as the type for matcher? Well, all we really care about is that it provide a shouldSelect 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 IMatch {

    // determines whether an item matches selection criterion

    boolean shouldSelect(MenuItem m);

  }

  

  public LinkedList<MenuItem> selectMatches(IMatch matcher) {

    LinkedList<MenuItem> result = new LinkedList<MenuItem>();

    for (MenuItem m : menu) {

      if (matcher.shouldSelect(m))

        result.add(m);

    }

    return result;

  }

This is all fine (compiles and all), but to use this method we would need objects of type IMatch. What sort of objects should implement IMatch? Put differently, how do we actually use this selectMatches method?

2.2 Creating IMatch Classes

Remember where we started: we had two similar methods (hasLowPrice and hasGarlic), and we created selectMatches to share their common code. So the object passed as matcher must supply the parts of hasLowPrice and hasGarlic that were not in common. For each of those original methods, we create a class of type IMatch whose shouldSelect method captures the non-shared computation:

  class LowPriceMatcher implements IMatch {

    public boolean shouldSelect(MenuItem m) {

      return m.hasLowPrice();

    }

  }

  

  class HasGarlicMatcher implements IMatch {

    public boolean shouldSelect(MenuItem m) {

      return m.hasGarlic();

    }

  }

2.3 Using selectMatches

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. For starters, assume we had an Examples class with the following MenuList:

  class Examples {

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

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

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

    MenuItem chicken = new MenuItem("garlic chicken", 14);

    MenuItem dessert = new MenuItem("garlic ice cream", 4);

    IMenu M = new MenuList();

  

    Examples(){

      M.addItem(bread);

      M.addItem(salad);

      M.addItem(pasta);

      M.addItem(chicken);

      M.addItem(dessert);

    }

  }

For the original classes that just implemented selectLowPrice and selectHasGarlic directly, we might have written the following test cases (note these just test the size of the output, not the contents—full test cases would also check the contents):

  boolean testPrice1(Tester t) {

    return t.checkExpect(M.selectLowPrice().size(),3);

  }

  

  boolean testGarlic1(Tester t) {

    return t.checkExpect(M.selectHasGarlic().size(),3);

  }

Since MenuList has replaced the selectLowPrice and selectHasGarlic with selectMatches, we need to rewrite the test cases as well. Here’s how the revised test cases look:

  boolean testPrice1(Tester t) {

    return

      t.checkExpect(M.selectMatches(new LowPriceMatcher()).size(),

                    3);

  }

  

  boolean testGarlic1(Tester t) {

    return

      t.checkExpect(M.selectMatches(new HasGarlicMatcher()).size(),

                    3);

  }

}

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

3 Parameterizing Methods Passed as Arguments

What if we wanted to allow a user of our menu program to customize the limit on the price? In other words, we want to replace the constant 10 in HasLowPrice (in the MenuItem class) with a parameter that gets configured when we use selectLowPrice. First, we need to add the parameter to hasLowPrice:

  boolean hasLowPrice(double limit) {

    return this.price < limit;

  }

Now, any calls to hasLowPrice must to provide a value for that parameter. We call hasLowPrice from LowPriceMatcher:

  class LowPriceMatcher implements IMatch {

    public boolean shouldSelect(MenuItem m) {

      return m.hasLowPrice(???);

    }

  }

What value do we pass to hasLowPrice? Our goal is to let the user of the LowPriceMatcher class supply that value. How do we provide values to classes? As inputs to constructors. So we expand the LowPriceMatcher class to take the value as a constructor input, as follows:

  class LowPriceMatcher implements IMatch {

    double limit;

  

    LowPriceMatcher(double limit) {

      this.limit = limit;

    }

  

    public boolean shouldSelect(MenuItem m) {

      return m.hasLowPrice(this.limit);

    }

  }

Now, we edit calls to the LowPriceMatcher constructor to supply the limit value. That occurs in our test cases:

  boolean testPrice1(Tester t) {

    return

      t.checkExpect(M.selectMatches(new LowPriceMatcher(10)).size(),

                    3);

  }

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.

4 But Why Would Anyone Do This???

Creating these extra classes and the interface seems a lot messier than just having the duplicated for loop from the starter code. So why on earth would we do this?

The real point here is less about cleaning up your code, and more about the methods that you provide for others who might use your code.

Imagine that you were writing a great Java package for managing menus. Clients will import your code (just as you have been importing the LinkedList class, for example). If your package provided only a fixed collection of select methods (like selectLowPrice and selectHasGarlic), then your clients can’t use your package with their own search criteria. If you provided the general select method, however, then your clients can search with their own criteria.

In short, when you create packages for other people, you want to include methods that let them effectively search, combine, or otherwise traverse the data that they put into your package. The ability to pass methods as arguments is critical for providing such support.

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 significant prior Java experience may be wondering why we are going through this exercise. After all, Java provides built-in iterators on lists. 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. Lists are just a conceptually easy example on which to illustrate this general point.