Признаки плохого кода

Глава из книги “Рефакторинг с использованием шаблонов (паттернов проектирования)”

Автор: Джошуа Кериевски
Источник: Рефакторинг с использованием шаблонов
Материал предоставил: Издательство Вильямс
Опубликовано: 31.10.2006
Версия текста: 1.0
Duplicated Code (Повторяющийся код)
Long Method (Длинный метод)
Conditional Complexity (Сложность условий)
Primitive Obsession (Зацикленность на примитивах)
Indecent Exposure (Неприличная демонстрация)
Solution Sprawl (Расплывшееся решение)
Alternative Classes with Different Interfaces (Альтернативные классы с различными интерфейсами)
Lazy Class (Ленивый класс)
Large Class (Большой класс)
Switch Statements (Операторы выбора вариантов)
Combinatorial Explosion (Комбинаторный взрыв)
Oddball Solution (Оригинальное решение)

Когда вы научитесь относиться к своим словам с необходимой степенью беспристрастности, то заметите, что, перечитывая отрывок пять-шесть раз подряд, вы все время обнаруживаете новые проблемные места [5, с. 229].

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

Наиболее распространенные проблемы проектов возникают в следующих случаях:

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

В главе "Bad Smells in Code" (Признаки плохого кода) из книги Refactoring [15] Мартин Фаулер (Martin Fowler) и Кент Бек (Kent Beck) представляют дополнительное руководство по определению проблем в проектах. Уподобляя проблемы проектов не очень приятным запахам , они объясняют, какие рефакторинги или их комбинации лучше сработают, чтобы освежить проект.

Признаки плохого кода Фаулера и Бека нацелены на проблемы, которые возникают повсеместно: в методах, классах, иерархиях, пакетах (именованных областях видимости, модулях) и целых системах. Такие их названия проблем, как функциональная зависть (Feature Envy), зацикленность на примитивах (Primitive Obsession) и гипотетическая всеобщность (Speculative Generality), предоставляют программистам богатый и красочный словарь для быстрого сообщения о проблемах в проекте.

Я решил, что будет полезным разобраться, к каким из 22 признаков кода Фаулера и Бека адресованы представленные мной в этой книге рефакторинги. Завершая эту работу, я обнаружил пять новых проблем кода, которые наводят на мысль о необходимости направляемых шаблонами рефакторингов. В целом рефакторинги в этой книге адресованы 12 признакам кода.

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

ПРИЗНАК*РЕФАКТОРИНГ
Duplicated Code (Повторяющийся код) [15]Form Template Method
Introduce Polymorphic Creation with Factory Method
Chain Constructors
Replace One/Many Distinctions with Composite
Extract Composite
Unify Interfaces with Adapter
Introduce Null Object
Long Method (Длинный метод) [15]Compose Method
Move Accumulation to Collecting Parameter
Replace Conditional Dispatcher with Command
Move Accumulation to Visitor
Replace Conditional Logic with Strategy
Conditional Complexity (Сложность условий)Replace Conditional Logic with Strategy
Move Embellishment to Decorator
Replace State-Altering Conditionals with State
Introduce Null Object
Primitive Obsession (Зацикленность на примитивах) [15]Replace Type Code with Class
Replace State-Altering Conditionals with State
Replace Conditional Logic with Strategy
Replace Implicit Tree with Composite
Replace Implicit Language with Interpreter
Move Embellishment to Decorator
Encapsulate Composite with Builder
Indecent Exposure (Неприличная демонстрация)Encapsulate Classes with Factory
Solution Sprawl (Расплывшееся решение)Move Creation Knowledge to Factory
Alternative Classes with Different Interfaces (Альтернативные классы с различными интерфейсами) [15]Unify Interfaces with Adapter
Lazy Class (Ленивый класс) [15]Inline Singleton
Large Class (Большой класс) [15]Replace Conditional Dispatcher with Command
Replace State-Altering Conditionals with State
Replace Implicit Language with Interpreter
Switch Statements (Операторы выбора вариантов) [15]Replace Conditional Dispatcher with Command
Move Accumulation to Visitor
Combinatorial Explosion (Комбинаторный взрыв)Replace Implicit Language with Interpreter
Oddball Solution (Оригинальное решение)Unify Interfaces with Adapter
Таблица 4.1

