КРАТКИЙ КОНСПЕКТ книги
"Эффективная работа с унаследованным кодом" Майкл. К. Физерс

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

Эта книга поможет понять с чего начинать рефакторинг и как покрыть тестами уже написанную большую систему.

Четыре причины изменений в программном коде:
1 ввод свойств (изменение поведений кода)
2 исправление программной ошибки
3 улучшение структуры кода
4 оптимизация использования ресурсов

Менять в программе можно: структуру, функциональные свойства и использование ресурсов.

Изменения в системе делаются двумя способами:

  • правка на удачу
  • покрытие тестами и модификация

    Проблема унаследованного кода: При изменении кода, тесты должны находиться на своих местах. А для размещения тестов на своих местах зачастую приходится изменять сам код. Поэтому первоначальные изменения приходится делать методом «правка на удачу» с большой осторожностью. Методы этой книги позволят уменьшить риск внесения ошибок при размещении тестов по местам.

    Алгоритм изменения унаследованного кода:
    1 Определение точек изменения
    2 Нахождение тестовых точек
    3 Разрывание зависимостей
    4 Написание тестов
    5 Внесение изменений и реорганизация кода

    При размещении тестов по местам появляются две причины для разрыва зависимостей:

  • распознавание (результатов выполнения кода)
  • разделение (вызов кода отдельно от всей системы)

    Для распознавания используется имитация взаимодействующих объектов. Такие объекты называются фиктивными (mock).

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

    У каждого шва имеется разрешающая точка — место, где можно выбрать то или иное поведение.

    Объектный шов:
    public function getPrice($service)
    {
    ...
    $service->calculate();
    ...
    }

    Метод getPrice является объектным швом, так как мы можем заменить вызов конкретного метода $service->calculate() не меняя окружающий ее код. Разрешающей точкой для данного шва является $service так как именно в этой точке мы можем решить, какой именно объект передать в метод, чтобы изменить вызов метода $service->calculate().

    Как ввести новое свойство

    Разработка посредством тестирования:
    1 Написание контрольного примера не прохождения теста
    2 Подготовка к компиляции
    3 Подготовка в прохождению
    4 Исключение дублирования кода
    5 Повтор

    Способы разрыва зависимостей:

    Ниже приведенные методы не сделают код лучше. Но они помогут написать тесты. А имея уже тесты, можно проводить рефакторинг, методами, основанными на тестах.

    Если изменения необходимы, а времени нет...

    Почкование метода/класса

    Почкование метода или класса нужно тогда, когда протестировать весь метод/класс не представляется возможным. Изначально написание новых методов/классов может показаться избыточным. Но со временем множество отпочкованых методов и классов можно пересмотреть, реорганизовав в другие классы.

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

    Алгоритм почкования:
    1 Определите место, где требуется внести изменения в код
    2 Если изменения можно выделить в виде отдельной последовательности операторов, написав новый метод, то напишите код вызова будущего метода (создайте объект и вызовите метод в случаи почкования класса).
    3 Определите в исходном коде все локальные переменные, которые вам могут понадобиться и сделайте их аргументами в вызове нового метода.
    4 Напишите новый метод/класс используя методику TDD

    Охват метода/класса

    Создается метод/класс с именем первоначального метода и делегируется старому коду. В созданный метод/класс добавляются необходимые действия.

    Охват метода это отличный способ ввести швы при добавлении новых свойств.

    Алгоритм охвата метода:
    1 Определите метод, который необходимо изменить
    2 Если изменения можно выразить в виде отдельной последовательности операторов, то переименуйте метод, а затем создайте новый метод с таким же именем и сигнатурой как у старого.
    3 Поместите вызов старого метода в новом метода
    4 Разработайте метод для нового свойства, используя TDD, а затем вызовите его из нового метода

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

    Охват класса делает тоже что и паттерн декоратор. Он позволяет вводить новое поведение в систему не вводя его в существующий класс.

    Алгоритм охвата класса:
    1 Определите место, где нужно внести изменения в код
    2 Если изменения можно выразить в виде отдельной последовательности операторов, то создайте класс, воспринимающий тот класс, который предполагается охватить в виде аргумента конструктора.
    3 Используя TDD, создайте в данном классе метод, выполняющий новые функции. Напишите еще один метод, вызывающий новый и старый методы в охватываемом классе.
    4 Получите экземпляр охватывающего класса в том месте кода, где вам требуется активизировать новое поведение.

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

    Адаптация параметра. Вместо реального параметра передается другой, который представляет собой экземпляр упрощенного класса.
    1. Создайте новый интерфейс, сделав его как можно более простым
    2. Создайте средство реализации нового интерфейса в выходном коде
    3. Создайте фиктивное средство реализации нового интерфейса
    4. Создайте простой контрольный пример, передав методу фиктивный объект
    5. Внесите в метод изменения, чтобы использовать новый параметр
    6. Выполните тест, чтобы проверить возможность тестирования фиктивного объекта

    Вынос объекта метода. Перенос длинного метода в новый класс. Локальные переменные в старом методе могут стать переменными экземпляров нового класса.
    1. Создайте класс, который будет содержать код метода
    2. Создайте конструктор для данного класса и сохраните сигнатуры. Для каждого аргумента конструктора создайте переменную класса и присвойте ей значение аргумента.
    3. Создайте новый метод в новом класса и скопируйте в него тело старого метода.
    4. Устраните ошибки в новом методе, которые возникают при компиляции метода. Сообщения об ошибках покажут места, где до сих пор используются методы или переменные из старого класса.
    5. Вернитесь к исходному коду и измените его так, чтобы он создавал экземпляр нового класса и передавал ему свои полномочия.

    Инкапсуляция глобальных ссылок. Объединить глобальные переменные в один класс. В итоге получаем обращение к члену класса, вместо обращения к глобальной переменной. Метод можно использовать при работе с вызовами API сторонних приложений. В итоге получим вызов методов класса (который можно подменить при тестировании) вместо внешнего приложения.
    1. Выявите глобальные объекты, которые требуется инкапсулировать и вынесите их в отдельный класс.
    2. Объявите глобальный экземпляр нового класса.
    3. Замените ссылки на глобальные объекты ссылками на методы нового класса.
    4. Там где понадобиться создать фиктивные объекты, воспользуйтесь вводом статического установщика, параметризацией конструктора и тп.

    Раскрытие статического метода. Если метод не использует данные экземпляров класса, его целесообразно сделать статическим.
    1. Извлеките тело метода в статический метод, сохранив сигнатуры.
    2. Если при компиляции обнаружится, что метод все же использует методы или свойства экземпляров, то попробуйте и их сделать статичными.
    3. Напишите тест для нового метода

    Извлечение и переопределение вызова. Если вывоз какого-то метода препятствует тестированию, то его можно вынести в новый метод класса. Для фиктивного объекта переопределить этот метод.
    1. Скопируйте вызов метода, который требуется извлечь в новый метод класса
    2. Замените вызов старого метода на вызов нового.
    3. Введите тестирующий подкласс и переопределите метод.

    Извлечение и переопределение фабричного метода. Создание объектов переносится в отдельный фабричный метод, который можно переопределить для средств тестирования.
    1. Выявите создание объектов в конструкторе класса
    2. Извлеките все, что относится к созданию объекта в фабричный метод
    3. Создайте тестирующий подкласс и переопределите для него фабричный метод

    Извлечение и переопределение получателя. Метод очень похож на извлечение и переопределение вызова. Этот метод следует выбрать при наличии многих проблематичных методов в одном и том же объекте.
    1. Выявите объект в котором требуется получатель
    2. Извлеките всю логику для создания объекта в получатель, в соответствии с шаблоном «одиночка»
    3. Замените все примеры применения объекта вызовами получателя и инициализируйте ссылку на объект пустым значением во всех конструкторах.
    4. Создайте подкласс и переопределите получатель, чтобы предоставить альтернативный объект для тестирования

    Извлечение средств реализации. Класс объявляется интерфейсом, а все его конкретные методы переносятся в подкласс.
    1. Сделайте копию исходного класса, присвоив исходному классу другое имя
    2. Преобразуйте исходный класс в интерфейс, удалив все не общедоступные методы и переменные.
    3. Сделайте абстрактными все оставшиеся общедоступные методы
    4. Сделайте выходной класс реализацией этого нового интерфейса
    5. Замените в коде места создания исходного класса на создание выходного класса

    Извлечение интерфейса. Нужно для того, чтобы правильно создать фиктивный класс.
    1. Создайте новый интерфейс. Не спешите вводить в него методы
    2. Реализуйте интерфейс в том классе, из которого он извлекается.
    3. Измените место, где требуется использовать объект, чтобы вместо исходного класса этим местом оказался интерфейс
    4. Выполните компиляцию и введите в интерфейс новое объявление каждого метода, об ошибке использования которого сообщает компилятор

    Ввод делегата экземпляра. Позволяет заменить вызов статического метода объектным швом. Имеет смысл использовать данный метод, только если есть возможность внешнего создания объекта класса.
    1. Выявите статический метод, который сложно использовать в тесте
    2. Создайте в этом же классе другой метод, который будет вызывать этот статический метод.
    3. Найдите в тестируемом коде места, где используется данный статический метод. Воспользуйтесь параметризацией метода или другим способом разрыва зависимостей, чтобы предоставить экземпляр класса.
    4. Замените вызов статического метода вызовом его делегата из экземпляра введенного в пункте 3.

    Ввод статического установщика. Позволяет заменить для тестирования объект «одиночку».
    1. Ослабьте защиту конструктора до такой степени, чтобы можно было создать подделку с помощью подклассификации одиночки.
    2. Введите статический установщик в класс одиночки. Установщик должен воспринимать ссылку на класс одиночки. Заменяя эту ссылку легко добиться создания фиктивного класса «одиночки»

    Параметризация метода. Если внутри метода создается объект, самый простой способ его подменить состоит в том, чтобы создать его вне метода и передать методу в качестве параметра. Такой же метод используется для параметризация конструктора. В данном случаи, если в конструкторе создается объект, его создание выносится за пределы конструктора и передается в конструктор в виде параметра.
    1. Выявите метод (или конструктор), который следует заменить.
    2. Введите в метод дополнительный параметр. Замените создание объекта присвоением значения этого параметра.
    3. В местах вызова метода организуйте создание объекта и передачу его в качестве параметра.

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

    Вытягивание свойства. Перенос метода в супер-класс, который проще тестировать из-за отсутствия множественных зависимостей.
    1. Выявите методы, которые требуется вытянуть вверх по иерархии
    2. Создайте абстрактный супер-класс для, класса содержащего эти методы
    3. Скопируйте методы в супер-класс и все недостающие свойства, о которых сообщит компилятор
    4. Когда оба класса скомпилируются успешно, создайте подкласс для абстрактного класса и введите в него все методы, которые нужно подготовить к тестированию.

    Вытеснение зависимости. Вытеснение зависимостей в подкласс, который можно заменить в средствах тестирования.
    1. Выявите зависимости, затрудняющие создание класса в средствах тестировании
    2. Создайте новый подкласс с именем, отражающим особенности окружения данных зависимостей
    3. Скопируйте переменные и методы, содержащие трудно-определимые зависимости в новый подкласс.
    4. Сделайте методы защищенными в исходном классе, а сам исходный класс — абстрактным

    Замена глобальной ссылки получателем. Обращение к глобальным переменным идет через метод «получателя» для каждого класса. Получать преобразовывает глобальные переменные в более подходящие типы данных.
    1. Выявите глобальную ссылку которую необходимо заменить
    2. Напишите получатель для глобальной ссылки
    3. Замените все ссылки на глобальный объект вызовами получателя
    4. Создайте тестирующий подкласс и переопределите получателя

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

    Когда следует применять описанные выше методы.

    Обращаться к совокупности методов, описанных выше имеет смысл когда:

    1) Класс нельзя ввести в средства тестирования

    Причины:

  • конструктор класса имеет множество побочных эффектов
  • в конструкторе класса происходит нечто очень важное, требующее распознавания
  • объекты класса сложно просто создать
  • средства тестирования трудно компонуются с находящимся в них классом

    2) Метод нельзя выполнить в средствах тестирования

    Причины:

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

    3) Требуются изменения в коде, но не понятно какие методы следует тестировать

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

    Для того, чтобы понять на что повлияют изменения необходимо осмыслить воздействие. Осмысление возможно в прямом и обратном направление (по восходящей и нисходящей от класса соответственно). Осмысление происходит путем составления графа связей (структуры взаимодействия). Чем проще структура взаимодействия, тем лучше код. Зачастую структура взаимодействия помогает лучше понять и разделить ответственности классов.

    Воздействия распространяются в коде тремя основными путями:
    1) Возвращаемые значения, используемые в вызывающей части программы
    2) Видоизменения объектов, передаваемых в качестве параметра, используемых в дальнейшем
    3) Видоизменение статических или глобальных данных, используемых в дальнейшем

    Процедура эвристического анализа для обнаружения воздействий в коде:
    1. Выявить метод, который подлежит изменению
    2. Если метод возвращает значение, то проанализировать все места, откуда он вызывается
    3. Проверить, видоизменяет ли метод какие либо значения. Если видоизменяет, то проанализировать методы, использующие эти значения, а так же методы, вызывающие эти методы
    4. Обязательно проверить супер-классы и подклассы, которые бывают пользователями переменных экземпляров и методов, выявленных в ходе данного анализа
    5. Проанализировать параметры, передаваемые методам, выявленным в ходе данного анализа. Проверить, используются ли эти параметры или любые объекты, возвращаемые их методами, в коде, который требуется изменить
    6. Найти глобальные переменные и статические данные, видоизменяемые в любом из методов, выявленных в ходе данного анализа

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

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

    Заключение

    Книга безусловно полезна. Думаю, каждый из нас сталкивался с унаследованным кодом (даже если унаследовали мы его от себя самого более ранней версии :)). В отличии от рефакторинга Фаулера, который предполагает, что тесты у вас уже есть, методы данной книги помогают написать эти самые тесты для уже существующей сложной системы, попутно делая ее лучше. В книге очень много полезного материала, которого с лихвой бы хватило на несколько книг. Поэтому для лучшего понимания всего, что есть в книге - ее стоит перечитать еще раз.

  • Копирование материалов разрешено при наличии активной ссылки на источник
    Яндекс.Метрика