Код с запашко́м (код с душко́м, дурно пахнущий код) — термин, обозначающий код с признаками (запахами) проблем в системе. Был введён Кентом Беком[1] и использован Мартином Фаулером в его книге «Рефакторинг. Улучшение существующего кода»[1].
Запахи кода (англ.code smells)➤ — это ключевые признаки необходимости рефакторинга[2]. Существуют запахи, специфичные как для парадигм программирования, так и для конкретных языков. Основной проблемой, с которой сталкиваются разработчики при борьбе с запахами кода, является то, что критерии своевременности рефакторинга невозможно чётко формализовать без апелляции к эстетике и условному чувству прекрасного. Запахи кода — это не набор чётких правил, а описание мест, на которые нужно обращать внимание при рефакторинге[3]. Они легко обнаруживаются, но при этом не во всех случаях свидетельствуют о проблемах[1].
Код с запашком ведёт к деградации кода (снижению его качества), и разработчики должны стремиться к устранению запашков путём применения однократного или многократного рефакторинга[4].
В процессе рефакторинга происходит избавление от запахов кода, что обеспечивает возможность дальнейшего развития приложения с той же или большей скоростью. Отсутствие регулярного рефакторинга с течением времени способно полностью парализовать проект, поэтому запахи кода необходимо устранять на ранних стадиях[2].
Существуют инструменты поиска и исправления запахов кода[5], однако опыт показывает, что никакие системы показателей не могут соперничать с человеческой интуицией, основанной на информации[6].
Дублирование кода — это использование одинаковых структур кода в нескольких местах. Объединение этих структур позволит улучшить программный код[6].
Примеры дублирования и методы их устранения:
Одно и то же выражение присутствует в двух методах одного и того же класса: необходимо применить «Выделение метода» (Extract Method) и вызывать код созданного метода из обеих точек;
Одно и то же выражение есть в двух подклассах, находящихся на одном уровне: необходимо применить «Выделение метода» (Extract Method) для обоих классов с последующим «Подъёмом поля» (Pull Up Field) или «Формированием шаблона метода» (Form Template Method), если код похож, но не совпадает полностью. Если оба метода делают одно и то же с помощью разных алгоритмов, можно выбрать более чёткий из этих алгоритмов и применить «Замещение алгоритма» (Substitute Algorithm);
Дублирующийся код находится в двух разных классах: необходимо применить «Выделение класса» (Extract Class) в одном классе, а затем использовать новый компонент в другом[6].
Длинный метод
Среди объектных программ дольше всего живут программы с короткими методами. Чем длиннее процедура, тем труднее её понять. Если у метода хорошее название, то не нужно смотреть его тело[3].
Следует придерживаться эвристического правила: если ощущается необходимость что-то прокомментировать, нужно написать метод. Даже одну строку имеет смысл выделить в метод, если она нуждается в разъяснениях[7].
Для сокращения метода достаточно применить «Выделение метода» (Extract Method);
Если локальные переменные и параметры препятствуют выделению метода, можно применить «Замену временной переменной вызовом метода» (Replace Temp with Query), «Введение граничного объекта» (Introduce Parameter Object) и «Сохранение всего объекта» (Preserve Whole Object)[3];
Условные операторы и циклы свидетельствуют о возможности выделения в отдельный метод. Для работы с условными выражениями подходит «Декомпозиция условных операторов» (Decompose Conditional). Для работы с циклом — «Выделение метода» (Extract Method)[7].
Большой класс
Когда класс реализует слишком обширную функциональность, стоит подумать о вынесении некоторой части кода в подкласс. Это избавит разработчиков от чрезмерного количества имеющихся у класса атрибутов и дублирования кода[7].
Для уменьшения класса используется «Выделение класса» (Extract Class) или «Выделение подкласса» (Extract Subclass). При этом следует обращать внимание на общность в названии атрибутов и на то, использует ли класс их все одновременно[3];
Если большой класс является классом GUI, может потребоваться переместить его данные и поведение в отдельный объект предметной области. При этом может оказаться необходимым хранить копии некоторых данных в двух местах и обеспечить их согласованность. «Дублирование видимых данных» (Duplicate Observed Data) предлагает путь, которым можно это осуществить[8].
Длинный список параметров
В длинных списках параметров трудно разбираться, они становятся противоречивыми и сложными в использовании.
Использование объектов позволяет, в случае изменения передаваемых данных, модифицировать только сам объект. Работая с объектами, следует передавать ровно столько, чтобы метод мог получить необходимые ему данные[8].
«Замена параметра вызовом метода» (Replace Parameter with Method) применяется, когда можно получить данные путём вызова метода объекта. Этот объект может быть полем или другим параметром.
«Сохранение всего объекта» (Preserve Whole Object) позволяет взять группу данных, полученных от объекта, и заменить их самим объектом.
«Введение граничного объекта» (Introduce Parameter Object) применяется, если есть несколько элементов данных без логического объекта[8].
Расходящиеся модификации
Проблема возникает, когда при модификации в системе невозможно выделить определённое место, которое нужно изменить. Это является следствием плохой структурированности ПО[8] или программирования методом копирования-вставки.
Если набор методов необходимо изменять каждый раз при внесении определённых модификаций в код, то применяется «Выделение класса» (Extract Class) (Например, три метода меняются каждый раз когда подключается новая БД, а четыре — при добавлении финансового инструмента)[3].
Стрельба дробью
При выполнении любых модификаций приходится вносить множество мелких изменений в большое число классов. «Стрельба дробью» похожа на «Расходящуюся модификацию», но является её противоположностью.
Расходящаяся модификация имеет место, когда есть один класс, в котором производится много различных
изменений, а «Стрельба дробью» — это одно изменение, затрагивающее много классов[9].
Если нет подходящего класса, то следует создать новый класс;
Если это необходимо, следует воспользоваться «Встраиванием класса» (Inline Class)[3].
Завистливые функции
Метод обращается к данным другого объекта чаще, чем к собственным данным[3].
«Перемещение метода» (Move Method) применяется, если метод явно следует перевести в другое место;
«Выделение метода» (Extract Method) применяется к части метода, если только эта часть обращается к данным другого объекта;
Метод использует функции нескольких классов: определяется, в каком классе находится больше всего данных, и метод помещается в класс вместе с этими данными, или с помощью «Выделения метода» (Extract Method) метод разбивается на несколько частей и они помещаются в разные места[10].
Фундаментальное практическое правило гласит: то, что изменяется одновременно, надо хранить в одном месте. Данные и функции, использующие эти данные, обычно изменяются вместе, но бывают исключения[10].
Группы данных
Группы данных, встречающихся совместно, нужно превращать в самостоятельный класс[10].
«Выделение метода» (Extract Method) используется для полей;
«Введение граничного объекта» (Introduce Parameter Object) или «Сохранение всего объекта» (Preserve Whole Object) для параметров методов[11].
Хорошая проверка: удалить одно из значений данных и проверить, сохранят ли смысл
остальные. Если нет, это верный признак того, что данные напрашиваются на объединение их в объект[10].
Одержимость элементарными типами
Проблема связана с использованием элементарных типов вместо маленьких объектов для небольших задач, таких как валюта, диапазоны, специальные строки для телефонных номеров и т. п.
«Замена значения данных объектом» (Replace Data Value with Object);
«Замена массива объектом» (Replace Array with Object);
Если это код типа, то используйте «Замену кода типа классом» (Replace Type Code with Class), «Замену кода типа подклассами» (Replace Type Code with Subclasses) или «Замену кода типа состоянием/стратегией» (Replace Type Code with State/Strategy)[3].
Операторы типа switch
Одним из очевидных признаков объектно-ориентированного кода служит сравнительно редкое использование операторов типа switch (или case). Часто один и тот же блок switch оказывается разбросанным по разным местам программы. При добавлении нового варианта приходится искать все эти блоки switch и модифицировать их. Как правило, заметив блок switch, следует подумать о полиморфизме[12].
Если switch переключается по коду типа, то следует использовать «Замену кода типа подклассами» (Replace Type Code with Subclasses) или «Замену кода типа состоянием/стратегией» (Replace Type Code with State/Strategy);
Может понадобиться «Выделение метода» (Extract Method) и «Перемещение метода» (Move Method) чтобы изолировать switch и поместить его в нужный класс;
В коде с таким запашком всякий раз при порождении подкласса одного из классов приходится создавать подкласс другого класса[12].
Общая стратегия устранения дублирования состоит в том, чтобы заставить экземпляры одной иерархии ссылаться на экземпляры другой иерархии, а затем убрать иерархию в ссылающемся классе c помощью «Перемещения метода» (Move Method) и «Перемещения поля» (Move Field)[12].
Ленивый класс
Класс, затраты на существование которого не окупаются выполняемыми им функциями, должен быть удалён [12].
При наличии подклассов с недостаточными функциями попробуйте «Свертывание иерархии» (Collapse Hierarchy);
Почти бесполезные компоненты должны быть подвергнуты «Встраиванию класса» (Inline Class)[12].
Теоретическая общность
Этот случай возникает, когда на определённом этапе существования программы обеспечивается набор механизмов, который, возможно, потребуется для некоторой будущей функциональности. В итоге программу становится труднее понимать и сопровождать[13].
Для незадействованных абстрактных классов используйте «Сворачивание иерархии» (Collapse Hierarchy);
Ненужная делегация может быть удалена с помощью «Встраивания класса» (Inline Class);
Методы с неиспользуемыми параметрами должны быть подвергнуты «Удалению параметров» (Remove Parameter)[3].
Временное поле
Временные поля — это поля, которые нужны объекту только при определённых обстоятельствах. Такое положение вещей трудно для понимания, так как ожидается, что объекту нужны все его поля[14].
Временные поля и весь код, работающий с ними, следует поместить в отдельный класс с помощью «Выделения класса» (Extract Class);
Удалить условно выполняемый код можно с помощью «Введения объекта Null» (Introduce Null Object) для создания альтернативного компонента[13].
Цепочка вызовов
Цепочка вызовов появляется тогда, когда клиент запрашивает у одного объекта другой объект, другой объект запрашивает ещё один объект и т. д. Такие последовательности вызовов означают, что клиент связан с навигацией по структуре классов. Любые изменения промежуточных связей означают необходимость модификации клиента[13].
Для удаления цепочки вызовов применяется приём «Сокрытие делегирования» (Hide Delegate)[13].
Посредник
Чрезмерное использование делегирования может привести к появлению классов, у которых большинство методов состоит только из вызова метода другого класса[13].
Если большую часть методов класс делегирует другому классу, нужно воспользоваться «Удалением посредника» (Remove Middle Man)[15].
Неуместная близость
«Неуместная близость» возникает тогда, когда классы чаще, чем следовало бы, погружены в закрытые части друг друга[15].
Избавиться от «Неуместной близости» можно с помощью «Перемещения метода» (Move Method) и «Перемещения поля» (Move Field);
По возможности следует прибегнуть к «Замене двунаправленной связи однонаправленной» (Change Bidirectional Association to Unidirectional), «Выделению класса» (Extract Class) или воспользоваться «Сокрытием делегирования» (Hide Delegate)[15].
Альтернативные классы с разными интерфейсами
Два класса, в которых часть функциональности общая, но методы, реализующие её, имеют разные параметры[16].
Применяйте «Переименование метода» (Rename Method) ко всем методам, выполняющим одинаковые действия, но различающимся сигнатурами[15].
Неполнота библиотечного класса
Библиотеки через некоторое время перестают удовлетворять требованиям пользователей. Естественное решение — поменять кое-что в библиотеках, но библиотечные классы не изменять. Следует использовать методы рефакторинга, специально предназначенные для этой цели[16].
Если надо добавить пару методов, используется «Введение внешнего метода» (Introduce Foreign Method);
Если надо серьёзно поменять поведение класса, используется «Введение локального расширения» (Introduce Local Extension)[16].
Классы данных
Классы данных — это классы, которые содержат только поля и методы для доступа к ним, это просто контейнеры для данных, используемые другими классами[16].
Следует применить «Инкапсуляцию поля» (Encapsulate Field) и «Инкапсуляцию коллекции» (Encapsulate Collection)[3].
Отказ от наследства
Если наследник использует лишь малую часть унаследованных методов и свойств родителя, это является признаком неправильной иерархии.
Необходимо создать новый класс на одном уровне с потомком и с помощью «Спуска метода» (Push Down Method) и «Спуска поля» (Push Down Field) вытолкнуть в него все бездействующие методы. Благодаря этому в родительском классе будет содержаться только то, что используется совместно[17].
Комментарии
Часто комментарии играют роль «дезодоранта» кода, который появляется в нём лишь потому, что код плохой. Почувствовав потребность написать комментарий, попробуйте изменить структуру кода так,
чтобы любые комментарии стали излишними[17].
Если для объяснения действий блока всё же требуется комментарий, попробуйте применить «Выделение метода» (Extract Method);
Если метод уже выделен, но по-прежнему нужен комментарий для объяснения его действия, воспользуйтесь «Переименованием метода» (Rename Method);
Если требуется изложить некоторые правила, касающиеся необходимого состояния системы, примените «Введение утверждения» (Introduce Assertion)[17].
Фаулер М.Глава 3. Код с душком // Рефакторинг. Улучшение существующего кода = Refactoring: Improving the Design of Existing Code / Пер. с англ. С. Маккавеева. — 1-е изд. — СПб.: Символ-Плюс, 2003. — С. 54—62. — 432 с с. — ISBN 5-93286-045-6.
Mantyla M. V., Vanhanen J., Lassenius C.Bad smells-humans as code critics (англ.) // Software Maintenance, 2004. Proceedings. 20th IEEE International Conference on : журнал. — 2004. — P. 399—408. — ISSN1063-6773. Архивировано 1 августа 2014 года.