Работа с несколькими пользовательскими моделями таблиц без повторяющегося кода

Я работаю над проектом, в котором у нас есть несколько классов предметной области для моделирования бизнес-данных. Эти классы являются простыми POJO, и мне нужно отобразить несколько таблиц, используя их. Например, рассмотрим этот класс:

public class Customer {

    private Long id;
    private Date entryDate;
    private String name;
    private String address;
    private String phoneNumber;

    public Customer(Long id, Date entryDate, String name, String address, String phoneNumber) {
        this.id = id;
        this.entryDate = entryDate;
        this.nombre = name;
        this.domicilio = address;
        this.telefono = phoneNumber;
    }

    // Getters and setters here
}

Затем я создал свою собственную табличную модель, основанную на AbstractTableModel. для работы непосредственно с классом Customer:

public class CustomerTableModel extends AbstractTableModel {

    private final List<String> columnNames;
    private final List<Customer> customers;

    public CustomerTableModel() {
        String[] header = new String[] {
            "Entry date",
            "Name",
            "Address",
            "Phone number"
        };
        this.columnNames = Arrays.asList(header);
        this.customers = new ArrayList<>();
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return Date.class;
            case 1: return String.class;
            case 2: return String.class;
            case 3: return String.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Customer customer = getCustomer(rowIndex);
        switch (columnIndex) {
            case 0: return customer.getEntryDate();
            case 1: return customer.getName();
            case 2: return customer.getAddress();
            case 3: return customer.getPhoneNumber();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return true;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex < 0 || columnIndex >= getColumnCount()) {
            throw new ArrayIndexOutOfBoundsException(columnIndex);
        } else {
            Customer customer = getCustomer(rowIndex);
            switch (columnIndex) {
                case 0: customer.setEntryDate((Date)aValue); break;
                case 1: customer.setName((String)aValue); break;
                case 2: customer.setAddress((String)aValue); break;
                case 3: customer.setPhoneNumber((String)aValue); break;
            }
            fireTableCellUpdated(rowIndex, columnIndex);
        }
    }

    @Override
    public int getRowCount() {
        return this.customers.size();
    }

    @Override
    public int getColumnCount() {
        return this.columnNames.size();
    }

    @Override
    public String getColumnName(int columnIndex) {
        return this.columnNames.get(columnIndex);
    }

    public void setColumnNames(List<String> columnNames) {
        if (columnNames != null) {
            this.columnNames.clear();
            this.columnNames.addAll(columnNames);
            fireTableStructureChanged();
        }
    }

    public List<String> getColumnNames() {
        return Collections.unmodifiableList(this.columnNames);
    }

