November 2004 archive

Adapting Classes

A few days ago, the wisdom of the java.io.Reader interface dawned on me suddenly, and at the same moment the world of interfaces came into a new light. I’ve always understood what an interface (or pure virtual class) is, and the purpose of them – they allow you to change the implementation of your class without changing the calling code. Some people have even told me that the use of interfaces can replace multiple inheritance – but I never really got how.

For those of you who are unaware, Reader is a basic interface that reads arrays of character data from “some source”. This seems like a good idea, of course. You get the data, and you don’t care about the source. Yay, nice and simple, and everyone is happy. Typically one creates a java.io.FileInputStream, creates a java.io.InputStreamReader (which implements Reader), and you’re off to the races.

One day, another class caught my eye: java.io.BufferedReader. This class implements the Reader interface, but doesn’t specify in the name any kind of data source. How does this class work? A BufferedReader takes another Reader instance as part of its constructor, and adapts it.

Why is this such a special idea? Because BufferedReader is not derived from InputStreamReader. As a result, any Reader can be buffered by this simple class. In the same way, other classes can adapt a BufferedReader to add additional functionality. A LineNumberReader can take a BufferedReader, an InputStreamReader, a KeyboardJunkReader, a RandomDataReader, or a ManagementBullshitReader – whatever Reader one wants to count the lines of. (The fact that LineNumberReader is derived from BufferedReader is irrelevant [and frankly, pointless...])

So, you’re thinking “fine, but so what?”. Let me give you an example of another simple interface that this kind of adapting would be cool for:

public interface XYDataset
{
    public int getCount();
    public Number getX(int index);
    public Number getY(int index);
}

This interface is pretty simple, and would be good as part of a plotting package. Any set of X and Y values could be plotted easily by creating an instance of this XYDataset wherever your data is. This is simple, effective, and cool. A basic implementation of this could have two List objects, or one List object, or … whatever, who cares – storing data is boring.

What if you find that some users are plotting thousands of points, and it’s very slow? Let’s create a filtering adapter class:

public class FilteringXYDataset implements XYDataset
{
    private XYDataset delegate;
    private int maxPoints = 1000; // only plot this many points.

    public FilteringXYDataset(XYDataset initDelegate)
    {
        delegate = initDelegate;
    }

    public int getCount()
    {
        int realCount = delegate.getCount();
        if (realCount < maxPoints)
            return realCount;
        else
            return maxPoints;
    }

    public Number getX(int index)
    {
        int correctItemCount = delegate.getCount();
        if (correctItemCount < numPts)
            return delegate.getX(index);

        int newIdx = (int)(correctItemCount * ((double)item / (double) maxPoints));
        return delegate.getX(newIdx);
    }
    // (repeat for getY)
}

Cool, the dataset is filtered now. It’s a crude filtering, but when you’re plotting a thousand points it’ll do nicely – it’s hard to tell a thousand points from ten thousand points on a normal screen sized plot.

What other dataset adapters could you use?

  • Add an extra 50% to the number of points, and generate them from a bezier smoothing curve.
  • Add a (0, 0) point to every dataset. Pretend the count is one greater, and then add the point in at the appropriate index.
  • Plotting arbitrary X-Y points on a log-log plot is impossible if the points are negative – so chop them out in another adapter class.

Adapting classes like this are simple and nifty – you take one interface, and provide the same interface back to the library user.

The really great part is that, without multiple inheritence, you can later create a dataset that is smoothed, filtered, and has negative points chopped out – all with one line of code. The smoothing and filtering algorithms are only written once, but can be applied in various orders and with various other tools. … and you still don’t know how the XYDataset is being stored. Good!