Статьи

Рефакторинг Legacy Code: Часть 1 — Золотой Мастер

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

В идеальном мире вы бы написали только новый код. Вы бы написали это красиво и идеально. Вам никогда не придется пересматривать свой код, и вам никогда не придется поддерживать проекты десять лет. В идеальном мире …

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

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

Для меня устаревший код — это просто код без тестов. ~ Майкл Перья

Что ж, это первое формальное определение устаревшего кода выражения, опубликованное Майклом Фезерсом в его книге « Эффективная работа с устаревшим кодом» . Конечно, индустрия использовала это выражение целую вечность, в основном для любого кода, который трудно изменить. Однако это определение может сказать что-то другое. Это объясняет проблему очень ясно, так что решение становится очевидным. «Трудно изменить» настолько расплывчато. Что мы должны сделать, чтобы его было легко изменить? Мы понятия не имеем! «Код без тестов», с другой стороны, очень конкретен. И ответ на наш предыдущий вопрос прост: сделайте код тестируемым и протестируйте его. Итак, начнем.

Эта серия будет основана на исключительной игре викторины от JB Rainsberger, разработанной для событий Legacy Code Retreat . Он сделан для того, чтобы быть похожим на настоящий унаследованный код, а также предлагать возможности для широкого спектра рефакторинга на достойном уровне сложности.

Игра Trivia размещена на GitHub и имеет лицензию GPLv3, поэтому вы можете свободно играть с ней. Мы начнем эту серию с проверки официального репозитория. Код также прилагается к этому руководству со всеми изменениями, которые мы сделаем, поэтому, если вы в какой-то момент запутаетесь, вы можете взглянуть на конечный результат.

1
$ git clone https://github.com/jbrains/trivia.git Cloning into ‘trivia’… remote: Counting objects: 429, done.

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

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

У нас есть два файла в нашем каталоге.

1
2
3
4
5
6
7
$ cd php/
$ ls -al
total 20
drwxr-xr-x 2 csaba csaba 4096 Mar 10 21:05 .
drwxr-xr-x 26 csaba csaba 4096 Mar 10 21:05 ..
-rw-r—r— 1 csaba csaba 5568 Mar 10 21:05 Game.php
-rw-r—r— 1 csaba csaba 410 Mar 10 21:05 GameRunner.php

GameRunner.php является хорошим кандидатом для нашей попытки запустить код.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ php ./GameRunner.php
Chet was added
They are player number 1
Pat was added
They are player number 2
Sue was added
They are player number 3
Chet is the current player
They have rolled a 4
Chet’s new location is 4
The category is Pop
Pop Question 0
Answer was corrent!!!!
Chet now has 1 Gold Coins.
Pat is the current player
They have rolled a 2
Pat’s new location is 2
The category is Sports
Sports Question 0
Answer was corrent!!!!
Pat now has 1 Gold Coins.
Sue is the current player
They have rolled a 1
Sue’s new location is 1
The category is Science
Science Question 0
Answer was corrent!!!!
Sue now has 1 Gold Coins.
Chet is the current player
They have rolled a 4
 
## Some lines removed to keep
## the tutorial at a reasonable size
 
Answer was corrent!!!!
Sue now has 5 Gold Coins.
Chet is the current player
They have rolled a 3
Chet is getting out of the penalty box
Chet’s new location is 11
The category is Rock
Rock Question 5
Answer was correct!!!!
Chet now has 5 Gold Coins.
Pat is the current player
They have rolled a 1
Pat’s new location is 10
The category is Sports
Sports Question 1
Answer was corrent!!!!
Pat now has 6 Gold Coins.

OK. Наше предположение было верным. Наш код работал и выдавал некоторые результаты. Анализ этих выводов поможет нам вывести некоторую базовую идею о том, что делает код.

  1. Мы знаем, что это мелочи игры. Мы знали это, когда проверяли исходный код.
  2. В нашем примере три игрока: Чет, Пэт и Сью.
  3. Есть какая-то игра в кости или похожая концепция.
  4. Есть текущее местоположение для игрока. Возможно на какой-то доске?
  5. Существуют различные категории, из которых задаются вопросы.
  6. Пользователи отвечают на вопросы.
  7. Правильные ответы дают игрокам золото.
  8. Неправильные ответы отправляют игроков в штрафную.
  9. Игроки могут выбраться из штрафной, исходя из не совсем понятной логики.
  10. Кажется, что пользователь, который первым достигает шести золотых монет, выигрывает.

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

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

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

… станет таким:

… что несколько лучше. Возможно, разница с этим небольшим количеством кода не будет большой, но это будет в нашем следующем файле.

Глядя на наш файл GameRunner.php , мы можем легко определить некоторые ключевые аспекты, которые мы наблюдали в выводе. Мы видим строки, которые добавляют пользователей (9-11), что вызывается метод roll () и выбирается победитель. Конечно, это далеко не внутренние секреты логики игры, но, по крайней мере, мы могли бы начать с определения ключевых методов, которые помогут нам обнаружить остальную часть кода.

