Статьи

Шаблон декоратора с Java 8

В недавнем посте я описал, как шаблон декоратора спас мой день. Я дал небольшой фрагмент кода, который содержал самый простой способ создания декораторов, но пообещал, что с Java 8 будет более хороший способ.

Вот:

Украшение с помощью Java 8

1
2
3
4
5
6
7
HyperlinkListener listener = this::changeHtmlViewBackgroundColor;
listener = DecoratingHyperlinkListener.from(listener)
    .onHoverMakeVisible(urlLabel)
    .onHoverSetUrlOn(urlLabel)
    .logEvents()
    .decorate(l -> new OnActivateHighlightComponent(l, urlLabel))
    .decorate(OnEnterLogUrl::new);

Я проведу остаток поста, объясняющего, как туда добраться.

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

Для продолжения моего последнего поста он использует Swing HyperlinkListener в качестве основы для оформления. Это дает дополнительное преимущество, заключающееся в простоте, поскольку этот интерфейс не является универсальным и имеет только один метод с одним аргументом (хорошо для лямбда-выражений!).

обзор

Как и другой пост, этот также не пытается учить сам шаблон. (Однако я нашел другое приятное объяснение .) Вместо этого он рекомендует способ реализовать его в Java 8 таким образом, чтобы его стало очень удобно использовать. Таким образом, пост в значительной степени опирается на функции Java 8, особенно методы по умолчанию и лямбда-выражения .

Диаграммы являются просто набросками и опускают много деталей. Более полные легко найти .

ваниль

Декоратор-шаблон

В обычной реализации шаблона есть интерфейс (называемый выше Component ), который будет реализован обычным способом как «обычными» классами, так и всеми декораторами.

Класс абстрактного декоратора

Декораторы обычно наследуют от промежуточного абстрактного базового класса ( AbstractDecorator ), что облегчает реализацию. Он принимает другой компонент в качестве аргумента конструктора и реализует сам интерфейс, перенаправляя все вызовы к нему. Таким образом, поведение декорированного компонента остается неизменным.

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

Создание декораторов

Обычно для создания декораторов не используется специальная техника; просто простые конструкторы. Со сложными декораторами вы можете даже использовать фабрику.

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

Мое предложение меняет способ создания декораторов.

С Java 8

Декоратор-Pattern-Java8

Чтобы использовать все возможности Java 8, я рекомендую добавить специальный интерфейс для всех декораторов, DecoratingComponent . Абстрактный суперкласс для декораторов реализует этот интерфейс, но, как и прежде, содержит только ссылку на Component .

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

Новый интерфейс

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

Относительно следующих примеров кода: Общие были созданы только для этого поста. Те, которые включают слушателей гиперссылок, взяты из демонстрационного приложения . Наиболее примечательным является DecoratingHyperlinkListener ( ссылка на исходный файл ), который расширяет HyperlinkListener Swing.

методы

Сам интерфейс на самом деле довольно прост и состоит из трех типов методов.

адаптер

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

Общий подход будет выглядеть так:

Метод статического адаптера

01
02
03
04
05
06
07
08
09
10
11
static DecoratingComponent from(Component component) {
    DecoratingComponent adapted = new DecoratingComponent() {
        @Override
        public SomeReturn someMethod(SomeArgument argument) {
            return component.someMethod(argument);
        }
 
        // ... more methods here ...
    };
    return adapted;
}

В случае DecoratingHyperlinkListener это намного проще, потому что это функциональный интерфейс, поэтому можно использовать лямбда-выражение:

Метод статического адаптера в «DecoratingHyperlinkListener»

1
2
3
static DecoratingHyperlinkListener from(HyperlinkListener listener) {
    return event -> listener.hyperlinkUpdate(event);
}

Общее украшение

Это основной метод интерфейса:

1
2
3
4
5
6
default DecoratingComponent decorate(
        Function<? super DecoratingComponent, ? extends DecoratingComponent>
            decorator) {
 
    return decorator.apply(this);
}

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

Этот метод может использоваться во всем коде для простого и удобного оформления любого компонента:

Декорирование с помощью ‘DecoratingComponent’

