Статьи

Как образец декоратора спас мой день

На работе я имею дело с огромной базой кода Java, которая была разработана более 15 лет многими разными разработчиками. Не все было сделано по книгам, но в то же время у меня обычно нет возможности рефакторировать каждую странность, с которой я сталкиваюсь.

Тем не менее, шаги по повышению качества кода можно предпринимать каждый день. И сегодня было именно так …

обзор

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

Ситуация

Наш пользовательский интерфейс содержит JEditorPanes Swing, которые используются для отображения HTML. Взаимодействие с различными ссылками (например, зависание и нажатие) вызывает один или несколько из следующих ответов:

  1. регистрация события
  2. изменение курсора (то, что JEditorPane уже делает самостоятельно; похоже, с мая 2000 года — что за… ?!)
  3. обновление панели со связанным контентом
  4. открытие внешнего браузера
  5. открытие внешнего приложения
  6. обработка внутреннего запроса на обслуживание

Эти ответы не одинаковы для всех панелей. Есть несколько из них с частично разными потребностями. (Если вы знаете шаблон декоратора, вы видите, куда это идет.)

Итак, вопрос: как вы реализуете эти ответы?

Решение с одним настраиваемым классом

Вы можете просто объединить все это вместе в одном классе, который реализует HyperlinkListener и (де) активировать различные ответы с флагами.

vaskas_complex_allinone

Этот класс был бы адом! Да, черт возьми. Это так просто.

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

(Кстати, основной причиной беспорядка является то, что AllInOneHyperlinkListener нарушает принцип единой ответственности . Я не буду подробно останавливаться на этом, поскольку этот пост уже достаточно длинный.)

Решение с наследованием

В любом случае, мне повезло, что я не столкнулся с одним классом слушателей-бегемотов. Вместо этого я нашел небольшую иерархию классов, которые разделяют эти обязанности между ними ( HL — сокращение от HyperlinkListener ):

  1. CursorSettingHL implements HL : регистрирует события и устанавливает курсор
  2. UrlProcessingHL extends CursorSettingHL :
    обрабатывает URL, обновляя содержимое панели или открывая внешний браузер / приложение
  3. ServiceRequestHandlingHL extends UrlProcessingHL : обрабатывает URL, если это запрос службы; в противном случае делегаты своего суперкласса

vaskas_complex_inheritance

Это выглядит лучше, не так ли? Что ж…

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

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

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

Нет, реальная проблема с этим решением — моделируемая гибкость. Похоже, вы можете выбирать, но на самом деле вы не можете. Посмотрите, что происходит, когда все меняется.

Изменение

Мы медленно переходим от Swing к JavaFX, и я хотел заменить JEditorPane на FX ‘ WebView . (На самом деле это немного хлопотно, чтобы получить HyperlinkListeners в WebView, но я вернусь к этому в другом посте.) WebView уже делает некоторые из вышеперечисленных вещей, так что это обновленный список ответов, которые новый слушатель имеет для запуска:

  1. регистрация события
  2. изменение курсора
  3. обновление панели новым контентом
  4. открытие внешнего браузера
  5. открытие внешнего приложения
  6. обработка внутреннего запроса на обслуживание

И тут вся система классов становится бесполезной. (По крайней мере, поскольку я не желаю, чтобы слушатель выполнял действия 2. и 3. для какого-то невидимого контроля.) В этот момент становится очень ясно, что обязанности перепутались. Мне все еще нужны некоторые из них, но не все, и поскольку они не разделены границами класса, я нахожусь в ситуации «все или ничего».

Выкройка декоратора на помощь

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

Образец Декоратора

Как я уже сказал, я не буду вдаваться в подробное объяснение модели, но основная идея заключается в следующем:

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

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

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

В действии

Теперь путь был ясен: я реорганизовал вышеуказанную функциональность в различные декораторы, такие как LoggingHyperlinkListenerDecorator и LoggingHyperlinkListenerDecorator .

vaskas_complex_decorator

Затем я удалил оригинальные классы и заменил их использование правильными комбинациями декораторов. Наконец-то я дошел до своей новой функциональности и выбрал только правильных декораторов. Есть хороший способ сделать это с Java 8, но для простоты давайте просто воспользуемся здесь конструкторами:

Собираем декораторов вместе

01
02
03
04
05
06
07
08
09
10
11
12
13
14
// use a lambda expression to create the initial listener
// which does nothing
HyperlinkListener listener = event -> {};
// these decorators first do their own thing and then call the
// decorated listener (the one handed over during construction);
// in the end, the last added decorator will act first
listener =
    new ExternalApplicationOpeningHyperlinkListenerDecorator(listener);
listener =
    new BrowserOpeningHyperlinkListenerDecorator(listener);
listener =
    new ServiceRequestHandlingHyperlinkListenerDecorator(listener);
listener =
    new LoggingHyperlinkListenerDecorator(listener);

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

vaskas_complex_decorated

Эффект

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

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

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

отражение

Этот пример из реальной жизни показал, как применение шаблона декоратора облегчает чтение, тестирование и изменение кода.

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

Большое спасибо Бендже в Википедии , которая создала прекрасный образ комплекса Васьки и опубликовала его в открытом доступе.

Ссылка: Как шаблон Decorator спас мой день от нашего партнера JCG Николая Парлога в блоге CodeFx .