Рефакторинг запутанных иерархий

Здесь мы рассмотрим в комплексе одну частую проблему, когда при наследовании классов возникают запутанные иерархии.

Что нам говорит Фаулер? править

Вначале напомним ряд взаимосвязанных причин проведения рефакторинга о которых писал М. Фаулер [1]

Расходящиеся модификации править

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

«Стрельба дробью» править

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

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

Посредник править

«Одной из главных характеристик объектов является инкапсуляция - сокрытие внутренних деталей от внешнего мира. Инкапсуляции часто сопутствует делегирование. К примеру, вы договариваетесь с директором о встрече. Он делегирует это послание своему календарю и дает вам ответ. Все хорошо и правильно. Совершенно не важно, использует ли директор календарь-ежедневник, электронное устройство или своего секретаря, чтобы вести учет личных встреч. Однако это может завести слишком далеко. Мы смотрим на интерфейс класса и обнаруживаем, что половина методов делегирует обработку другому классу. Тут надо воспользоваться «Удалением посредника» и общаться с объектом, который действительно знает, что происходит.»

Завистливые функции править

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

К счастью, лечение здесь очевидно: метод явно напрашивается на перевод в другое место. Иногда завистью страдает только часть метода; в таком случае к завистливому фрагменту применяется «Выделение метода». Иногда метод использует функции нескольких классов, так в который из них его лучше поместить? На практике мы определяем, в каком классе находится больше всего данных, и помещаем метод вместе с этими данными. Иногда легче с помощью «Выделения метода» разбить метод на несколько частей и поместить их в разные места.

Есть несколько сложных схем, нарушающих это правило. Фундаментальное практическое правило гласит: то, что изменяется одновременно, надо хранить в одном месте. Данные и функции, использующие эти данные, обычно изменяются вместе, но бывают исключения. Наталкиваясь на такие исключения, мы перемещаем функции, чтобы изменения осуществлялись в одном месте.»

Параллельные иерархии наследования править

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

Разделение наследования править

«Разделение наследования имеет дело с запутанной иерархией наследования, в которой различные варианты представляются объединенными вместе так, что это сбивает с толку. Есть иерархия наследования, которая выполняет одновременно две задачи. Создайте две иерархии и используйте делегирование для вызова одной из другой.

Наследование .. способствует написанию чрезвычайно «сжатого» кода в подклассах. Отдельные методы могут приобретать непропорциональное своим размерам значение благодаря своему местоположению в иерархии.

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

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

Техника разделения править

«Выделите различные задачи, выполняемые иерархией. Создайте двумерную сетку (или трехмерную, или четырехмерную) и пометьте оси разными задачами. Предполагается, что более чем для двух измерений данный рефакторинг надо применять многократно (по одному за раз). Определите, какая задача важнее и должна быть сохранена в текущей иерархии, а какую надо переместить в другую иерархию.»

Какие паттерны имеют отношение к решению проблемы "Разделения наследования" править

Для полноценного понимания от читателя требуется знание структурных паттернов и паттернов поведения. С ними можно ознакомится по книге "Приемы объектно-ориентированного проектирования. Паттерны проектирования" [2]. Но наибольшее внимание нужно уделить описанным ниже. Здесь не будет описано что это такое, но так как они все очень сходны между собой, будет напомнено, чем они похожи и чем различаются.

Структурные паттерны править

  • Адаптер - преобразует интерфейс одного класса в интерфейс другого, который ожидают клиенты. Адаптер обеспечивает совместную работу классов с несовместимыми интерфейсами, которая без него была бы невозможна.
  • Фасад - предоставляет унифицированный интерфейс вместо набора интерфейсов некоторой подсистемы. Фасад определяет интерфейс более высокого уровня, который упрощает использование подсистемы.
  • Мост (альтернативное название Описатель/Тело) - отделяет абстракцию от ее реализации так, чтобы то и другое можно было изменять независимо.

Вот что пишет "Банда четырех":

«Структурные паттерны похожи между собой. Объяснение таково: все структурные паттерны основаны на небольшом множестве языковых механизмов структурирования кода и объектов (наследовании для паттернов уровня класса и композиции [агрегации] для паттернов уровня объектов). Но имеющееся сходство может быть обманчиво, ибо с помощью разных паттернов можно решать совершенно разные задачи.

У паттернов адаптер и мост есть несколько общих атрибутов. Тот и другой повышают гибкость, вводя дополнительный уровень косвенности при обращении к другому объекту. Оба перенаправляют запросы другому объекту, используя иной интерфейс.

Основное различие между адаптером и мостом в их назначении. Цель первого - устранить несовместимость между двумя существующими интерфейсами. При разработке адаптера не учитывается, как эти интерфейсы реализованы и то, как они могут независимо развиваться в будущем. Он должен лишь обеспечить совместную работу двух независимо разработанных классов, так чтобы ни один из них не пришлось переделывать. С другой стороны, мост связывает абстракцию с ее, возможно, многочисленными реализациями. Данный паттерн предоставляет клиентам стабильный интерфейс, позволяя в то же время изменять классы, которые его реализуют. Мост также подстраивается под новые реализации, появляющиеся в процессе развития системы.

В связи с описанными различиями адаптер и мост часто используются в разные моменты жизненного цикла системы. Когда выясняется, что два несовместимых класса должны работать вместе, следует обратиться к адаптеру. Тем самым удастся избежать дублирования кода. Заранее такую ситуацию предвидеть нельзя. Наоборот, пользователь моста с самого начала понимает, что у абстракции может быть несколько реализаций и развитие того и другого будет идти независимо. Адаптер обеспечивает работу после того, как нечто спроектировано; мост - до того.

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

Паттерны поведения править

  • Посредник - определяет объект, инкапсулирующий способ взаимодействия множества объектов. Посредник обеспечивает слабую связанность системы, избавляя объекты от необходимости явно ссылаться друг на друга и позволяя тем самым независимо изменять взаимодействия между ними.
  • Стратегия - определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются.
  • Посетитель - описывает операцию, выполняемую с каждым объектом из некоторой структуры. Паттерн посетитель позволяет определить новую операцию, не изменяя классы этих объектов.
  • Наблюдатель - определяет зависимость типа «один ко многим» между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом и автоматически обновляются.

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

Можно подумать, что структурные паттерны - это проектирование на уровне диаграмм классов, т.е. статического представления, а паттерны поведения описываются динамическим представлением, например, диаграммами последовательностей. Но если обратить внимание на выделенные слова, что внимание акцентировано на связях между объектами, то получается и здесь внимание сосредоточенно на статическом представлении. И различия практически нет. Но все же, здесь просто более тонкое различие. Да, в паттернах поведения также важны статические связи между классами/объектами, но зная только это они будут похожи на структурные паттерны, но если углубится и рассмотреть еще технику работы с объектами в методах (динамический аспект взаимодействия), то отличия станут видны.

Теперь посмотрим для чего "Банда четырех" предлагает использовать паттерны поведения, и главное в каких случаях:

«Инкапсуляция вариаций - элемент многих паттернов поведения. Если определенная часть программы подвержена периодическим изменениям, эти паттерны позволяют определить объект для инкапсуляции такого аспекта. Другие части программы, зависящие от данного аспекта, могут кооперироваться с ним. Обычно паттерны поведения определяют абстрактный класс, с помощью которого описывается инкапсулирующий объект. Например: (1) объект стратегия инкапсулирует алгоритм; (2) объект посредник инкапсулирует протокол общения между объектами.»

Примечания править

  1. Рефакторинг, М. Фаулер (2003)
  2. см. w:Design Patterns

Литература править