Код с запашком
Код с запашко́м (код с душко́м, дурно пахнущий код) — термин, обозначающий код с признаками (запахами) проблем в системе. Был введён Кентом Беком[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].
- Вынести все изменения в один класс позволят «Перемещение метода» (Move Method) и «Перемещение поля» (Move Field);
- Если нет подходящего класса, то следует создать новый класс;
- Если это необходимо, следует воспользоваться «Встраиванием класса» (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 и поместить его в нужный класс;
- После настройки структуры наследования следует использовать «Замену условного оператора полиморфизмом» (Replace Conditional with Polymorphism)[3].
Параллельные иерархии наследования
[править | править код]В коде с таким запашком всякий раз при порождении подкласса одного из классов приходится создавать подкласс другого класса[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].
См. также
[править | править код]Примечания
[править | править код]- ↑ 1 2 3 Martin, 1999.
- ↑ 1 2 Vigorous Hive_CodeSmell.
- ↑ 1 2 3 4 5 6 7 8 9 10 11 Дурно пахнущий код.
- ↑ Counsell_Code Smells, 2010.
- ↑ devconf.
- ↑ 1 2 3 Мартин Фаулер_Рефакторинг, 2003, с. 54.
- ↑ 1 2 3 Мартин Фаулер_Рефакторинг, 2003, с. 55.
- ↑ 1 2 3 4 Мартин Фаулер_Рефакторинг, 2003, с. 56.
- ↑ Мартин Фаулер_Рефакторинг, 2003, с. 56—57.
- ↑ 1 2 3 4 Мартин Фаулер_Рефакторинг, 2003, с. 57.
- ↑ Дурно пахнущий код, с. 57.
- ↑ 1 2 3 4 5 Мартин Фаулер_Рефакторинг, 2003, с. 58.
- ↑ 1 2 3 4 5 Мартин Фаулер_Рефакторинг, 2003, с. 59.
- ↑ Временное поле.
- ↑ 1 2 3 4 Мартин Фаулер_Рефакторинг, 2003, с. 60.
- ↑ 1 2 3 4 Рефакторинг кода.
- ↑ 1 2 3 Мартин Фаулер_Рефакторинг, 2003, с. 61.
Литература
[править | править код]- Martin Fowler. Refactoring. Improving the Design of Existing Code (англ.). — Addison-Wesley, 1999. — ISBN 0-201-48567-2.
- Фаулер М. Глава 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. — ISSN 1063-6773. Архивировано 1 августа 2014 года.
- S. Counsell, R. M. Hierons, H. Hamza, S. Black, and M. Durrand. Exploring the Eradication of Code Smells: An Empirical and Theoretical Perspective (англ.) // Advances in Software Engineering Volume 2010 : журнал. — 2010.
Ссылки
[править | править код]- Рефакторинг кода . Codingcraft.ru. Дата обращения: 6 ноября 2013.
- Временное поле (Temporary Field) . Codingcraft.ru. Дата обращения: 5 ноября 2013.
- 2 признака кода с душком: убей его и лови всё молча . Glan-saratov.ru. Дата обращения: 22 сентября 2013. Архивировано из оригинала 12 августа 2012 года.
- CodeSmell (англ.). Martinfowler.com. Дата обращения: 13 октября 2013.
- Запахи плохого кода . Vihv.org. Дата обращения: 13 октября 2013. Архивировано из оригинала 9 декабря 2013 года.
- Экстремальное программирование по-русски, дурно пахнущий код . xp.1024.info. Дата обращения: 26 октября 2013.
- Code Smell (англ.). Cunningham & Cunningham, Inc. (c2.com). Дата обращения: 23 ноября 2013.
- Андрей Вокин. Рефакторинг и анализ Ruby и Rails кода . DevConf. devconf.ru (9 июня 2012). Дата обращения: 22 декабря 2013. Архивировано из оригинала 24 декабря 2013 года.