* Номера страниц указывают страницы книги, где обсуждается данный признак, а ссылка на [15] означает, что признак обсуждается также в главе "Bad Smells in Code" книги Фаулера и Бека Refactoring.

Duplicated Code (Повторяющийся код)

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

Часто явное и/или слабо выраженное дублирование в подклассах иерархии можно удалить, применяя Form Template Method (с. 143). Если метод в разных подклассах реализован одинаково, за исключением шага создания объекта, то применение Introduce Polymorphic Creation with Factory Method (с. 60) подготовит путь к удалению остального дублирования посредством Template Method.

Если повторяющийся код содержат конструкторы класса, такое дублирование часто можно устранить применением Chain Constructors (с. 235).

Если имеется отдельный код для обработки одиночного объекта или коллекции объектов, то, применяя Replace One/Many Distinctions with Composite (с. 155), возможно, удастся удалить дублирование.

Если каждый из подклассов иерархии реализует свой собственный Composite, эти реализации могут оказаться идентичными и в этом случае можно использовать Extract Composite (с. 149).

Если объекты обрабатываются по-разному только потому, что имеют различные интерфейсы, то применение Unify Interfaces with Adapter (с. 172) подготовит почву для удаления повторяющейся логики обработки.

Если условная логика имеет дело с нулевым объектом и дублируется по всей системе, то применение Introduce Null Object (с. 210) устранит дублирование и упростит систему.

Long Method (Длинный метод)

В своем описании этого признака Фаулер и Бек [15] приводят несколько важных причин, по которым короткие методы лучше длинных. Основная причина связана с распределением логики. Два длинных метода легко могут содержать повторяющийся код. Всего лишь разбив эти методы на более мелкие, зачастую можно найти для них способы совместно использовать единую логику.

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

Каков предпочтительный размер небольшого метода? Я бы сказал, что до 10 строк кода, хотя большинство методов используют от одной до пяти строк кода. Если подавляющее большинство методов в системе имеют небольшой размер, то несколько методов могут быть и побольше - конечно, не в ущерб пониманию и при условии отсутствия дублирования.

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

Когда я сталкиваюсь с длинным методом, сразу же возникает желание разделить его на составные методы [6], применяя рефакторинг Compose Method (с. 85). Обычно при этом применяется также Extract Method [15]. Если код, который вы преобразуете в составные методы, накапливает информацию в общей переменной, подумайте о применении рефакторинга Move Accumulation to Collecting Parameter (с. 217).

Если метод длинный из-за того, что содержит большой оператор выбора вариантов (switch statement) для диспетчеризации и обработки запросов, его можно сократить, используя Replace Conditional Dispatcher with Command (с. 134).

Если для сбора данных от многочисленных классов с различными интерфейсами используется оператор выбора вариантов, размер метода можно сократить, применяя рефакторинг Move Accumulation to Visitor (с. 222).

Если метод длинный, поскольку содержит несколько версий алгоритма и условную логику, которая определяет версию для использования во время выполнения, то размер метода можно сократить, применяя Replace Conditional Logic with Strategy (с. 88).

Conditional Complexity (Сложность условий)

В начале работы условная логика проста для понимания и состоит не более чем из нескольких строк кода. Но, к сожалению, она редко остается таковой и далее. Например, вы реализуете несколько новых функциональных возможностей, и ваша условная логика тут же становится обширной и трудной для понимания. Несколько рефакторингов в книге Refactoring [15] и нашем каталоге предназначены для решения таких проблем.

Если условная логика выбирает, какой из нескольких вариантов вычислений выполнять, то можно рассмотреть возможность применения Replace Conditional Logic with Strategy (с. 88).

Если условная логика контролирует, какой из нескольких фрагментов поведения должен быть выполнен в дополнение к основному поведению класса в каждом конкретном случае, возможно, необходимо использовать Move Embellishment to Decorator (с. 99).

Если условные выражения, управляющие сменой состояний объекта, сложны, подумайте над упрощением логики путем применения Replace State-Altering Conditionals with State (с. 115).

Обработка нулевых значений часто приводит к созданию условной логики. Если одна и та же условная логика обработки нулевых значений дублируется повсюду в системе, ситуацию можно улучшить с помощью Introduce Null Object (с. 210).

Primitive Obsession (Зацикленность на примитивах)

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

