开发者

What design approach to take when showing a list of heterogeneous items

Let's imagine I want to display a list of stock items in a table (using Java). The domain model consists of an abstract base class StockItem from which various other types of stock item derive. StockItem provides a minimal interface (getId() and getDescription()) but beyond that, the subclasses might have significant variations.

If I restrict myself to the methods defined on StockItem, I won't be able to present sufficient detail to the user so this means that some columns will refer to fields that are not applicable to some rows (for example, physical goods are countable and this count should appear in the table whereas service items, which are also expected to appear in the table, are not countable, in which case "N/A" or similar should be shown).

To stick with the example of "Countable", it seems to me that there are a couple of solutions (but bear in mind that Countable won't be the only interface involved).

  1. Make the countable interface part of the base class and force everything to be Countable even when they aren't. Classes for which this is meaningless would need to return some special value or throw an exception or otherwise indicate that they are breaking the contract of a StockItem.

  2. In my iterator, use lots of instanceof checks and cast appropriately. If I introduce a new subclass of StockItem or otherwise change the inheritence tree, I'll have to remember to change this code.

Both of these seem like antipatterns to me and I'd be interested to hear about any more elegent approaches that might be taken. I suspect there is no magic bullet to this but if other languages have features that make this type of thing easier, I'd be interested to hear them also (only out of general interest though, I won't be reimplemen开发者_Python百科ting this system any time soon:)

Thanks, Phil


Adaptor pattern

In your StockItem class add a the method:

/**
 * Adapt the current instance to the type
 * denoted by clazz else return null
 * ...
 */
public <T> T adapt(Class<T> clazz){
    if( clazz.isInstance(this)){
        return (T)this;
    }
    return null;
}

This will be inherited by all sub-classes and will allow a caller to safely convert type. Don't be put off by the generics, it's just saying I want this method to return an instance that's the same type as the clazz parameter.

Now your table provider can implement something like this:

public String getColumnText(Object cell, int columnIndex) {
    if (cell instanceof StockItem) {
        StockItem item = (StockItem) cell;

        switch(columnIndex) {
            case ID:
                return item.getID;
            case DESCRIPTION:
                return item.getDescription;
            case COUNT:
            Countable countableItem = item.adapt(Countable.class);
                return countableItem == null ? "N/A" : countableItem.getCount();
        }
    }
    return "N/A";
}


I'd start with a table model class, something like a bean with one field for each column in your table. An instance of that class can be displayed easily.

Then I'd introduce an interface

public interface TableModelAdapter {   // bad name - this is not the adapter pattern...
  public TableModel getTableModel();
}

and make StockItem implement this interface. StockItem will implement some default behaviour, all subclasses will add something to the fields in the table model:

public class Book {

  // ..

  @Override
  public TableModel getTabelModel {
    TableModel model = super.getTableModel();  // the initial one with default entries
    model.setTitle(this.title);                // like: a book can display a title
    return model;
   }
}


As a really simple solution you might add a method:

/**
* Get key/value for all fields in the right order.
*/
public SortedPairRepresentation getPairRepresentation(){
  // For example one might use a SortedSet<Pair<String,String,>> pairs
  // that sort on the key (lexicographically or in a custom order 
  // s.th. IDs and description are always first). Or you just rely
  // on StockItem and its Subclasses to return them in the correct order.
  // It's up to you.
}

I admit this is not the most elegant version from the table's POV, but at least it is a method that makes sense on your domain object and has no side effects on other clients of the domain object.

It might even be more clear if you don't enforce a sorting order, but this may come in really handy when you have to display lots of elements, because creating them in a sorted way, ensures that displaying them in the table can be done by simply going through the each pair representation once.


I'd probably go with some kind of factory which can create renderers/components for specific types. A simple example, let's pretend some things are countable, and others have an age limit:

class OrderItem {
    long getId(){ ... }
    String getDescription(){ ... }
}
interface Countable {
    int getCount();
}
interface AgeLimited{
    int getMinimumAge();
}

A simple base renderer with a default implementation would look like this (pretending you just want to render OrderItems as strings; in a real app, you'd probably return a JComponent or whatever):

abstract class Renderer<T extends OrderItem> {
    private final Class<T> type;
    protected Renderer(Class<T> type){
        this.type = type;
    }
    Class<T> getType(){ return type; }
    abstract String render(T orderItem);
}

class DefaultRenderer<T extends OrderItem> extends Renderer<T> {

    public DefaultRenderer(Class<T> type){ super(type); }

    String render(T orderItem){
        return orderItem.getId() + " - " + orderItem.getDescription();
    }
}

A bit verbose - thank you, non-reified generics! - but extending it is easy:

class CountableRenderer<T extends OrderItem & Countable> extends DefaultRenderer<T> {

    public CountableRenderer(Class<T> type){ super(type); }

    String render(T countableOrderItem){
        return String.format("%s (%d pcs)",
                             super.render(countableOrderItem),
                             countableOrderItem.getCount());
    }
}

class AgeLimitedRenderer<T extends OrderItem & AgeLimited> extends DefaultRenderer<T> {

    public AgeLimitedRenderer(Class<T> type){ super(type); }

    String render(T ageLimitedOrderItem){
        return String.format("%s (age limit: %d)",
                             super.render(ageLimitedOrderItem),
                             ageLimitedOrderItem.getMinimumAge());
    }
}

Now, we need some kind of registry where we associate renderers and the types they render:

class RendererRegistry {

    private final Map<Class<?>, Renderer<?>> renderers = new HashMap<Class<?>, Renderer<?>>();

    <T extends OrderItem> void register(Class<? extends T> componentType, Renderer<T> renderer) {
        renderers.put(componentType, renderer);
    }

    String renderItem(OrderItem item) {
        Renderer<?> renderer = renderers.get(item.getClass());
        return doRender(renderer, item);
    }

    private <T extends OrderItem> String doRender(Renderer<T> renderer, OrderItem item) {
        return renderer.render(renderer.getType().cast(item));
    }

}

Now, given some domain classes...

class Film extends OrderItem implements AgeLimited {
    public int getMinimumAge(){return 0;}
}
class AdultFilm extends Film {
    public int getMinimumAge(){return 18;}
}
/**
 * Non-alcoholic beer, doesn't need an age limit ;)
 */
class Beer extends OrderItem implements Countable {
    public int getCount(){return 6;}
}

We can go about creating a registry like this:

RendererRegistry registry = new RendererRegistry();
registry.register(OrderItem.class, new DefaultRenderer<OrderItem>(OrderItem.class));
Renderer<Film> filmRenderer = new AgeLimitedRenderer<Film>(Film.class);
registry.register(Film.class, filmRenderer);
registry.register(AdultFilm.class, filmRenderer);
registry.register(Beer.class, new CountableRenderer<Beer>(Beer.class));
System.out.println(registry.renderItem(new OrderItem()));
System.out.println(registry.renderItem(new Beer()));
System.out.println(registry.renderItem(new Film()));
System.out.println(registry.renderItem(new AdultFilm()));

There's lots of improvements which could go here (like inferring suitable renderers for non-registered classes, declaring renderers with annotations on the domain classes, some decent error handling, and more), but this basic idea has worked well for me in the past. It's pretty flexible and as type-safe as it gets when dealing with Java generics, and it's pretty good at decoupling the item type/renderer associations from both the renderer and the domain objects.


Very interesting question indeed.

I do not have a straight answer to this one (probably there is none), but let me tell you how I would approach it.

I would consider using a table component which would adapt according to what it is showing (increase/decrease visible columns). I would have a renderer, editor & columns for each class which extends the StockItem, and a getter method to get them which would be part of the IStockItem interface. This way the renderer & editor classes would take responsibility for representation of an object for which they have been made.

The renderer and editor would take the responsibility of making sure the fields of a table are accordingly represented. Since we expect some columns will not apply for a particular subclass of stock item the renderer, the renderer & editor could assume that any field beyond what a class defines is blank and not editable.

This approach might be a bit of a pain, especially swapping the renderers in a table for each row, I think.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