Saturday, November 01, 2008

In search of the perfect API (part 3)

This is the third installment of the short series discussing how to enhance a library API to make it better for its users. The series is based on DesignGridLayout as a practical example.

The first post discussed the original API of DesignGridLayout V0.1, focusing on its weak points for the end user.

The second post introduced the API of V0.9, showing how most of V0.1 pain points were solved (in particular by using interfaces and making public only what has to be public).

This third post will describe two evolutions (performed in two distinct steps) made in the current development trunk (which I call 1.0-SNAPSHOT), originated in new features requested by some users.

First evolution to 1.0-SNAPSHOT

When implementing issue #5 (vertical resize behavior), it had to be possible to add a "vertical grow weight" factor to any kind of row (except empty rows), in the same manner as you can assign "weighty" in Swing GridBagLayout.

For that, I only modified DesignGridLayout class:
public class DesignGridLayout {
...
public INonGridRow leftRow();
public INonGridRow leftRow(double weight);
public INonGridRow rightRow();
public INonGridRow rightRow(double weight);
public INonGridRow centerRow();
public INonGridRow centerRow(double weight);
public IGridRow row();
public IGridRow row(double weight);
public void emptyRow(int height);
}
That was really straightforward and it worked well for the library user. Note that we still did not solve the API issue (existing from V0.1) allowing multiple calls to label() on grid rows, but where only one call is effective.

Second evolution of 1.0-SNAPSHOT


Issue #13 (grids with multiple labels) was an important request from DesignGridLayout users so I had first to find the right API before implementing this feature.

The first idea that came to my mind was an API that would allow the following snippet:
layout.row().label(lbl1).add(field1).label(lbl2).add(field2);
layout.row() .add(field3).label(lbl4).add(field4);
So I need multiple calls to label(JLabel) (which was easy because the current API already enabled that, that was even its main remaining flaw).

Please note the absence of a label at the beginning of the second row, which means that there will be no label at this position in the displayed form (however empty space will be added, so that field1 and field3 are vertically aligned with each other). But this possibility required to have the same possibility for the second (and next) label in a row, I needed something like that:
layout.row().label(lbl1).add(field1).label(lbl2).add(field2);
layout.row() .add(field3).label() .add(field4);
Note the new no-arg label() method. Here is the new IGridRow interface (excerpt):
public interface IGridRow {
...
IGridRow label(JLabel);
IGridRow label();
}
Adding this method solves the problem but leads to inconsistency in the API, that we see in the snippet above, you now have two ways to obtain the same result:
layout.row().label(lbl1).add(field1).label(lbl2).add(field2);
layout.row() .add(field3).label() .add(field4);
or
layout.row().label(lbl1).add(field1).label(lbl2).add(field2);
layout.row().label() .add(field3).label() .add(field4);
The second way should actually be the only way to get this result! There are two reasons why I want that:
  1. API Consistency (as mentioned before)
  2. It allows unclear code when creating specific layouts where one would want to completely skip the first label AND the first field (and define only the second label and field)
For example, what layout would be produced by the following snippet:
layout.row().label(lbl1).add(field1).label(lbl2).add(field2);
layout.row().label(lbl4).add(field4);
Should lbl4 belong to the first label column or the second one? If we want to avoid such inconsistencies we have to make it mandatory to call one of both label() methods when starting a grid row.

Concretely this requires adding a new interface returned by DesignGridLayout#row():
public class DesignGridLayout {
...
public ISubGridStarter row();
public ISubGridStarter row(double weight);
}

public interface ISubGridStarter {
public IGridRow label(JLabel label);
public IGridRow label();
}
I also refactored IGridRow interface to extend ISubGridStarter:
public interface IGridRow extends ISubGridStarter {
...
}
This way, we have one unique, consistent way to define our layout.

In next installment, I will explain some last minute changes in the 1.0-SNAPSHOT API, in order to make it definitely better (and easier to use).

No comments:

Post a Comment