Фаулер и Бек [15] объясняют, как проявляется зацикленность на примитивах, когда код слишком зависит от них. Обычно это происходит, когда еще не видно, как абстракция более высокого уровня может прояснить и упростить код. Рефакторинги Фаулера включают многие из наиболее распространенных решений этой проблемы. Наша книга основана на этих решениях, но наряду с ними предлагает и другие.

Если значение примитива управляет логикой класса и не является безопасным с точки зрения типов (т.е. клиенты могут присваивать ему небезопасные или некорректные значения), рассмотрите применение Replace Type Code with Class (с. 200). Результатом будет код, безопасный с точки зрения типов и допускающий расширение новым поведением (чем-то, что невозможно сделать с помощью примитивов).

Если переходом между состояниями объекта управляет сложная условная логика, использующая значения примитивов, можно применить Replace State-Altering Conditionals with State (с. 115). Результатом будут многочисленные классы, представляющие каждое состояние и упрощающие логику переходов.

Если сложная условная логика управляет тем, какой алгоритм запускать, и основывается на значениях примитивов, рассмотрите применение Replace Conditional Logic with Strategy (с. 88).

Если вы неявно создаете древовидную структуру с использованием примитивного представления, такого, как строка, ваш код для работы с ним может быть сложен, подвержен ошибкам и/или полон повторов. Сократить количество таких проблем может применение Replace Implicit Tree with Composite (с. 125).

Если множество методов класса существуют для поддержания многочисленных комбинаций значений примитивов, то, возможно, это неявный язык. Если так, рассмотрите применение Replace Implicit Language with Interpreter (с. 187).

Если значения примитивов существуют в классе только для обеспечения дополнений к основной ответственности класса, возможно, следует использовать Move Embellishment to Decorator (с. 99).

В заключение, даже если у вас есть класс, он может быть все же слишком примитивным, чтобы облегчить жизнь клиентам. Это возможно в случае, когда у вас есть реализация Composite [12], работа с которым весьма сложна. Способ построения Composite для клиентов можно упростить, применив Encapsulate Composite with Builder (с. 65).

Indecent Exposure (Неприличная демонстрация)

Этот признак указывает на недостаток того, что Дэвид Парнас (David Parnas) так превосходно назвал "сокрытием информации" [21]. Подобная проблема возникает, когда методы или классы, которые должны быть невидимы для клиентов, открыты для них и клиенты знают о коде, который неважен или важен только косвенно. Разумеется, это усложняет проект.

Рефакторинг Encapsulate Classes with Factory (с. 54) устраняет этот признак. Не каждый класс, полезный клиентам, должен быть открытым (т.е. иметь открытый конструктор). К некоторым классам следует обращаться только через их общие интерфейсы. Вы можете добиться этого, сделав конструкторы закрытыми и используя фабрику для создания экземпляров.

Solution Sprawl (Расплывшееся решение)

Когда код и/или данные, реализующие ответственность, расползаются по многочисленным классам, мы сталкиваемся с признаком Solution Sprawl. Этот признак часто является результатом того, что к системе быстро добавляется функциональная возможность, но, поскольку не выделяется достаточно времени для того, чтобы упростить и объединить проект, он не может оптимально соответствовать этой возможности.

Расплывшееся решение - это брат-близнец признака "Хирургии при помощи дробовика" (Shotgun Surgery), описанного Фаулером и Беком [15]. С этим признаком вы сталкиваетесь, когда добавление или изменение функций системы служит причиной изменения слишком многих фрагментов кода. Оба признака связаны с одной проблемой, хотя обнаруживаются по-разному. Мы обнаруживаем расплывшееся решение при знакомстве с кодом, в то время как с "хирургией при помощи дробовика" мы сталкиваемся, осуществляя ее.

Move Creation Knowledge to Factory (с. 46) - рефакторинг, который решает проблему расплывшейся ответственности за создание объекта.

Alternative Classes with Different Interfaces (Альтернативные классы с различными интерфейсами)

По Фаулеру и Беку [15] эта проблема возникает в коде, когда интерфейсы двух классов различны, а классы все еще достаточно похожи. Если между двумя классами можно обнаружить сходство, то часто можно провести рефакторинг классов с тем, чтобы они использовали общий интерфейс.

