Рефакторинг программного обеспечения
Типичные недостатки кода
- Повторяющийся код (Duplicated Code)
Номер один среди недостатков – Повторяющийся код. Если есть два повторяющихся участка кода, то можно быть уверенным, что программа только выиграет, если эти участки перейдут в один.
Простейший случай, когда код повторяется в телах двух методов одного класса. Тогда надо просто применить Извлечение метода и вызывать новый метод из всех мест, где повторялся код.
Другой общий случай, когда код повторяется в двух подклассах одного класса. Тогда нужно использовать Извлечение метода в обоих подклассах, а затем применить Поднимание метода . Если код похож, но не одинаков, надо использовать Извлечение метода , чтобы выделить одинаковые части.
Затем можно применить Формирование шаблонного метода . Если методы делают одно и тоже с помощью разных алгоритмов, то надо выбрать лучший и применить Замену алгоритма .
Если код дублируется в несвязанных классах, можно применить Извлечение класса к одному классу, и использовать извлеченный класс в других. Другая возможность состоит в том, что метод принадлежит одному классу и вызывается другим, либо принадлежит какому-либо постороннему классу, и вызывается всеми классами с повторяющимся кодом.
В общем, метод надо выбирать, согласуясь со здравым смыслом – главное, чтобы не было дублирующегося кода.
Длинные методы сложны для понимания и сопровождения, так что есть смысл в их упрощении и уменьшении.
В 99% случаев применяется Извлечение метода .
Если в методе большое количество параметров и локальных переменных, то при использовании Извлечения метода в него придется передавать все эти параметры и переменные в виде параметров, что может привести к еще большей путанице. В этом случае можно применить Замену временной переменной вызовом метода . Длинный список параметров уменьшается с помощью Передачи объекта целиком и Введения объекта параметров . Если вышеперечисленное слабо помогает, то пришло время для Избавления от метода с помощью объекта метода .
Как выбрать код для извлечения? Очень полезно ознакомиться с комментариями: кусок кода с комментариями, объясняющими, что происходит, заменяется на метод с таким же содержательным названием. Можно извлекать даже отдельную строку, если она нуждается в объяснении.
Также используется Декомпозиция условий для упрощения логических выражений.
Когда класс пытается отвечать за все, возникает огромное количество его экземпляров, а отсюда недалеко и до повторяющегося кода.
Решение – Извлечение класса либо Извлечение подкласса . Для применения этих рефакторингов надо выбрать членов класса, которые будут из него удалены. Иногда полезно применить эти рефакторинги несколько раз.
Также бывает полезно рассмотреть, как клиенты используют методы класса и применить Извлечение интерфейса – это может натолкнуть на идею, как поделить класс на части.
Если этим большим классом является класс, описывающий пользовательский интерфейс, надо перенести данные и поведение в класс предметной области, воспользовавшись Дублированием данных пользовательского интерфейса .
- Длинный список параметров
Длинные списки параметров неудобны в обращении: их трудно понимать и их постоянно приходится менять при смене информации, которая нужна методу.
Использование Замены параметра вызовом метода позволяет заменить параметр на запрос к известному объекту. Использование Передачи объекта целиком заменяет передачу отдельных частей объект передачей всего объекта сразу. Если передается связный набор данных, не принадлежащий к какому-либо объекту, спасает использование Введения объекта параметров .
Но надо помнить, делая эти замены, что они вносят в систему новые зависимости между объектом-хозяином метода и используемыми в методе объектами. Если появление этих зависимостей нежелательно, то можно передавать параметры по старому. Если же списки параметров слишком длинны, либо их изменения слишком часты, то надо заново обдумывать структуру зависимостей в программе.
- Различное изменение класса
Такие изменения возникают, когда один класс постоянно изменяется различным образом в связи с различными причинами. Гораздо лучше, когда один класс подвержен изменениям только в связи с одной деятельностью, и когда любое изменение условий затрагивает только один класс.
Решается проблема с помощью Извлечения класса для множеств членов класса, зависящих от одного типа изменеий.
Изменение многих классов – это обратное явление к Различным изменениям класса. То есть, если при каждом изменении надо внести кучу переделок в большое количество разных классов, то в конце концов какая-нибудь переделка забудется и все благополучно перестанет работать.
Для борьбы со злом надо использовать Перемещение поля и Перемещение метода для скопления изменяющихся членов в одном классе. Если подходящего класса нет, надо его создать. Иногда полезно применить Встраивание класса , добавив один зависящий от изменений данного типа класс к другому. Вследствие этого можно получить легкую дозу Различных изменений класса , но зачастую с этим легче иметь дело, чем с человеконенавистническими способами лечения.
- Зависть к чужим членам класса
Обычная ситуация: метод заинтересован в членах класса, отличного от того, к которому он принадлежит гораздо больше, чем в членах своего класса. Тогда Перемещение метода – очевидное решение. Иногда только часть метода страдает от зависти – тогда Извлечение метода применяется к страдающей части, затем Перемещение метода для переноса получившегося метода в дом его мечты.
Если метод использует части многих классов, то помещается в том, который он использует больше других, либо делится и разносится по этим классам.
Данные нередко группируются вместе: то как поля класса, то как параметры метода. Такой набор данных имеет смысл выделить в отдельный объект с помощью Извлечения класса . Если эти данные встречаются в вызовах методов, можно воспользоваться Передачей объекта целиком или Введением объекта параметров .
Хорошая проверка пучковости – посмотреть, что получиться, если удалить одно из входящих в пучок полей. Если остальные при этом потеряют смысл, значит данные действительно надо группировать. Вместе с полями в новые объекты переносятся и методы, которые их обрабатывают.
Большинство языков программирования имеют два вида данных: примитивные типы и записи.
Одно из ценных свойств объектов заключается в том, что они стирают границу примитивными типами и большими классами. Можно легко использовать маленькие объекты, которые будут неотличимы от примитивных типов языка.
Этому способствуют: Замещение поля объектом для отдельных полей, Замена кодирования типа классом для кодирования типа, не влияющее на поведение, Замена кодирования типа подклассом либо Замена кодирования типа Состоянием/Стратегией для кодирования типа, которое влияет.
Если есть группа полей, которые должны быть вместе, не надо этому мешать – Извлечение класса . Если примитивы расположились в списках параметров – Введение объекта параметров , если попали массив – Замена массива на объект .
Отличительный признак хорошей объектно-ориентированной программы – относительно небольшое количество операторов выбора. Основная проблема с этими операторами в дублирующемся коде: если такие операторы разбросаны по всей программе, и добавляется новый вариант выбора, то надо найти все эти операторы и изменить их. Полиморфизм предоставляет красивый выход из этой ситуации.
В большинстве случаев, когда возникает оператор выбора, надо рассматривать применение полиморфизма.
Если оператор основан на кодировании типа, и нужен класс, который будет соответствовать определенному значению – тогда применяется Извлечение метода , чтобы вынести этот оператор, и Перемещение метода для помещения его в класс, где требуется полиморфизм.
- Здесь надо выбрать между Заменой кодирования типа подклассами и Заменой кодирования типа состоянием/стратегией . Когда структура наследования будет установлена, можно использовать Замену условия полиморфизмом .
- Если же имеется немного альтернатив, влияющих на один метод, и изменения не ожидаются, то полиморфизм излишен. В этом случае надо использовать Замену параметра набором методов . Если среди возможных альтернатив есть NULL, то надо использовать Введение NULL объекта .
- Параллельные иерархии наследования
Параллельная иерархия – частный случай изменения многих классов: каждый раз, когда надо создать подкласс какого-либо класса, приходится создавать подкласс и у другого. Признаком этого может служить повторение префиксов имен в двух иерархиях.
Общая стратегия избавления от этого недостатка – увериться в том, что экземпляры одной иерархии ссылаются на экземпляры другой. После использования Перемещения метода и Перемещения поля иерархия ссылающихся классов исчезает.
Каждый класс требует затрат на понимание и поддержку, поэтому классы, которые не делают достаточно, чтобы уделять им отдельное внимание, должны быть уничтожены.
Такая ситуация может сложиться, если класс был задуман как полнофункциональный, но в результате рефакторинга ужался до неприличных размеров, либо класс добавили в расчете на некие будущие разработки, до которых руки так и не дошли.
Если ленивость касается подклассов, используется Уничтожение иерархии , если просто классов, используется Встраивание класса .
Ситуация возникает, когда ведется широкомасштабная подготовка к реализации кучи возможностей, которые так и не воплощаются в жизнь. В результате приходится выметать эти излишние приготовления.
Метлами служат: Уничтожение иерархии при ненужном наследовании, Встраивание класса при бесполезном делегировании, Избавление от параметра при лишних параметрах, Переименование метода при слишком общих и непонятных именах методов
Иногда некоторые поля нужны объекту только при определенных обстоятельствах. Такое положение вещей трудно понимаемо, так как ожидается, что объекту нужны все его поля.
Для борьбы с этой напастью можно использовать Извлечение класса , предоставляя сироткам их собственный дом. Здесь же можно избавить от проверки на NULL, с помощью Введения NULL объекта .
Этот случай возникает, когда сложному алгоритму нужно много переменных. Если имеется список параметров, переменные описываются как поля, при этом эти поля имеют смысл только во время работы алгоритма. В этом случае снова пригодится Извлечение класса , чтобы вытащить эти поля вместе с методом, который их использует – получится объект метода.
Цепочка вызовов – это когда клиент спрашивает какой-то объект о другом объекте, другой объект еще об одном и так далее. Пользование таким путем означает, что клиент зависит от всего пути, и любые изменения в промежуточных звеньях заставят клиента также измениться.
Выход – Скрывание делегирующего класса . Применить этот рефакторинг можно к различным частям цепи. В принципе, можно применить и ко всем звеньям, но тогда все объекты в цепи станут промежуточными серверами. Зачастую лучшим выходом является рассмотрение, для чего используется результирующий объект. Может быть, имеет смысл использовать Извлечение метода , чтобы выделить эту функциональность, а затем применить Перемещение метода для передвижения получившегося метода назад по цепи.
Одно из главных преимуществ объектов – инкапсуляция – сокрытие внутренних деталей. Инкапсуляция идет рука об руку с делегированием.
Бывают классы, у которых большинство методов состоят только из вызова метода другого класса. Это значит, что пришло время использовать Избавление от промежуточного сервера и обращаться напрямую к объекту, который реально знает, что происходит. Можно воспользоваться также Встраиванием метода , встроив делегирующий метод в тела вызывающих его методов. Если имеет место добавочное поведение, поможет Замена делегирования наследованием для описания промежуточного сервера как подкласса того класса, который он расширяет.
Лечение: Перемещение поля , Перемещение метода для передвижения членов туда, где они нужнее; Замена двунаправленной связи на однонаправленную для упрощения структуры связей; Извлечение класса , если у нескольких классов есть общая потребность в чем-то (пусть все вместе используют новый класс); Скрывание делегирующего класса для установления посредника.
Подклассы зачастую хотят знать о родителях гораздо больше, чем то, о чем им могут поведать. Раз так, пригодится Замена наследования делегированием .
- Альтернативные классы с разными интерфейсами
Пусть есть два класса, в которых часть функциональности общая, но методы, реализующие ее, имеют разные параметры.
Тогда используется Переименование методов , чтобы методы, делающие одно и тоже, назывались одинаково. Далее – Перемещение метода , чтобы интерфейсы, обеспечивающие функциональность, стали одинаковыми. Если при этом надо перемещать слишком много кода, надо задуматься об Извлечении суперкласса .
- Неполный библиотечный класс
Если кем-то реализован набор классов, то, как правило, через некоторое время он перестает удовлетворять требованиям пользователей. Естественное решение – поменять кое-что в этой библиотеке, но вся беда в том, что эти классы не изменить. Тогда используется пара рефакторингов, специально предназначенных для этой цели.
Если надо добавить пару методов, используется Введение внешнего метода . Если надо серьезно поменять поведение класса, используется Введение локального расширения .
Это класс, в котором есть поля методы, методы выборки и установки значений (Get и Set методы), и больше ничего. Функционально это просто контейнеры для данных, используемые другими классами.
На начальных стадиях разработки поля могут быть публичными. Если так, надо применить Инкапсулирование поля . Если есть коллекция, надо проверить правильность ее инкапсулирования. Если она инкапсулирована неправильно, необходимо применить Инкапсулирование коллекции . Если поля не должны изменяться, надо использовать Избавление от метода установки значения .
Далее нужно проверить, где используются методы выборки и установки значений данного класса. В этих местах можно использовать Перемещение метода , чтобы передвинуть поведение в класс данных. Если нет смысла двигать весь метод, можно воспользоваться Извлечением метода , чтобы выделить метод, который можно переместить.
Подклассы получают от своих родителей множество наследуемых полей и методов, но выбирают только часть из них.
Обычная история – иерархия неверна. В этом случае создается еще один подкласс суперкласса и используются Спускание метода и Спускание поля с тем расчетом, чтобы в суперклассе остались только общие члены.
Особый случай, когда подкласс не хочет поддерживать интерфейс суперкласса. Тогда надо применять Замену наследования делегированием .