Статьи

Рефакторинг унаследованного кода: часть 7 — определение уровня представления

Старый код Гадкий код. Сложный код. Код спагетти. Глупый бред. В двух словах, Кодекс Наследия . Это серия, которая поможет вам работать и справляться с этим.

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

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

SRP — это один из принципов SOLID, о котором мы подробно говорили в предыдущем уроке: SOLID: Часть 1 — Принцип единой ответственности . Если вы хотите углубиться в детали, я рекомендую вам прочитать статью, в противном случае просто продолжайте читать и ознакомьтесь с кратким изложением принципа единой ответственности ниже.

SRP в основном говорит, что любой модуль, класс или метод должен нести одну ответственность. Такая ответственность определяется как оси изменений. Ось изменения — это направление, причина изменения. Таким образом, SRP означает, что у нашего класса должна быть одна причина для изменения.

Хотя это звучит довольно просто, как вы определяете «причину перемен»? Мы должны думать об этом с точки зрения пользователей нашего кода, как обычных конечных пользователей, так и различных отделов программного обеспечения. Эти пользователи могут быть представлены как актеры. Когда актер хочет, чтобы мы изменили наш код, это причина изменения, которая определяет ось изменения. Такой запрос должен по возможности затрагивать только один из наших модулей, классов или даже методов.

Одним очень очевидным примером может быть, если наша команда разработчиков пользовательского интерфейса потребует от нас предоставить всю информацию, которая должна быть представлена ​​таким образом, чтобы наше приложение могло доставляться через веб-страницу HTML, а не через наш текущий интерфейс командной строки.

Поскольку наш код стоит сегодня, мы могли бы просто отправить весь текст какому-нибудь внешнему смарт-объекту, который преобразовал бы его в HTML. Но это может работать только потому, что HTML в основном основан на тексте. Что, если наша команда по пользовательскому интерфейсу захочет представить нашу викторину как настольный интерфейс с окнами, кнопками и различными таблицами?

Что если наши пользователи захотят увидеть игру на виртуальной игровой доске в виде города с улицами, а игроков — как людей, гуляющих вокруг квартала?

Мы могли бы идентифицировать этих людей как UI Actor. И мы должны понимать, что, поскольку наш код стоит сегодня, нам нужно изменить наш класс пустяков и почти все его методы. wasCorrectlyAnswered() ли изменять метод wasCorrectlyAnswered() из класса Game если я хочу исправить опечатку на экране в тексте или если я хочу представить нашу программу для викторин в виде виртуальной игровой доски? Нет. Ответ абсолютно нет.

Чистая архитектура — это концепция, продвигаемая в основном Робертом К. Мартином. По сути, это говорит о том, что наша бизнес-логика должна быть четко определена и четко отделена границами от других модулей, не связанных с основной функциональностью нашей системы. Это приводит к разъединенному и хорошо тестируемому коду.

Возможно, вы видели этот рисунок во всех моих уроках и курсах. Я считаю это настолько важным, что я никогда не пишу код и не говорю о нем, не думая об этом. Это полностью изменило способ написания кода в Syneto и внешний вид нашего проекта. Раньше у нас был весь наш код в среде MVC с бизнес-логикой в ​​моделях. Это было сложно понять и проверить. Плюс бизнес-логика была полностью связана с этой конкретной платформой MVC. Хотя это может работать с небольшими домашними проектами, когда речь идет о большом проекте, от которого зависит будущее компании, включая всех ее сотрудников, вы должны перестать играть с инфраструктурами MVC, и вы должны начать думать о как организовать свой код. Как только вы сделаете это и сделаете все правильно, вы никогда не захотите возвращаться к тем способам, которыми вы проектировали свои проекты ранее.

Мы уже начали отделять нашу бизнес-логику от презентации в нескольких предыдущих уроках. Иногда мы наблюдали некоторые функции печати и извлекали их в частные методы в том же классе Game . Это было наше бессознательное, говорящее нам, чтобы вытолкнуть представление из бизнес-логики на уровне метода.

Теперь пришло время проанализировать и наблюдать.

Это список всех переменных, методов и функций из нашего файла Game.php . Вещи, помеченные оранжевым «f», являются переменными. Красный «м» означает метод. Если за ним следует зеленый замок, это общедоступный. За ним следует красная блокировка приватно. И из этого списка все, что нас интересует, это следующая часть.