Мы должны сделать то же самое форматирование и Game.php файла Game.php .

Этот файл намного больше; Около 200 строк кода. Большинство методов имеют соответствующий размер, но некоторые из них достаточно велики, и после форматирования мы видим, что в двух местах отступ кода выходит за пределы четырех уровней. Высокий уровень отступов обычно означает множество сложных решений, поэтому сейчас мы можем предположить, что эти пункты в нашем коде будут более сложными и более разумными для изменения.

И мысль об изменениях приводит нас к нашему отсутствию испытаний. Методы, которые мы видели в Game.php , довольно сложны. Не волнуйтесь, если вы их не понимаете. На данный момент, они тоже загадка для меня. Устаревший код — это загадка, которую мы должны разгадать и понять. Мы сделали первый шаг, чтобы понять это, и теперь пришло время нашего второго.

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

Вместо того, чтобы пытаться выяснить, что тестировать, мы можем много раз тестировать все, так что в итоге мы получаем огромное количество продукции, о которой мы почти наверняка можем предположить, что она была получена путем использования всех частей нашего наследия. код. Рекомендуется запускать код как минимум 10 000 (десять тысяч) раз. Мы напишем тест, чтобы запустить его вдвое больше и сохранить результат.

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

Вы найдете в прилагаемом архиве кода, внутри папки с source но вне папки с trivia нашу папку Test . В этой папке мы создаем файл: GoldenMasterTest.php .

01
02
03
04
05
06
07
08
09
10
11
12
class GoldenMasterTest extends PHPUnit_Framework_TestCase {
 
    function testGenerateOutput() {
        ob_start();
        require_once __DIR__ .
        $output = ob_get_contents();
        ob_end_clean();
 
        var_dump($output);
    }
 
}

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

Код довольно прост, он буферизует вывод и помещает его в переменную $output . require_once() также запустит весь код внутри включенного файла. В нашем дампе мы увидим уже знакомый результат.

Однако во второй раз мы можем наблюдать что-то странное:

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

01
02
03
04
05
06
07
08
09
10
11
do {
 
    $aGame->roll(rand(0, 5) + 1);
 
    if (rand(0, 9) == 7) {
        $notAWinner = $aGame->wrongAnswer();
    } else {
        $notAWinner = $aGame->wasCorrectlyAnswered();
    }
 
} while ($notAWinner);

Анализируя основной код от бегуна, мы видим, что он использует функцию rand() для генерации случайных чисел. Наша следующая остановка — официальная документация PHP для исследования этой функции rand() .

Генератор случайных чисел засевается автоматически.

Документация говорит нам, что посев происходит автоматически. Теперь у нас есть другая задача. Нам нужно найти способ контролировать семя. Функция srand() может помочь с этим. Вот его определение из документации.

Заполняет генератор случайных чисел начальным числом или случайным значением, если начальное число не задано.

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

1
2
3
4
5
6
7
8
9
function testGenerateOutput() {
    ob_start();
    srand(1);
    require_once __DIR__ .
    $output = ob_get_contents();
    ob_end_clean();
 
    var_dump($output);
}

Мы ставим srand(1) перед нашим require_once() . Теперь вывод всегда один и тот же.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
class GoldenMasterTest extends PHPUnit_Framework_TestCase {
 
    function testGenerateOutput() {
        file_put_contents(‘/tmp/gm.txt’, $this->generateOutput());
        $file_content = file_get_contents(‘/tmp/gm.txt’);
        $this->assertEquals($file_content, $this->generateOutput());
    }
 
    private function generateOutput() {
        ob_start();
        srand(1);
        require_once __DIR__ .
        $output = ob_get_contents();
        ob_end_clean();
        return $output;
    }
 
}

Это изменение выглядит разумным. Правильно? Мы извлекли генерацию кода в метод, запустили его дважды и ожидали, что результат будет одинаковым. Однако они не будут.

Причина в том, что require_once() не будет требовать один и тот же файл дважды. Второй вызов метода generateOutput() создаст пустую строку. Итак, что мы можем сделать? Что если мы просто require() ? Это должно быть запущено каждый раз.

Что ж, это приводит к другой проблеме: "Cannot redeclare echoln()" . Но откуда это? Это прямо в начале файла Game.php . Причина возникновения этой ошибки заключается в том, что в GameRunner.php мы include __DIR__ . '/Game.php'; include __DIR__ . '/Game.php'; , который пытается включить файл Game дважды, каждый раз, когда мы вызываем метод generateOutput() .

1
include_once __DIR__ .

Использование include_once в GameRunner.php решит нашу проблему. Да, нам нужно было изменить GameRunner.php без тестов для этого, пока! Однако мы можем быть на 99% уверены, что наше изменение не нарушит сам код. Это небольшое и достаточно простое изменение, которое нас не сильно пугает. И самое главное, это делает тесты успешными.

