воскресенье, 15 января 2012 г.

Использование @Decorator

Хочу поделиться недавним открытием: использование в j2ee приложениях механизма Декораторов из JSR 299 действительно очень удобно и, на мой вкус, хорошо реализовано с точки зрения интерфейса.

Что такое Декоратор?
Это один из паттернов (шаблонов) проектирования, предназначенный для динамического подключения дополнительного поведения к объекту. Шаблон Декоратор предоставляет гибкую альтернативу практике создания подклассов с целью расширения функциональности. Определение честно скопировал из Википедии.
Попытаюсь пояснить, чем может быть плохо наследование. Пусть есть у нас иерархия классов, формирующих строку приветствия. Суперкласс пишет просто "Здравствуйте", следующий наследник добавляет к этому приветствию имя пользователя, следующий дополнительно поздравляет с новым годом, следующий напоминает, что надо заплатить за интернет/сходить на выборы. Думаю, идея ясна. Но вот прошёл новый год, поздравлять с ним уже не надо. Но класс, напоминающий об оплате за интернет, отнаследован от него... Хотелось бы иметь механизм, позволяющий конфигурировать, какое дополнительное поведение нужно добавить, а какое нет. Именно это и представляет собой Декоратор: множество обёрток, которые можно использовать в любой комбинации. Стандарт JSR 299 определяет правила, по которым реализован механизм Декораторов в javaEE.

Знакомимся ближе
Для меня всегда наиболее понятной формой объяснения был пример, поэтому и в данной заметке покажу использование Декоратора на примере. Допустим, необходимо уметь формировать приветственные сообщения для пользователя при входе на некоторый сайт. Только учтите, что пример надуманный и в обычных условиях его можно (и нужно) реализовать проще.
Итак, интерфейс, определяющий способность формировать приветственное сообщение и сервис, его реализующий:
public interface Greeting {
    
    String helloString();    
}
@Stateless
public class GreetingService implements Greeting {

    @Override
    public String helloString() {
        return "Здравствуйте!";
    }    
}
Ну в этом коде ничего необычного и заслуживающего упоминания нет. Теперь к нему создадим модель (backing bean), стоящую за веб-страницей:
@SessionScoped
@Named
public class GreetingBean implements Serializable {
    
    @Inject
    private transient Greeting greetingService;
    
    public String getHelloString() {
        return greetingService.helloString();
    }    
}
На самой странице вызовем метод приветствия из модели getHelloString. Саму страницу мне сгенерировал netbeans:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    
        Facelet Title
    
    
        #{greetingBean.helloString}
    
</html>
Всё, мы получили работающий код, выводящий "Здравствуйте!" при заходе на страницу. Теперь нужно решить задачу кастомизации приветствия:
  • нужно поздравлять с новым годом;
  • нужно сообщать о том, что вышел новый плагин для более комфортной работы с сайтом.


Приступим к декорированию приветствия?
@Decorator
public class NewYearGreeting implements Greeting {
    
    @Inject @Delegate
    private Greeting greeting;

    @Override
    public String helloString() {
        return greeting.helloString() + " С новым годом!";
    }    
}
@Decorator
public class ReminderGreeting implements Greeting {
    
    @Inject @Delegate
    private Greeting greeting;

    @Override
    public String helloString() {
        return greeting.helloString() + " Не забудьте обновить ваш плагин!";
    }    
}
Это два класс-обёртки (декоратора) для изменения поведения сервиса приветствия.
  • @Decorator - определяет, что создаваемые классы являются Декораторами.
  • implements Greeting - определяет, поведение какого именно интерфейса эти декораторы могут изменять.
  • @Inject @Delegate private Greeting greeting - сюда с помощью dependency injection будет подставлена какая-то имплементация интерфейса Greeting. Правила такие же как везде: допускается @Any и создание своих квалификаторов (@Qualifier) для определения конкретной реализации, если их несколько.


По умолчанию, механизм декораторов выключен. Для включения необходимо отредактировать файл beans.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
           
           
                ru.vrn.vrk.service.ReminderGreeting   
                ru.vrn.vrk.service.NewYearGreeting                
           
</beans>
При этом порядок, в котором перечислены декораторы важен: именно в таком порядке они и будут применяться, т.е. сначала ReminderGreeting, а затем NewYearGreeting. Собираем приложение ещё раз и видим при входе:
> Здравствуйте! С новым годом! Не забудьте обновить ваш плагин!
Признаться, я был удивлён, т.к. ожидал иного порядка фраз. Оказалось, ReminderGreeting действительно применяется первым. Но в процессе выполнения его метода helloString() идёт обращение к Greeting, и теперь контейнер применяет следующий декоратор NewYearGreeting. Порядок вызовов можно описать так:
  • GreetingBean.getHelloString: ReminderGreeting.helloString()
  • ReminderGreeting.helloString: NewYearGreeting.helloString() + " Не забудьте обновить ваш плагин!"
  • NewYearGreeting.helloString: GreetingService.helloString() + " С новым годом!"
  • GreetingService.helloString: "Здравствуйте!"
Это несложно проверить, если в helloString() декораторов прописать вывод имени класса в лог, а уж затем возврата строки-приветствия.

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