01
02
03
04
05
06
07
08
09
10
11
12
13
Component some = ...;
DecoratingComponent decorated = DecoratingComponent
    // create an instance of 'DecoratingComponent' from the 'Component'
    .from(some)
    // now decorate it
    .decorate(component -> new MyCoolComponentDecorator(component, ...));
 
// if you already have an instance of 'DecoratingComponent', it get's easier
decorated = decorated
    .decorate(component -> new MyBestComponentDecorator(component, ...));
 
// constructor references are even clearer (but cannot always be used)
decorated = decorated.decorate(MyBestComponentDecorator::new);

Бетонные украшения

Вы также можете добавить методы для украшения экземпляров с помощью конкретных декораторов:

Конкретные украшения в ‘DecoratingHyperlinkListener’

1
2
3
4
5
6
7
default DecoratingHyperlinkListener logEvents() {
    return LogEventsToConsole.decorate(this);
}
 
default DecoratingHyperlinkListener onHoverMakeVisible(JComponent component) {
    return OnHoverMakeComponentVisible.decorate(this, component);
}

Они делают украшения очень лаконичными и читаемыми:

Декорирование с помощью ‘DecoratingComponent’

1
2
DecoratingComponent decorated = ...
decorated = decorated.logEvents();

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

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

Почему дополнительный интерфейс?

Да, да, все эти функции Java 8 очень хороши, но не могли бы вы просто добавить эти методы в AbstractDecorator ? Хороший вопрос!

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

Принцип единой ответственности

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

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

Тип иерархии

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

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

Другие различия

Помимо введения нового интерфейса, эта вариация шаблона не сильно меняется.

Изменения в классе абстрактных декораторов

Если у вас есть доступ к классу, вы должны позволить ему реализовать DecoratingComponent вместо Component . Поскольку новые абстрактные методы не были введены, это не повлечет за собой дальнейших изменений. Это показано на диаграмме UML выше.

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

Общее украшение

1
2
3
4
5
6
7
8
9
// note the more general second type of the 'Function' interface
default DecoratingComponent decorate(
        Function<? super DecoratingComponent, ? extends Component> decorator) {
 
    // create the decorated instance as before
    Component decorated = decorator.apply(this);
    // since it is no 'DecoratingComponent' use 'from' to turn it into one
    return from(decorated);
}

Изменения в декораторах

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

пример

Итак, давайте снова посмотрим на фрагмент сверху:

Украшение с помощью Java 8

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// create a 'HyperlinkListener' with a method reference
HyperlinkListener listener = this::changeHtmlViewBackgroundColor;
// decorate that instance with different behaviors
// (note that each call actually returns a new instance
//  so the result has to be assigned to a variable)
listener = DecoratingHyperlinkListener
    // adapt the 'HyperlinkListener' to be a 'DecoratingHyperlinkListener'
    // (looks better if it is not on its own line)
    .from(listener)
    // call some concrete decorator functions
    .onHoverMakeVisible(urlLabel)
    .onHoverSetUrlOn(urlLabel)
    .logEvents()
    // call the generic decorator function with a lambda expression
    .decorate(l -> new OnActivateHighlightComponent(l, urlLabel))
    // call the generic decorator function with a constructor reference
    .decorate(OnEnterLogUrl::new);

отражение

Мы увидели, как статические и стандартные методы интерфейса Java 8 можно использовать для создания свободного API для шаблона декоратора. Это делает код более кратким и одновременно более читабельным, не мешая механизму шаблона.

Мы использовали методы по умолчанию для создания черт, о которых пишет Брайан Гетц :

Главное, что нужно понять о методах по умолчанию, заключается в том, что основной целью проектирования является развитие интерфейса , а не «превращение интерфейсов в (посредственные) черты»

Извини, Брайан, это было слишком заманчиво. 😉

Получили некоторое представление о шаблоне декоратора? Хотите улучшить мою идею или критиковать ее? Тогда оставьте комментарий! И не забудьте проверить код на GitHub .

Ссылка: Шаблон Decorator With Java 8 от нашего партнера JCG Николая Парлога из блога CodeFx .