Все выбранные методы имеют что-то общее. Все их имена начинаются с «показа» … чего-то. Это все методы, связанные с печатью вещей на экране. Все они были определены нами в предыдущих уроках и легко извлечены, по одному за раз. Теперь мы должны заметить, что они представляют собой группу методов, которые принадлежат друг другу. Группа, которая делает одну конкретную вещь, выполняет одну единственную ответственность, отображает информацию на экране.

Лучше всего проиллюстрировано и объяснено в разделе «Рефакторинг — улучшение дизайна существующего кода » Мартина Фаулера. Основная идея рефакторинга «Извлечь класс» заключается в том, что после того, как вы поймете, что ваш класс работает и что это должно быть сделано двумя классами, вы предпринимаете действия, чтобы два класса. В этом есть особая механика, как объясняется в приведенной ниже цитате из вышеупомянутой книги.

  • Решите, как разделить обязанности класса.
  • Создайте новый класс, чтобы выразить разделенные обязанности.
    • Если обязанности старого класса больше не соответствуют его имени, переименуйте старый класс.
  • Сделайте ссылку со старого на новый класс.
    • Вам может понадобиться двусторонняя ссылка. Но не делайте обратную ссылку, пока не найдете нужного.
  • Используйте Move Field на каждом поле, которое вы хотите переместить.
  • Компилируйте и тестируйте после каждого хода.
  • Используйте Move Method для перемещения методов из старого в новое. Начните с низкоуровневых методов (вызываемых, а не вызывающих) и переходите на более высокий уровень.
  • Компилируйте и тестируйте после каждого хода.
  • Просмотрите и уменьшите интерфейсы каждого класса.
    • Если у вас была двусторонняя ссылка, проверьте, можно ли сделать ее односторонней.
  • Решите, выставлять ли новый класс. Если вы действительно выставляете класс, решите, выставлять ли его как ссылочный объект или как объект неизменяемого значения.

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

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

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

Наше первое действие — создать новый пустой класс.

1
2
3
class Display {
 
}

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

Если вы предпочитаете, и ваш язык программирования поддерживает это, PHP поддерживает, вы можете создать новый класс в том же файле, что и старый. Или вы можете создать новый файл для него с самого начала. Я лично не нашел окончательной причины идти тем или иным путем или запретить любой из этих двух путей. Так что решать вам. Просто решите и продолжайте.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require_once __DIR__ .
 
function echoln($string) {
    echo $string .
}
 
class Game {
    static $minimumNumberOfPlayers = 2;
    static $numberOfCoinsToWin = 6;
 
    private $display;
 
    // … //
 
    function __construct() {
 
        //…//
 
        $this->display = new Display();
    }
 
    // … all the other methods … //
}

Просто. Не так ли? В конструкторе Game мы только что инициализировали переменную закрытого класса, которую назвали так же, как новый класс display . Нам также нужно было включить файл Display.php в наш файл Game.php . У нас еще нет автозагрузчика. Возможно, в будущем уроке мы представим его, если необходимо.

И, как обычно, не забудьте запустить свои тесты. На этом этапе достаточно юнит-тестов, чтобы убедиться в отсутствии опечаток во вновь добавленном коде.

Давайте сделаем эти два шага одновременно. Какие поля мы можем определить, которые должны перейти от Game к Display ?

Просто глядя на список …

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
static $minimumNumberOfPlayers = 2;
static $numberOfCoinsToWin = 6;
 
private $display;
 
var $players;
var $places;
var $purses;
var $inPenaltyBox;
 
var $popQuestions;
var $scienceQuestions;
var $sportsQuestions;
var $rockQuestions;
 
var $currentPlayer = 0;
var $isGettingOutOfPenaltyBox;

… мы не можем найти переменную / поле, которое должно принадлежать Display . Может быть, некоторые появятся вовремя. Так что нечего делать для этого шага. А насчет тестов мы уже запускали их минуту назад. Время двигаться дальше.

Это само по себе еще один рефакторинг. Вы можете сделать это несколькими способами, и вы найдете хорошее определение этого в той же книге, о которой мы говорили ранее.

Как уже упоминалось выше, мы должны начать с самого нижнего уровня методов. Те, которые не вызывают другие методы. Вместо этого они называются.