    public void addCustomer(Customer customer) {
        int rowIndex = this.customers.size();
        this.customers.add(customer);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void addCustomers(List<Customer> customerList) {
        if (!customerList.isEmpty()) {
            int firstRow = this.customers.size();
            this.customers.addAll(customerList);
            int lastRow = this.customers.size() - 1;
            fireTableRowsInserted(firstRow, lastRow);
        }
    }

    public void insertCustomer(Customer customer, int rowIndex) {
        this.customers.add(rowIndex, customer);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void deleteCustomer(int rowIndex) {
        if (this.customers.remove(this.customers.get(rowIndex))) {
            fireTableRowsDeleted(rowIndex, rowIndex);
        }
    }

    public Customer getCustomer(int rowIndex) {
        return this.customers.get(rowIndex);
    }

    public List<Customer> getCustomers() {
        return Collections.unmodifiableList(this.customers);
    }

    public void clearTableModelData() {
        if (!this.customers.isEmpty()) {
            int lastRow = customers.size() - 1;
            this.customers.clear();
            fireTableRowsDeleted(0, lastRow);
        }
    }
}

До сих пор все просто отлично. Однако у этого подхода есть как минимум две проблемы:

  1. Поскольку мне нужно реализовать одну табличную модель для каждого класса, я буду генерировать много повторяющегося кода, чтобы, по сути, сделать три вещи: определить соответствующий заголовок таблицы, добавить/удалить объекты в/из базовой структуры (списка), переопределить оба setValueAt() и getValueAt() методов для работы с пользовательскими объектами.

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

Вопрос. Есть ли способ избавиться от шаблонного кода, сделав мою табличную модель гибкой и пригодной для повторного использования?


person dic19    schedule 21.10.2014    source источник


Ответы (2)


Как и другие модели Swing (например: DefaultComboBoxModel, DefaultListModel), мы можем использовать Generics для создания гибкой и многоразовой табличной модели, а также предоставляет API для работы с пользовательскими POJO.

Эта модель стола будет иметь следующие особенности:

  • Он расширяется от AbstractTableModel, чтобы использовать преимущества обработки событий табличной модели.
  • В отличие от CustomerTableModel, показанной выше, эта табличная модель должна быть абстрактной, потому что она не должна переопределять метод getValueAt(): просто потому, что мы не знаем, какой класс или тип данных будет обрабатывать эта табличная модель, задача переопределения вышеупомянутого метода остается за подклассами. .
  • Он наследует пустую реализацию setValueAt() от AbstractTableModel. Это имеет смысл, потому что isCellEditable() также наследуется от этого класса и всегда возвращает false.
  • Реализация по умолчанию getColumnClass() также наследуется и всегда возвращает Object.class.

Эти функции делают эту модель таблицы действительно простой в реализации в зависимости от наших требований:

  • Если нам нужно отобразить таблицу только для чтения, то мы должны переопределить 2 метода top: getValueAt() и getColumnClass() (последний рекомендуется, но не обязателен).
  • Если наша таблица должна быть редактируемой, то мы должны переопределить 4 метода сверху: два упомянутых выше плюс isCellEditable() и setValueAt().

Давайте посмотрим на код нашей модели таблицы:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.table.AbstractTableModel;

/**
 * Abstract base class which extends from {@code AbstractTableModel} and 
 * provides an API to work with user-defined POJO's as table rows. Subclasses 
 * extending from {@code DataObjectTableModel} must implement 
 * {@code getValueAt(row, column)} method. 
 * <p />
 * By default cells are not editable. If those have to be editable then 
 * subclasses must override both {@code isCellEditable(row, column)} and 
 * {@code setValueAt(row, column)} methods.
 * <p />
 * Finally, it is not mandatory but highly recommended to override 
 * {@code getColumnClass(column)} method, in order to return the appropriate 
 * column class: default implementation always returns {@code Object.class}.
 * 
 * @param <T> The class handled by this TableModel.
 * @author dic19
 */
public abstract class DataObjectTableModel<T> extends AbstractTableModel {

    private final List<String> columnNames;
    private final List<T> data;

    public DataObjectTableModel() {
        this.data = new ArrayList<>();
        this.columnNames = new ArrayList<>();
    }

    public DataObjectTableModel(List<String> columnIdentifiers) {
        this();
        if (columnIdentifiers != null) {
            this.columnNames.addAll(columnIdentifiers);
        }
    }

    @Override
    public int getRowCount() {
        return this.data.size();
    }

    @Override
    public int getColumnCount() {
        return this.columnNames.size();
    }

    @Override
    public String getColumnName(int columnIndex) {
        return this.columnNames.get(columnIndex);
    }

    public void setColumnNames(List<String> columnNames) {
        if (columnNames != null) {
            this.columnNames.clear();
            this.columnNames.addAll(columnNames);
            fireTableStructureChanged();
        }
    }

    public List<String> getColumnNames() {
        return Collections.unmodifiableList(this.columnNames);
    }

    public void addDataObject(T dataObject) {
        int rowIndex = this.data.size();
        this.data.add(dataObject);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void addDataObjects(List<T> dataObjects) {
        if (!dataObjects.isEmpty()) {
            int firstRow = data.size();
            this.data.addAll(dataObjects);
            int lastRow = data.size() - 1;
            fireTableRowsInserted(firstRow, lastRow);
        }
    }

    public void insertDataObject(T dataObject, int rowIndex) {
        this.data.add(rowIndex, dataObject);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void deleteDataObject(int rowIndex) {
        if (this.data.remove(this.data.get(rowIndex))) {
            fireTableRowsDeleted(rowIndex, rowIndex);
        }
    }

    public void notifyDataObjectUpdated(T domainObject) {
        T[] elements = (T[])data.toArray();
        for (int i = 0; i < elements.length; i++) {
            if(elements[i] == domainObject) {
                fireTableRowsUpdated(i, i);
            }
        }
    }

    public T getDataObject(int rowIndex) {
        return this.data.get(rowIndex);
    }

    public List<T> getDataObjects(int firstRow, int lastRow) {
        List<T> subList = this.data.subList(firstRow, lastRow);
        return Collections.unmodifiableList(subList);
    }

    public List<T> getDataObjects() {
        return Collections.unmodifiableList(this.data);
    }

    public void clearTableModelData() {
        if (!this.data.isEmpty()) {
            int lastRow = data.size() - 1;
            this.data.clear();
            fireTableRowsDeleted(0, lastRow);
        }
    }
}

Итак, взяв эту табличную модель и класс Customer, полная реализация будет выглядеть так:

String[] header = new String[] {"Entry date", "Name", "Address", "Phone number"};
DataObjectTableModel<Customer> model = new DataObjectTableModel<>(Arrays.asList(header)) {
    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return Date.class;
            case 1: return String.class;
            case 2: return String.class;
            case 3: return String.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Customer customer = getDataObject(rowIndex);
        switch (columnIndex) {
            case 0: return customer.getEntryDate();
            case 1: return customer.getName();
            case 2: return customer.getAddress();
            case 3: return customer.getPhoneNumber();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return true;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex < 0 || columnIndex >= getColumnCount()) {
            throw new ArrayIndexOutOfBoundsException(columnIndex);
        } else {
            Customer customer = getDataObject(rowIndex);
            switch (columnIndex) {
                case 0: customer.setEntryDate((Date)aValue); break;
                case 1: customer.setName((String)aValue); break;
                case 2: customer.setAddress((String)aValue); break;
                case 3: customer.setPhoneNumber((String)aValue); break;
            }
            fireTableCellUpdated(rowIndex, columnIndex);
        }
    }
};

Как мы видим, в нескольких строках кода (LOC ‹ 50) у нас есть полная реализация.


Работает ли это с объектами JPA?

Это так, пока у сущностей есть общедоступные геттеры и сеттеры. В отличие от реализаций JPA, эта табличная модель не работает с отражением, поэтому нам придется обращаться к свойствам объекта, используя открытый интерфейс класса для реализации методов getValueAt() и setValueAt().

Работает ли он с JDBC?

Нет, это не так. Нам пришлось бы обернуть наборы результатов в классы предметной области и использовать предлагаемый интерфейс класса, как упоминалось выше.

Работает ли он с классами Java по умолчанию?

Да, это так. Еще раз, используя предлагаемый интерфейс класса. Например, возьмем класс java.io.File, у нас может быть следующая реализация табличной модели:

String[] header = new String[] {
    "Name",
    "Full path",
    "Last modified",
    "Read",
    "Write",
    "Execute",
    "Hidden",
    "Directory"
};

DataObjectTableModel<File> model = new DataObjectTableModel<File>(Arrays.asList(header)) {
    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return String.class;
            case 1: return String.class;
            case 2: return Date.class;
            case 3: return Boolean.class;
            case 4: return Boolean.class;
            case 5: return Boolean.class;
            case 6: return Boolean.class;
            case 7: return Boolean.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        File file = getDataObject(rowIndex);
        switch (columnIndex) {
            case 0: return file.getName();
            case 1: return file.getAbsolutePath();
            case 2: return new Date(file.lastModified());
            case 3: return file.canRead();
            case 4: return file.canWrite();
            case 5: return file.canExecute();
            case 6: return file.isHidden();
            case 7: return file.isDirectory();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }
};
person dic19    schedule 21.10.2014

В соответствии с ответом dic19 вы можете использовать Модель таблицы строк, который также использует дженерики и предоставляет множество общих методов, позволяющих динамически обновлять TableModel.

Вам также потребуется реализовать пару методов, поскольку модель также является абстрактной. Код JButtonTableModel.java показывает, как это можно сделать.

Кроме того, если вы хотите по-настоящему пофантазировать, вы можете взглянуть на Bean Table Model (ссылка в блоге выше), которая расширяет RowTableModel. Эта модель использует отражение для построения TableModel, поэтому вам не нужно реализовывать пользовательскую модель.

person camickr    schedule 22.10.2014