Sunday, November 02, 2008

In search of the perfect API (part 4)

This is the fourth post of my series about how the API of DesignGridLayout library has been improved. This series can be useful for anyone developing libraries (DesignGridLayout just illustrates improvements).

The first post discussed the original V0.1 API and its related issues.

The second post has shown how to solve most V0.1 issues.

The third post has dealt with API improvements driven by new features. In particular, one interesting change to the API has been to introduce a new interface ISubGridStarter to reduce the list of available methods in a context, these methods then returning the "full" interface IGridRow.

In this installment, I will talk about the latest changes made to the API before V1.0 is released (which should happen very soon now).

Latest 1.0-SNAPSHOT: back to 0.1?

First of all, I was not very happy with the new methods I introduced in DesignGridLayout class in one of both previous snapshots:
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);
There is a lot of repeat here. In addition, it is not clear why "row()" is not "gridRow()" (lack of consistency).
In addition, the ISubGridStarter interface uses methods all named "label()" to actually do 2 things:
  1. Start a new canonical sub-grid
  2. Set a label to this new sub-grid (or no label at all)
So the name is not representative of the method behavior. In particular, it is extremely weird to call label() with no argument to indicate we start a new sub-grid with no label!

Actually, I prefer something like that (in terms of usage):
layout.row(0.5).grid() .add(...).grid(lbl3).add(...);
Several changes have occurred to make this kind of code possible.

First I introduced (once again) a new interface:
public interface IRowCreator extends ISubGridStarter {
public INonGridRow left();
public INonGridRow right();
public INonGridRow center();
I also modified ISubGridStarter (only the method names):
public interface ISubGridStarter {
public IGridRow gridJLabel);
public IGridRow grid();
You have to remember that IGridRow extends ISubGridStarter (which allows to later start new sub-grids in a row).

Finally, I simplified DesignGridLayout API:
public class DesignGridLayout implements LayoutManager {
public IRowCreator row();
public IRowCreator row(double weight);
public void emptyRow();
Now we have only 2 general methods to create a new row (whatever its type), one allows you to specify the vertical growth weight factor (the no-arg row() will automatically determine if the actual row should have variable height and, if so, assign it a default weight of 1.0).

Also note that we still keep emptyRow(), but it has lost its "int height" argument, this is due to an improvement of the behavior (issue #16: make emptyRow smarter).

Now we can code our layouts like that:
layout.row().grid(lbl1).add(field1)             .grid(lbl2) .add(field2).add(field3);
layout.row().grid() .add(field4).add(field5) .grid(lbl3) .add(field6);
layout.row().grid() .grid(lbl4) .add(field7).add(field8).add(field9);
The snippet above will exhibit the following layout:

The source code reflects the way the container will be laid out.
Code completion is helpful in that it does not authorize useless, or erroneous, method calls.

In fact, we have just built a simple "DSL" for laying out a Swing container!

To summarize, here is what we have gained in consistency (compared with previous 1.0-SNAPSHOT):
  • you add a new row to your form with one of two methods, named the same (row())
  • once a new row has been created, you can only choose its type in a clear way (left(), right(), center() or grid())
  • on a grid row, you can start a new sub-grid at any time with grid(), this is consistent with the way you started the grid row (because a grid row starts with a sub-grid)
In addition to previous improvements:
  • it is never possible for users to call a meaningless API (e.g. add(JComponent child, int span) in a non-grid row)
  • thus the number of options (callable methods) to the user in a context is restricted to its minimum (easing the search of methods through code completion in your IDE)
Please note that throughout this series, the API was simplified a little bit (there are a few more methods to some interfaces, but not many) in order to make it easier to follow.

This API should now be considered stabilized and there should be no change until the first official release candidate 1.0-rc1 of DesignGridLayout (which should be in a few days now).

In the next (and last) installment, I will talk about the future extensions of DesignGridLayout API (due for V1.1), necessary to implement the issue #10 (Support for components spanning several rows).

No comments:

Post a Comment