Однако иногда нельзя непосредственно изменить интерфейс класса, поскольку у вас нет возможности управления кодом. Типичный пример - работа с библиотекой стороннего производителя. В этом случае для создания общего для двух классов интерфейса можно применить Unify Interfaces with Adapter (с. 172).

Lazy Class (Ленивый класс)

Описывая этот признак, Фаулер и Бек отмечают: "Класс, который не делает достаточно для того, чтобы себя окупить, должен быть ликвидирован" [15, 83]. Неудивительно столкнуться с не окупающим себя синглтоном [12]. Более того, он может не только не окупаться, но еще и дорого вам обходиться, внося дополнительные зависимости в ваш проект, подобно глобальным данным. Рефакторинг Inline Singleton (с. 78) объясняет быструю гуманную процедуру для ликвидации Singleton.

Large Class (Большой класс)

Фаулер и Бек [15] замечают, что наличие слишком многих переменных в экземпляре класса обычно указывает на то, что класс пытается делать слишком много. Вообще большие классы обычно содержат слишком много ответственности. Методы Extract Class и Extract Subclass [15] - одни из главных рефакторингов, обычно рекомендуемых для решения этой проблемы, - помогают перенести ответственность в другие классы. Направляемые шаблонами рефакторинги этой книги пользуются ими, чтобы уменьшить размеры классов.

Рефакторинг Replace Conditional Dispatcher with Command (с. 134) выделяет поведение в классы Command [12], что может значительно уменьшить размер класса, который предоставляет разное поведение в ответ на различные запросы.

Рефакторинг Replace State-Altering Conditionals with State (с. 115) может сократить большой класс, заполненный с кодом перехода между состояниями, до небольшого класса, который делегирует часть своей ответственности семейству классов State [12].

Рефакторинг Replace Implicit Language with Interpreter (с. 187) может уменьшить большой класс до небольшого путем трансформации обширного кода, эмулирующего язык, в небольшой Interpreter [12].

Switch Statements (Операторы выбора вариантов)

Операторы выбора вариантов (или эквивалентные им структуры if:elseif:elseif:) по своей сути не плохи. Они становятся плохими только тогда, когда делают проект более запутанным или менее гибким, чем это необходимо. В данном случае лучше всего проводить рефакторинг, уходя от таких операторов выбора вариантов к более объектно-ориентированным или полиморфным решениям.

Рефакторинг Replace Conditional Dispatcher with Command (с. 134) описывает, как разбить большой оператор выбора вариантов на набор объектов Command [12], каждый из которых может быть найден и вызван без использования при этом условной логики.

Рефакторинг Move Accumulation to Visitor (с. 222) описывает пример, в котором операторы выбора вариантов используются для получения данных от экземпляров классов с различными интерфейсами. После рефакторинга кода к Visitor [12] никакая условная логика уже не нужна и проект становится более гибким.

Combinatorial Explosion (Комбинаторный взрыв)

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

Пусть, например, в классе есть множество методов для выполнения запросов. Каждый из этих методов выполняет запрос, используя особые условия и данные. Чем больше специализированных запросов необходимо поддерживать, тем больше методов для запросов придется создать. Очень скоро произойдет взрывное увеличение числа методов, управляющих множеством способов выполнения запросов. У вас также есть неявный язык запросов. Можно убрать все эти методы и проблему комбинаторного взрыва, применяя Replace Implicit Language with Interpreter (с. 187).

Oddball Solution (Оригинальное решение)

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

Чтобы удалить такое дублирование, сначала определите предпочтительное решение. В некоторых случаях предпочтительным может оказаться менее используемое решение, если оно лучше решения, применявшегося большую часть времени. После определения предпочтительного решения зачастую можно применить Substitute Algorithm [15], чтобы распространить выбранное решение на всю систему. При наличии согласованного решения вы можете переместить все его экземпляры в одно место и таким образом избавиться от дублирования.

Признак плохого кода Oddball Solution обычно присутствует тогда, когда существует предпочтительный способ коммуникации с множеством классов, однако различия в интерфейсах мешают общаться с ними единым согласованным образом. В этом случае рассмотрите применение Unify Interfaces with Adapter (с. 172) для построения общего интерфейса, через который можно единообразно поддерживать связь со всеми классами. Сделав это, можно обнаружить способы удаления повторяющейся логики обработки.


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