Теперь, когда у нас есть код, который мы можем запускать много раз, пришло время генерировать некоторые выходные данные.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
function testGenerateOutput() {
    $this->generateMany(20, ‘/tmp/gm.txt’);
    $this->generateMany(20, ‘/tmp/gm2.txt’);
    $file_content_gm = file_get_contents(‘/tmp/gm.txt’);
    $file_content_gm2 = file_get_contents(‘/tmp/gm2.txt’);
    $this->assertEquals($file_content_gm, $file_content_gm2);
}
 
private function generateMany($times, $fileName) {
    $first = true;
    while ($times) {
        if ($first) {
            file_put_contents($fileName, $this->generateOutput());
            $first = false;
        } else {
            file_put_contents($fileName, $this->generateOutput(), FILE_APPEND);
        }
        $times—;
    }
}

Мы извлекли другой метод здесь: generateMany() . У него есть два параметра. Один для числа раз, когда мы хотим запустить наш генератор, другой — файл назначения. Это поместит сгенерированный вывод в файлы. При первом запуске он очищает файлы, а для остальных итераций добавляет данные. Вы можете заглянуть в файл, чтобы увидеть сгенерированный результат 20 раз.

Но ждать! Один и тот же игрок выигрывает каждый раз? Это возможно?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
cat /tmp/gm.txt |
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.

Да! Возможно! Это более чем возможно. Это верная вещь. У нас есть то же самое семя для нашей случайной функции. Мы играем в одну и ту же игру снова и снова.

Нам нужно играть в разные игры, в противном случае почти наверняка, только небольшая часть нашего унаследованного кода фактически выполняется снова и снова. Задача золотого мастера — тренироваться как можно больше. Нам нужно заново запускать генератор случайных чисел каждый раз, но контролируемым образом. Одним из вариантов является использование нашего счетчика в качестве начального значения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
private function generateMany($times, $fileName) {
    $first = true;
    while ($times) {
        if ($first) {
            file_put_contents($fileName, $this->generateOutput($times));
            $first = false;
        } else {
            file_put_contents($fileName, $this->generateOutput($times), FILE_APPEND);
        }
        $times—;
    }
}
 
private function generateOutput($seed) {
    ob_start();
    srand($seed);
    require __DIR__ .
    $output = ob_get_contents();
    ob_end_clean();
    return $output;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
cat /tmp/gm.txt |
Sue now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Pat now has 6 Gold Coins.
Pat now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Sue now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Sue now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Sue now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Pat now has 6 Gold Coins.
Chet now has 6 Gold Coins.
Chet now has 6 Gold Coins.

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

Первое, что вы можете попробовать — запустить наш код для 20 000 итераций игры.

1
2
3
4
5
6
7
8
function testGenerateOutput() {
    $times = 20000;
    $this->generateMany($times, ‘/tmp/gm.txt’);
    $this->generateMany($times, ‘/tmp/gm2.txt’);
    $file_content_gm = file_get_contents(‘/tmp/gm.txt’);
    $file_content_gm2 = file_get_contents(‘/tmp/gm2.txt’);
    $this->assertEquals($file_content_gm, $file_content_gm2);
}

Это будет почти работать. Будут созданы два файла по 55 МБ.

1
2
3
ls -alh /tmp/gm*
-rw-r—r— 1 csaba csaba 55M Mar 14 20:38 /tmp/gm2.txt
-rw-r—r— 1 csaba csaba 55M Mar 14 20:38 /tmp/gm.txt

С другой стороны, тест не пройден с недостаточной ошибкой памяти. Неважно, сколько у вас оперативной памяти, это не удастся. У меня есть 8 ГБ плюс 4 ГБ подкачки, и это не удается. Две строки слишком велики, чтобы их можно было сравнить в нашем утверждении.

Другими словами, мы генерируем хорошие файлы, но PHPUnit не может их сравнивать. Нам нужен обходной путь.

1
$this->assertFileEquals(‘/tmp/gm.txt’, ‘/tmp/gm2.txt’);

Кажется, это хороший кандидат, но все равно не получается. Какой позор. Нам нужно изучить ситуацию дальше.

1
$this->assertTrue($file_content_gm == $file_content_gm2);

Это, однако, работает.

Он может сравнить две строки и потерпеть неудачу, если они разные. Однако имеет небольшую цену. Он не сможет точно сказать, что не так, когда строки различаются. Он просто скажет: "Failed asserting that false is true." , Но об этом мы поговорим в следующем уроке.

Мы сделали для этого урока. Мы многому научились для нашего первого урока, и у нас хорошее начало для нашей будущей работы. Мы встретили код, проанализировали его по-разному и в основном поняли его основную логику. Затем мы создали набор тестов, чтобы убедиться, что он выполняется максимально. Да. Тесты очень медленные. На моем процессоре Core i7 им требуется 24 секунды, чтобы сгенерировать вывод дважды. К счастью, в нашей дальнейшей разработке мы будем сохранять файл gm.txt и генерировать еще один только один раз за запуск. Но 12 секунд — все еще огромное количество времени для такой маленькой базы кода.

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