Этой статьей начинается серия статей про рефакторинг [1] - изменение кода без изменения того, за чем этот код написали. Статья рассчитана на читателя, который наслышан про рефакторинг от более продвинутых напарников но сам никогда не использовал его или использовал но крайне редко; для тех, кто хотел бы копнуть вглубь и попрактиковаться; для тех, кто хотел бы добавить рефакторинг в арсенал инструментов "на каждый день". Автор основывается на двух довольно известных книгах [1, 2] с дополнением знаниями, полученными им в процессе практических экспериментов с рефакторингом. Чтобы заразить идеей потенциального читателя, предлагаю перед углублением в текст статьи посмотреть видеоролик, в котором Автор записал сеанс одного из своих рефаткорингов.
Мое знакомство с методом началось с замечания, что он должен помещаться на экран: «метод, не помещающийся на экран — плохой метод». На вопрос "почему?"я получил ответ "чтобы было удобно читателю". С тех пор прошло несколько лет. И теперь я не согласен с этим утверждением. Попробую в этой статье раскрыть этот вопрос.
Раньше я писал процедуры и функции. Процедура для меня была списком действий, которые должен был выполнить компьютер, чтобы "моя была довольна", а функция, к тому же, что-то возвращала. Список команд компьютеру часто был очень длинным. Иногда процедуры создавались для повторного использования кода, намного реже - для устранения дублирования.
Т.к. процедуроведение оттачивалось годами, то это хорошо отпечаталось на моем понимании ООП. Класс был просто удобным хранилищем для сходных процедур, которые я с гордостью называл методами. Редко, но некоторые из них все же использовали общие данные — поля класса. Позже пришло понимание инкапсуляции — класс не хранилище подобных процедур, а модель объекта реального мира. Методы начали рассматриваться немного иначе, но процедурный стиль не был искоренен окончательно - об этом свидетельствовала все еще большая величина методов.
Слава Байту, в моей жизни появился Рефакторинг. Он то и поставил все на свои места. Если оставлять мост между процедурным и ООП стилем, то метод — это процедура, делающая одно действие и обрабатывающая при этом данные своего объекта. Следом объявилось новое свойство методов - они выделяются не только с целью устранения дублирования [2]. Это было ценное открытие для меня. Моя эйфория по этому поводу была предметом многочисленных споров с сотрудниками во время парной разработки. «Зачем создавать метод который никогда не будет использован?!!» А затем, чтобы сделать нечто, что будет просто осуществлять одно действие и больше ничего. Если уж метод не будет востребован — сделай его приватным.
Цель этой статьи - продемонстрировать, что можно вытворять с большими методами делая процедурный код более объектно ориентированным с помощью Рефакторинга. На словах что-то объяснить сложно. В работе мне помогает парное программирование. Тут же на примере я попробую продемонстрировать базовый тип рефакторинга — выделение метода (Extract method). Я всегда ленился делать это руками, но когда я узнал, что в IDE (любой) присутствует подобная функция - я влюбился в этот тип рефакторинга. Итак, вначале был экстракт метод (Extract Method). Он же самый, как мне кажется, используемый — дорога в увлекательный мир ООП, шаблонов, рафакторинга и архитектуры.
Суть Extract Method — часть сложного метода сделать отдельным полноценным методом, после использовать делегацию (старый вызывает новый). Подобным образом мы устраняли дублирование в процедурах. Наш исходный метод (для простоты понимания выбирался довольно простой метод):
public void foo(List<String> answers) {
for (int index == 0; index < this.size; index ++) {
this.answer = answers.get(index);
if (this.answer == Answers.DEFAULT_ANSWER) {
this.count = this.count + SOME_CONSTANT;
} else {
this.count = this. count - SOME_CONSTANT;
}
this.saveAnswer();
}
}
Первый шаг - опишем что же метод делает на родном языке. «Проходясь по всем answers, выбирает и устанавливает каждый его элемент в this.answer, а потом сравнивает это свойство с чем-то. Основываясь на результате сравнения определяет - уменьшать или увеличивать значения this.count. После выполняет некое сохранение».
Второй шаг — выделим из описания все действия (все формы глаголов).
Проходясь по всем answers, выбирает и устанавливает каждый его элемент в this.answer, а потом сравнивает это свойство с чем-то. Основываясь на результате сравнения определяет - уменьшать или увеличивать значения this.count. После выполняет некое сохранение.
Итого: проходится, выбирает, устанавливает, сравнивает, основываясь на чем-то определяет, уменьшает, увеличивает и выполняет сохранение. 7 действий на 1 метод. Многовато, если вспомнить, что наша цель - не более одного действия/глагола на метод.
Третий шаг - отделить действия, которые, по твоему мнению, более подходят для исходного метода от остальных, которые можно считать низкоуровневыми. Для этих целей я пользуюсь так называемым (мной) уровнем абстракции. Термин возможно и неудачный, но привычный для меня. Его можно переопределить по собственному желанию. Смысл в том, насколько сложные данные обрабатываются методом и/или насколько сложны действия над этими данными. Например, сложение двух чисел немного проще, чем это же сложение зависимое от условия, а цикл вносит еще немного веса в сложность.
В большом методе, скорее всего, намешано логики с разными «уровнями абстракции»: от работы с примитивами до обработки сложных типов данных; от использования операции сложения до использования каких-то специфических алгоритмов. С этим нам предстоит разобраться. В методе должна остаться только самая высокоуровневая логика - чаще всего это получается сделать.
Возьмем исходный метод и оценим отдельные его составляющие:
public void foo(List<String> answers) {
for (int index == 0; index < this.size; index ++) {
this.answer = answers.get(index);
if (this.answer == Answers.DEFAULT_ANSWER) {
this.count = this.count + SOME_CONSTANT;
} else {
this.count = this. count - SOME_CONSTANT;
}
this.saveAnswer();
}
}
Тут красным цветом обозначена самая низкоуровневая логика (сравнивает, уменьшает, увеличивает), синим — самая высокоуровневая (проходится, выбирает), зеленый - где-то посреднике (устанавливает, сравнивает, основываясь на чем-то определяет, выполняет сохранение).
На практике чаще проще определить самую низкоуровневую операцию, но есть и исключения, когда легче выделить сразу все, что что не является самой высокоуровневой. В данном примере - можно выделить сразу тело цикла помечено красным и зеленым цветом, а можно и начать с красной низкоуровневой реализации.
Выделим низкоуровневый код:
private boolean isDefaultAnswer() {
return this.answer == Answers.DEFAULT_ANSWER;
}
priavte void increaseCounter() {
this.count = this.count + SOME_CONSTANT;
}
priavte void decreaseCounter() {
this.count = this.count – SOME_CONSTANT;
}
исходный метод немного преобразится:
public void foo(List<String> answers) {
for (int index == 0; index < this.size; index ++) {
this.answer = answers.get(index);
if (this.isDefaultAnswer()) {
this.increaseCounter();
} else {
this.decreaseCounter();
}
this.saveAnswer();
}
}
Напомню, что выделение делается автоматически. От нас требуется выделить код, нажать гарячую клавишу, ввести название метода и нажать Enter.
Хотя сложность метода так же как и раньше определяется 7ю глаголами, теперь мы не видим реализации низкоуровненой логики. Это улучшает понимабельность кода программистом, т.к. он не видит лишней информации (которая чаще всего его отвлекает).
Идем дальше и выделим условный блок, тем самым спрячем красно-зеленую реализацию:
private void changeCounter() {
if (this.isDefaultAnswer()) {
this.increaseCounter ();
} else {
this.decreaseCounter ();
}
}
public void foo(List<String> answers) {
for (int index == 0; index < this.size; index ++) {
this.answer = answers.get(index);
this.changeCounter();
this.saveAnswer();
}
}
Стоит отметить очень важное свойство метода — его Имя. Что есть имя метода? Это то, что скрывает реализацию. Это то, что отвечает на вопрос "что?". Реализация же — отвечает на вопрос "как?". Название метода — это и есть тот глагол, о котором мы говорили выше. Если удается создать красивое имя для метода содержащее один глагол, то это верный знак - метод выделябельный. В противном случае лучше оставить как есть.
Еще раз (это важно): название метода должно отвечать на вопрос ЧТО делает метод и содержать ОДИН глагол. Методы с названием типа doSomething1AndSomething2 – это не методы, а процедуры.
В нашем случае изменения счетчика и сохранение ответа это действия в разных плоскостях, и если удастся их объединить под красивым одноглагольным именем — супер! Мне пока не приходит ничего в голову, потому оставляем как есть.
В самом конце стоит пересмотреть имя для исходного метода foo - теперь эта задача решается программистом в разы легче.
public void processAnswers(List<String> answers) {
for (int index == 0; index < this.size; index ++) {
this.answer = answers.get(index);
this.changeCounter();
this.saveAnswer();
}
}
Кстати, изменения названия метода (Rename method) так же частый гость, и в ИДЕ, с большей долей вероятности, она так же автоматизирована.
В этом примере нам не приходилось делать никаких действий перед выделением - мы просто выделяли нужный блок в метод. Иногда бывает, что блок не готов к выделению. Чаще всего выделению мешает локальная переменная. О ней узнаем больше в статье «Локальные переменные — зло?». Забегая наперед скажу, что нам помогут такие звери как: встраивание локальной переменной, замена локальной переменной вызовом метода, расщепление локальной переменной, введение поясняющей переменной и некоторые другие [1].
Выделение метода — отправная точка к другим типам рефакторинга. В результате у нас образовалось некоторое количество новых методов, которым, возможно, не место в этом классе (как это определять и что с этим делать расскажу в «Где моя тачка, Чувак?»). Кроме того у нас остался исходный метод processAnswers. Его как раз и предлагаю еще поковырять.
Воспользуемся для установки значения поля this.answer его сеттер (если его нет, то создадим):
public void processAnswers(List<String> answers) {
for (int index == 0; index < this.size; index ++) {
this.setAnswer(answers.get(index));
this.changeCounter();
this.saveAnswer();
}
}
Т.к. цикл проходится по всем элементам списка, то воспользуемся его сокращенной версией (ведено в Java 1.5):
public void processAnswers(List<String> answers) {
for (String answer : answers) {
this.setAnswer(answer);
this.changeCounter();
this.saveAnswer();
}
}
Вот теперь намного лучше.
После таких обработок можно заметить, что результирующий метод не использует данных исходного класса непосредственно, а значит есть шанс, что он является либо его расширением, либо частью другого класса использующего этот. Он может быть и частью исходного класса, но это должна позволять его метафора (про метафоры напишу в статье «Безымянный класс»). Все же я бы пересмотрел интерфейс изменяемого класса на всякий случай.
Вот так вот мы расправились с довольно простым методом и поняли глубже как он работает.
Продолжение следует....
Список чтива:
1. Мартин Фаулер "Рефакторинг"
2. Стив Макконнелл "Совершенный код"