Пример рефакторинга «извлечение метода» для кода обновления таблицы


Исходный код

public void updateTable() {

    /**
        * Удаляем лишние строки таблицы, оставшиейся от предыдущего списка
        */
      while (viewingData.getResult().getRowDataItems().size() + 1 < dataTable.getRowCount()) {
        dataTable.removeRow(dataTable.getRowCount()-1);
    }

    /**
        * Отображаем заголовки таблицы
        */
    int titleColumnIndex=0;
    String[] columnIds = newString[viewingData.getViewingColumns().size()];
    for (String viewingColumnId : viewingData.getViewingColumns().keySet()) {
        columnIds[titleColumnIndex] = viewingColumnId;
        ViewingColumnData viewingColumn= viewingData.getViewingColumns().get(viewingColumnId);

        HorizontalPanel titlePanel=new HorizontalPanel();
        titlePanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);

        Widget columnTitleButton;
        if (viewingColumn.isSortable()) {
            columnTitleButton = newButton(viewingColumn.getTitle());
            ((Button)columnTitleButton).addClickListener(newSortDataClickListener(viewingColumnId));
        } else {
            columnTitleButton = newLabel(viewingColumn.getTitle());
        }

        titlePanel.add(columnTitleButton);
        columnTitleButton.setStyleName(TABLE_STYLE);

        if (viewingData.getOrderColumnId() != null && viewingData.getOrderColumnId().equals( viewingColumnId) ) {
            Image orderDirectionImage;
            if (viewingData.getOrderDesc()) {
                orderDirectionImage = new Image(GWT.getModuleBaseURL() + UP_ARROW_IMG);
            } else {
                orderDirectionImage = new Image(GWT.getModuleBaseURL() + DOWN_ARROW_IMG);
            }
            titlePanel.add(orderDirectionImage);
        } else {
        }

        dataTable.setWidget(0, titleColumnIndex, titlePanel);

        FlexTable.FlexCellFormatter titlePanelFormatter = dataTable.getFlexCellFormatter();
        titlePanelFormatter.setStyleName(0, titleColumnIndex, "tableTitle");
        if (viewingColumn.getWidth() > 0) {
            titlePanelFormatter.setWidth(0, titleColumnIndex, viewingColumn.getWidth() + "%");
        }
        titlePanelFormatter.setHorizontalAlignment(0, titleColumnIndex, HasHorizontalAlignment.ALIGN_CENTER);

        titleColumnIndex++;
    }
    /**
        * Удаляем лишние столбцы в заголовке
        */
    intcolumnToRemove= dataTable.getCellCount(0) - viewingData.getViewingColumns().size();
    dataTable.removeCells(0, viewingData.getViewingColumns().size(), columnToRemove);

    /**
        * Добавляем чекбокс
        */
    addTitleCheckbox();

    /**
        * Отображаем значения раскрытых групп
        */
      if (viewingData.getGroupingFilterItems() != null) {
        for (intgroupingIndex=0; groupingIndex < viewingData.getGroupingFilterItems().size(); groupingIndex++ ) {
            String groupingFilterValue = viewingData.getGroupingFilterItems().get(groupingIndex);
            Label valueLabel = new Label(groupingFilterValue);
            Image img = new Image(LEFT_ARROW_IMG);
            HorizontalPanel valuePanel = new HorizontalPanel();
            valuePanel.add(img);
            valuePanel.add(valueLabel);
            valuePanel.setWidth("100%");
            valuePanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_LEFT);

            ClickListener hideGroupClickListener = new HideGroupClickListener(groupingIndex);
            valueLabel.addClickListener(hideGroupClickListener);
            img.addClickListener(hideGroupClickListener);

            String cellStyle;
            if (groupingIndex % 2 == 0) {
                cellStyle = "tableRow";
            } else {
                cellStyle = "tableRowOdd";
            }

            for (int column=0; column < viewingData.getViewingColumns().size(); column++) {
                String text=null;

                /**
                    *
                    */
                if (groupingIndex == viewingData.getGroupingFilterItems().size()-1 && viewingData.getFunctionData().containsKey(columnIds[column])) {
                    text = "(" + viewingData.getFunctionData().get(columnIds[column]) + ")";
                } else {

Как делать рефакторинг «извлечение метода»

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

Обычно фрагмент определяется тем, какие переменные в нём задействованы.

Удаление лишних строк из таблицы

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

    while (viewingData.getResult().getRowDataItems().size() + 1 < dataTable.getRowCount()) {
        dataTable.removeRow(dataTable.getRowCount()-1);
    }

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

public void shrinkRowsTo(maxRowsCount)
{
    while (dataTable.getRowCount() > maxRowsCount) {
        dataTable.removeRow(dataTable.getRowCount() - 1);
    }
}

Код метода максимально обобщённый, не привязанный ни к каким другим объектам. Задача — удалить лишние строки, а откуда берётся количество, сколько строк оставить — методу не важно.

Исходный фрагмент будет выглядеть теперь так:

    dataTable.shrinkRowsTo(viewingData.getResult().getRowDataItems().size() + 1);

Создание кнопки-заголовка

    Widget columnTitleButton;
    if (viewingColumn.isSortable()) {
        columnTitleButton = new Button(viewingColumn.getTitle());
        ((Button)columnTitleButton).addClickListener(newSortDataClickListener(viewingColumnId));
    } else {
        columnTitleButton = new Label(viewingColumn.getTitle());
    }

    titlePanel.add(columnTitleButton);
    columnTitleButton.setStyleName(TABLE_STYLE);

В этом коде заключено сразу два знания: как создать кнопку-заголовок, и куда потом её добавить.

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

public Widget createColumnTitleButton()
{
    Widget columnTitleButton;

    if (isSortable()) {
        columnTitleButton = newButton(getTitle());
        ((Button)columnTitleButton).addClickListener(newSortDataClickListener(getId()));
    } else {
        columnTitleButton = newLabel(getTitle());
    }

    columnTitleButton.setStyleName(TABLE_STYLE);

    return columnTitleButton;
}
    titlePanel.add(viewingColumn.createColumnTitleButton());

Создание кнопки с иконкой направления сортировки

    if (viewingData.getOrderColumnId() != null && viewingData.getOrderColumnId().equals( viewingColumnId) ) {
        Image orderDirectionImage;
        if (viewingData.getOrderDesc()) {
            orderDirectionImage = new Image(GWT.getModuleBaseURL() + UP_ARROW_IMG);
        } else {
            orderDirectionImage = new Image(GWT.getModuleBaseURL() + DOWN_ARROW_IMG);
        }
        titlePanel.add(orderDirectionImage);
    }

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

Первые два знания тесно связаны с экземпляром viewingData, поэтому перенесу их туда.

public boolean isSortedColumn(String columnId)
{
    return getOrderColumnId() != null && getOrderColumnId().equals(columnId);
}

public Image createDirectionImage()
{
    returnnewImage(GWT.getModuleBaseURL() + (getOrderDesc() ? UP_ARROW_IMG : DOWN_ARROW_IMG));
}

Заодно избавился от небольшого дублирования реализации при создании экземпляра иконки.

В результате исходный код рефакторится следующим образом:

    if (viewingData.isSortedColumn(viewingColumnId)) {
        titlePanel.add(viewingData.createDirectionImage());
    }

Создание форматтера

Следующий автономный фрагмент посвящён созданию некоего форматтера:

    FlexTable.FlexCellFormattertitlePanelFormatter= dataTable.getFlexCellFormatter();
    titlePanelFormatter.setStyleName(0, titleColumnIndex, "tableTitle");
    if (viewingColumn.getWidth() > 0) {
        titlePanelFormatter.setWidth(0, titleColumnIndex, viewingColumn.getWidth() + "%");
    }
    titlePanelFormatter.setHorizontalAlignment(0, titleColumnIndex, HasHorizontalAlignment.ALIGN_CENTER);

Хотя совершенно непонятно, как он цепляется к таблице. Предположу, что когда-то эта функциональность была нужна, а потом её частично выпилили и забыли.

При любом раскладе этот код надо извлечь в отдельный метод. Я бы отнёс этот код к экземпляру dataTable:

public FlexTable.FlexCellFormatter createFlexColumnFormatter(int titleColumnIndex, ViewingColumnData viewingColumn)
{
    FlexTable.FlexCellFormattertitlePanelFormatter= dataTable.getFlexCellFormatter();
    titlePanelFormatter.setStyleName(0, titleColumnIndex, "tableTitle");
    if (viewingColumn.getWidth() > 0) {
        titlePanelFormatter.setWidth(0, titleColumnIndex, viewingColumn.getWidth() + "%");
    }
    titlePanelFormatter.setHorizontalAlignment(0, titleColumnIndex, HasHorizontalAlignment.ALIGN_CENTER);
    return titlePanelFormatter;
}

Удаление лишних колонок из таблицы

int columnToRemove = dataTable.getCellCount(0) - viewingData.getViewingColumns().size();
    dataTable.removeCells(0, viewingData.getViewingColumns().size(), columnToRemove);

Смысл этого кода в том, чтобы оставить в заголовке dataTable столько колонок, сколько есть в viewingData.

Здесь тоже два знания: как удалять колонки и где взять количество, сколько штук оставить.

Так как действие касается экземпляра dataTable, есть резон создать в нём отдельный метод для укорачивания колонок:

public void shrinkTitleColumns(int shrinkTo)
{
    removeCells(0, shrinkTo, getCellCount(0) - shrinkTo);
}

Как и в прошлый раз со строками, метод максимально обобщённый, работает просто с количеством колонок, независимо от того, откуда оно взялось.

Исходный примет следующий вид:

    dataTable.shrinkTitleColumns(viewingData.getViewingColumns().size());

Теперь здесь осталось только одно знание: где взять нужное количество колонок; о подробностях собственно удаления знает только метод shrinkTitleColumns.

Создание полей значений

Дальше идёт довольно переплетённый фрагмент кода:

    String groupingFilterValue= viewingData.getGroupingFilterItems().get(groupingIndex);
    LabelvalueLabel=newLabel(groupingFilterValue);
    Imageimg=newImage(LEFT_ARROW_IMG);
    HorizontalPanelvaluePanel=newHorizontalPanel();
    valuePanel.add(img);
    valuePanel.add(valueLabel);
    valuePanel.setWidth("100%");
    valuePanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_LEFT);

    ClickListenerhideGroupClickListener=newHideGroupClickListener(groupingIndex);
    valueLabel.addClickListener(hideGroupClickListener);
    img.addClickListener(hideGroupClickListener);

Его апофеозом является создание экземпляра valuePanel, хотя последовательность действий неочевидна. Поскольку всё начинается с viewingData, пусть извлечённый метод относится к нему. Заодно попробую немного переупорядочить строки так, чтобы стали лучше видны взаимозависимости между объектами.

public HorizontalPanel createValuePanel(int groupingIndex)
{
    HorizontalPanel valuePanel = new HorizontalPanel();

    ClickListener hideGroupClickListener = new HideGroupClickListener(groupingIndex);

    String groupingFilterValue = getGroupingFilterItems().get(groupingIndex);
    Label valueLabel = newLabel(groupingFilterValue);
    valueLabel.addClickListener(hideGroupClickListener);
    valuePanel.add(valueLabel);

    Image img=newImage(LEFT_ARROW_IMG);
    img.addClickListener(hideGroupClickListener);
    valuePanel.add(img);

    valuePanel.setWidth("100%");
    valuePanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_LEFT);

    return valuePanel;
}

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

В этом фрагменте по-прежнему содержатся много знаний: как формировать метку, как добавлять метку, как формировать иконку, как добавлять иконку, как форматировать виджет — но критической необходимости разделять эти знания пока нет.

Как будет выглядеть отрефакторенный код

public void updateTable() {
    dataTable.shrinkRowsTo(viewingData.getResult().getRowDataItems().size() + 1);

    int titleColumnIndex=0;
    String[] columnIds = newString[viewingData.getViewingColumns().size()];

    for (String viewingColumnId : viewingData.getViewingColumns().keySet()) {
        columnIds[titleColumnIndex] = viewingColumnId;
        ViewingColumnData viewingColumn= viewingData.getViewingColumns().get(viewingColumnId);

        HorizontalPanel titlePanel = new HorizontalPanel();
        titlePanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);

        titlePanel.add(viewingColumn.createColumnTitleButton());

        if (viewingData.isSortedColumn(viewingColumnId)) {
            titlePanel.add(viewingData.createDirectionImage());
        }

        dataTable.setWidget(0, titleColumnIndex, titlePanel);

        dataTable.createFlexColumnFormatter();

        titleColumnIndex++;
    }

    dataTable.shrinkTitleColumns(viewingData.getViewingColumns().size());

    addTitleCheckbox();

    if (viewingData.getGroupingFilterItems() != null) {
        for (intgroupingIndex=0; groupingIndex < viewingData.getGroupingFilterItems().size(); groupingIndex++ ) {
            HorizontalPanelvaluePanel= viewingData.createValuePanel(groupingIndex);

            // ...

Теперь можно сделать ещё несколько итераций рефакторинга, извлекая в новые методы код для формирования titlePanel, всего набора колонок, фильтров и т. п.

Теория