Какой подход к дизайну следует использовать при отображении списка разнородных элементов - PullRequest
7 голосов
/ 20 мая 2011

Давайте представим, что я хочу отобразить список товарных позиций в таблице (используя Java).Модель предметной области состоит из абстрактного базового класса StockItem, из которого происходят различные другие типы позиций.StockItem предоставляет минимальный интерфейс (getId () и getDescription ()), но помимо этого подклассы могут иметь значительные различия.

Если я ограничусь методами, определенными в StockItem, я не смогу представитьдостаточно подробно для пользователя, так что это означает, что некоторые столбцы будут ссылаться на поля, которые не применимы к некоторым строкам (например, физические товары являются исчисляемыми, и этот счет должен отображаться в таблице, тогда как элементы услуг, которые также должны появиться втаблицы, не считаются, в этом случае должно быть указано «N / A» или аналогичный).

Чтобы придерживаться примера "Countable", мне кажется, что есть несколько решений (но имейте в виду, что Countable не будет единственным задействованным интерфейсом).

  1. Сделать счетный интерфейс частью базового класса и заставить все быть Счетным, даже если это не так.Классы, для которых это бессмысленно, должны возвращать какое-то специальное значение, генерировать исключение или иным образом указывать, что они нарушают контракт StockItem.

  2. В моем итераторе используйте множество проверок instanceof и приведите их соответствующим образом.Если я введу новый подкласс StockItem или иным образом изменим дерево наследования, мне придется помнить об изменении этого кода.

Оба из них кажутся мне антипаттернами, и я быбыть заинтересованным услышать о любых более изящных подходах, которые могли бы быть приняты.Я подозреваю, что в этом нет волшебной палочки, но если другие языки имеют функции, облегчающие подобные вещи, мне было бы интересно их услышать (хотя только из общего интереса, я не буду повторно реализовывать эту систему в любое время).скоро :)

Спасибо, Фил

Ответы [ 5 ]

3 голосов
/ 20 мая 2011

Адаптер шаблон

В вашем классе StockItem добавьте метод:

/**
 * 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;
}

Это будет унаследовано всеми подклассами и позволит вызывающей стороне безопасно преобразовать тип. Не отчаивайтесь обобщениями, просто я хочу, чтобы этот метод возвращал экземпляр того же типа, что и параметр clazz.

Теперь ваш поставщик таблиц может реализовать что-то вроде этого:

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";
}
1 голос
/ 20 мая 2011

Я бы, вероятно, выбрал какую-то фабрику, которая может создавать средства визуализации / компоненты для определенных типов. Простой пример, давайте представим, что некоторые вещи исчисляются, а другие имеют возрастное ограничение:

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

Простой базовый рендер с реализацией по умолчанию выглядел бы так (притворяясь, что вы просто хотите визуализировать OrderItem s как строки; в реальном приложении вы, вероятно, вернете JComponent или что-то еще):

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();
    }
}

Немного многословно - спасибо, нерафинированные дженерики! - но продлить это легко:

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());
    }
}

Теперь нам нужен какой-то реестр, где мы связываем средства визуализации и типы, которые они отображают:

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

}

Теперь, учитывая некоторые классы домена ...

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;}
}

Мы можем создать реестр следующим образом:

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

Есть много улучшений, которые могли бы пойти сюда (например, вывести подходящие средства визуализации для незарегистрированных классов, объявить средства визуализации с аннотациями к классам домена, некоторую достойную обработку ошибок и т. Д.), Но эта основная идея хорошо сработала для меня в прошлое. Он довольно гибкий и такой же безопасный для типов, как и при работе с обобщениями Java, и он довольно хорош при отделении ассоциаций типа / рендерера элементов от объектов рендерера и домена.

1 голос
/ 20 мая 2011

В качестве действительно простого решения вы можете добавить метод:

/**
* 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.
}

Я допускаю, что это не самая элегантная версия из таблицы POV, но, по крайней мере, это метод, который имеет смысл для вашего объекта домена и не имеет побочных эффектов для других клиентов объекта домена.

Это может быть даже более ясно, если вы не применяете порядок сортировки, но это может оказаться очень полезным, когда вам нужно отобразить много элементов, потому что создание их отсортированным образом гарантирует, что они будут отображаться в таблице. можно сделать, просто пройдя через представление каждой пары один раз.

1 голос
/ 20 мая 2011

Я бы начал с класса модели таблицы, что-то вроде бина с одним полем для каждого столбца в вашей таблице. Экземпляр этого класса может быть легко отображен.

Тогда я бы представил интерфейс

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

и заставьте StockItem реализовать этот интерфейс. StockItem будет реализовывать поведение по умолчанию, все подклассы будут добавлять что-то к полям в табличной модели:

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;
   }
}
0 голосов
/ 20 мая 2011

Действительно очень интересный вопрос.

У меня нет прямого ответа на этот вопрос (вероятно, его нет), но позвольте мне рассказать вам, как бы я к нему подошел.

Я бы сказалРассмотрите возможность использования компонента таблицы, который будет адаптироваться в соответствии с тем, что он показывает (увеличить / уменьшить видимые столбцы).Я хотел бы иметь средство визуализации, редактор и столбцы для каждого класса, который расширяет StockItem, и метод getter для их получения, который будет частью интерфейса IStockItem.Таким образом, классы рендерера и редактора будут нести ответственность за представление объекта, для которого они были созданы.

Рендерер и редактор будут нести ответственность за то, чтобы поля таблицы были соответственно представлены.Поскольку мы ожидаем, что некоторые столбцы не будут применяться к определенному подклассу элемента товара, используемого для визуализации, средство визуализации и редактор могут предположить, что любое поле, выходящее за рамки определения класса, является пустым и недоступным для редактирования.

Этот подход может быть немногоя думаю о боли, особенно обменивать рендеры в таблице для каждого ряда.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...