1
2
3
private function displayPlayersNewLocation() {
    echoln($this->players[$this->currentPlayer] . «‘s new location is » . $this->places[$this->currentPlayer]);
}

displayPlayersNewLocation() кажется, хороший кандидат. Давайте проанализируем, что он делает.

Мы видим, что он не вызывает другие методы в Game . Вместо этого он использует три поля: players , currentPlayer и places . Они могут превратиться в два или три параметра. Пока все довольно мило. Но как насчет echoln() , единственного вызова функции в нашем методе? Откуда этот echoln() ?

Он находится вверху нашего файла Game.php , вне самого класса Game .

1
2
3
function echoln($string) {
    echo $string .
}

Это определенно делает то, что говорит. Повторяет строку с новой строкой в ​​конце. И это чистая презентация. Это должно войти в класс Display . Итак, давайте скопируем это туда.

1
2
3
4
5
6
7
class Display {
 
    function echoln($string) {
        echo $string .
    }
 
}

Запустите наши тесты снова. Мы можем оставить золотой мастер отключенным до тех пор, пока не закончим извлечение всей презентации в новый класс Display . В любое время, если вы чувствуете, что выходные данные могли быть изменены, также повторно запустите золотые мастер-тесты. На этом этапе тесты подтвердят, что мы не вводили опечатки, дублирующиеся объявления функций или какие-либо другие ошибки в этом отношении, копируя функцию на новое место.

Теперь перейдите и удалите echoln() из файла Game.php , запустите наши тесты и ожидайте, что они не пройдут.

1
PHP Fatal error: Call to undefined function echoln() in /…/Game.php on line 55

Ницца! Наш модульный тест здесь очень помогает. Он работает очень быстро и говорит нам точное положение проблемы. Мы идем к линии 55.

Смотри! Там есть echoln() . Тесты никогда не лгут. Давайте исправим это, вызвав $this->dipslay->echoln() .

1
2
3
4
5
6
7
8
function add($playerName) {
    array_push($this->players, $playerName);
    $this->setDefaultPlayerParametersFor($this->howManyPlayers());
 
    $this->display->echoln($playerName . » was added»);
    echoln(«They are player number » . count($this->players));
    return true;
}

Это заставляет тест проходить по линии 55 и не выполнять 56.

1
PHP Fatal error: Call to undefined function echoln() in /…/Game.php on line 56

И решение очевидно. Это утомительный процесс, но это по крайней мере легко.

1
2
3
4
5
6
7
8
function add($playerName) {
    array_push($this->players, $playerName);
    $this->setDefaultPlayerParametersFor($this->howManyPlayers());
 
    $this->display->echoln($playerName . » was added»);
    $this->display->echoln(«They are player number » . count($this->players));
    return true;
}

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

1
PHP Fatal error: Call to undefined function echoln() in /…/Game.php on line 169

Это в wrongAnswer() .

01
02
03
04
05
06
07
08
09
10
11
function wrongAnswer() {
    echoln(«Question was incorrectly answered»);
    echoln($this->players[$this->currentPlayer] . » was sent to the penalty box»);
    $this->inPenaltyBox[$this->currentPlayer] = true;
 
    $this->currentPlayer++;
    if ($this->shouldResetCurrentPlayer()) {
        $this->currentPlayer = 0;
    }
    return true;
}

Исправление этих двух вызовов переводит нашу ошибку в строку 228.

1
2
3
private function displayCurrentPlayer() {
    echoln($this->players[$this->currentPlayer] . » is the current player»);
}

Метод display ! Может быть, это должен быть наш первый способ двигаться. Мы пытаемся сделать небольшую тестовую разработку (TDD) здесь. А когда тесты не пройдены, нам не разрешается писать больше производственного кода, который не является абсолютно необходимым для прохождения теста. И все, что влечет за собой, это просто изменять echoln() пока все наши модульные тесты не пройдут.

Вы можете ускорить этот процесс, используя функции поиска и замены вашей IDE или редактора. Просто запустите все тесты, включая золотого мастера, после того, как вы закончите с этой заменой. Наши модульные тесты охватывают не весь код, а все echoln() .

Мы можем начать с первого кандидата, displayCurrentPlayer() . Скопируйте его в Display и запустите ваши тесты.

Затем сделайте его общедоступным на Display и в displayCurrentPlayer() при вызове Game $this->display->displayCurrentPlayer() вместо непосредственного выполнения echoln() . Наконец, запустите ваши тесты.

Они потерпят неудачу. Но, сделав изменения таким образом, мы убедились, что изменили только одну вещь, которая может потерпеть неудачу. Все другие методы все еще вызывают Game ‘s displayCurrentPlayer() . И это тот, кто делегирует на Display .

1
Undefined property: Display::$display

Наш метод использует поля класса. Это должны быть сделаны параметры для функции. Если вы следите за своими ошибками теста, вы должны получить что-то подобное в Game .

1
2
3
private function displayCurrentPlayer() {
    $this->display->displayCurrentPlayer($this->players[$this->currentPlayer]);
}

И это в Display .

1
2
3
function displayCurrentPlayer($currentPlayer) {
    $this->echoln($currentPlayer . » is the current player»);
}

Замените вызовы в Game на локальный метод на метод в Display . Не забудьте также о перемещении параметров на один уровень вверх.

1
2
3
4
private function displayStatusAfterRoll($rolledNumber) {
    $this->display->displayCurrentPlayer($this->players[$this->currentPlayer]);
    $this->displayRolledNumber($rolledNumber);
}

Наконец, удалите неиспользуемый метод из Game . И запустите свои тесты, чтобы убедиться, что все в порядке.

Это утомительный процесс. Вы можете немного ускорить его, приняв сразу несколько методов и используя все, что может сделать ваша IDE, для перемещения и замены кода между классами. Остальные методы останутся для вас упражнением, или вы можете прочитать больше в этой главе с основными моментами процесса. Готовый код, прилагаемый к этой статье, будет содержать полный класс Display .

Ах, и не забывайте о коде, который еще не извлечен в методах «отображения» внутри Game . Вы можете переместить эти echoln() для отображения напрямую. Наша цель — вообще не вызывать echoln() из Game , а сделать его приватным на Display .

Примерно через полчаса работы Display начинает выглядеть красиво.

Все методы отображения из Game находятся в Display . Теперь мы можем искать все оставшиеся в Game вызовы echoln и перемещать их тоже. Тесты проходят, конечно.

Но как только мы сталкиваемся с askQuestion() , мы понимаем, что это также просто код представления. А это значит, что различные массивы вопросов также должны идти в Display .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class Display {
    private $popQuestions = [];
    private $scienceQuestions = [];
    private $sportsQuestions = [];
    private $rockQuestions = [];
 
    function __construct() {
        $this->initializeQuestions();
    }
    // … //
    private function initializeQuestions() {
        $categorySize = 50;
        for ($i = 0; $i < $categorySize; $i++) {
            array_push($this->popQuestions, «Pop Question » . $i);
            array_push($this->scienceQuestions, («Science Question » . $i));
            array_push($this->sportsQuestions, («Sports Question » . $i));
            array_push($this->rockQuestions, «Rock Question » . $i);
        }
    }
}

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

После извлечения следующих двух методов мы понимаем, что было бы лучше назвать их внутри класса Display без префикса «display».

1
2
3
4
5
6
7
function correctAnswer() {
    $this->echoln(«Answer was correct!!!!»);
}
 
function playerCoins($currentPlayer, $playerCoins) {
    $this->echoln($currentPlayer . » now has » . $playerCoins . » Gold Coins.»);
}

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

Посмотрите внимательно на выбранную строку, 119. Это похоже на наш недавно извлеченный метод в Display .

1
2
3
function correctAnswer() {
    $this->echoln(«Answer was correct!!!!»);
}

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

Остальная часть метода не представляет особой проблемы.

Теперь, когда все функции презентации представлены в Display , мы должны пересмотреть методы и публиковать только те, которые используются в Game . Этот шаг также мотивирован принципом разделения интерфейсов, о котором мы говорили в предыдущем уроке.

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

Поскольку золотые мастер-тесты работают медленно, мы также можем положиться на нашу IDE, которая поможет нам ускорить процесс. PHPStorm достаточно умен, чтобы выяснить, не используется ли метод. Если мы сделаем метод закрытым, и он внезапно станет неиспользуемым, станет ясно, что он использовался вне Display и должен оставаться открытым.

Наконец, мы можем реорганизовать Display так, чтобы приватные методы были в конце класса.

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