Компонентные фреймворки позволяют быстро стоить приложения, используя готовые строительные блоки — компоненты. Они позволяют быстро строить приложения малой и средней сложности, но при этом очень сложно создавать большие, гибкие и настраиваемые приложения. Также по мере развития приложения его всё труднее и труднее адаптировать под новые требования клиентов. Задача этой статьи выяснить причины этих проблем и найти подходящее решение.
Компоненты — строительные блоки приложения
Компоненты являются основным средством расширения функциональных возможностей приложения. Они предназначены для многократного использования, имеют определённый интерфейс и взаимодействуют с программной средой посредством событий. Процесс создания компонентов несколько отличается от создания приложения на основе них. Компонент не только должен содержать полезный функционал, но и быть изначально спроектирован для повторного использования.
Повторное использование компонентов
Чтобы компоненты можно было повторно использовать, они должны быть спроектированы в соответствии с принципом слабого связывания. Для этого во многих фреймворках реализуется событийная модель на основе шаблона Обозреватель (Observer). Он позволяет подписываться нескольким получателям на одно и то же событие.
Обозреватель — это своего рода посредник, который хранит в себе список получателей. Когда происходит событие внутри компонента, то оно отправляется всем получателям по этому списку.
Благодаря посреднику компонент не знает о своих получателях. И получатели могут подписываться на события от разных компонентов определённого типа.
Использовать компоненты легче, чем их создавать
Используя компоненты можно быстро создавать разные формы, панели, окна и другие составные элементы интерфейса. Однако чтобы можно было повторно использовать новые составные элементы, нужно превратить в компоненты.
Какой ценой это достигается? Для этого нужно определить, какие внешние события будет генерировать компонент, и уметь пользоваться механизмом отправки сообщений.
Т.е. нужно создать, как минимум, новые классы событий и определить интерфейсы или callback методы получения этих событий. Такой подход добавляет сложности в реализации повторно используемых элементов приложения (форм, панелей, окон, страниц). Хорошо, если в системе с десяток составных элементов. Такому подходу ещё можно как-то следовать. Но что если система состоит из сотен элементов?
Если не следовать этому подходу, то это приведёт к сильному связыванию между элементами системы и сведёт к нулю шансы на их повторное использование. А это, в свою очередь, повлечёт за собой дублирование кода, что усложнит поддержку в дальнейшем и приведёт к росту ошибок в системе.
Проблема усугубляется ещё тем, что пользователи компонентов зачастую не знают, как определять и отправлять новые события для своих составных элементов. Но при этом они с лёгкостью могут пользоваться уже готовыми событиями, определёнными в компонентном фреймворке. Т.е. они умеют и знают, как получать события, а не как создавать и отправлять их.
Чтобы решить эту проблему, давайте рассмотрим, как можно упростить использование событийной модели в приложении.
Слишком много Event Listeners
В Java Swing, GWT, JSF, Vaadin используется шаблон Observer для реализации событийной модели. Где на одно событие могут подписаться множество получателей. Реализацией здесь служит список, в который добавляются Event Listeners. Когда происходит событие, то оно отправляется всем получателям из этого списка. Каждый компонент создаёт свой набор Event Listeners для одного или нескольких событий.
Это ведёт к увеличению количества классов в приложении. Что, в свою очередь, усложняет поддержку и развитие системы.
Например, в Java, такое положение вещей было до появления аннотаций. С аннотациями стало возможным подписывать методы на определённые события. Примером может служить реализация событийной модели в CDI (Contexts and Dependency Injection) из Java EE 6.
public class PaymentHandler {
public void creditPayment(@Observes @Credit PaymentEvent event) {
...
}
}
public class PaymentBean {
@Inject
@Credit
Event<PaymentEvent> creditEvent;
public String pay() {
PaymentEvent creditPayload = new PaymentEvent();
// populate payload ...
creditEvent.fire(creditPayload);
}
}
А также реализация Event Bus в Guava Libraries:
// Class is typically registered by the container.
class EventBusChangeRecorder {
@Subscribe public void recordCustomerChange(ChangeEvent e) {
recordChange(e.getChange());
}
}
// somewhere during initialization
eventBus.register(new EventBusChangeRecorder());
// much later
public void changeCustomer() {
ChangeEvent event = getChangeEvent();
eventBus.post(event);
}
Как результат — нет необходимости реализовывать множество Event Listeners для своих компонентов. Использовать события в приложении стало намного проще.
Использование Event Bus удобно, когда компоненты приложения одновременно размещаются на экране и обмениваются сообщениями через него, как показано на рисунке ниже.
Заголовок, меню слева, содержание посередине, панель справа, все эти элементы с помощью использования EventBus автоматические превращаются в повторно используемые компоненты. Т.к. они не зависят напрямую друг от друга, а использую общую шину для коммуникаций.
Подписался на события — не забудь отписаться!
Заменив Event Listeners аннотациями – был сделан большой шаг вперед, чтобы упростить использование событийной модели.
Однако чтобы компонент мог получать события из Event Bus, его нужно зарегистрировать. И чтобы он перестал получать события, то удалить его из списка получателей. Кто должен взять на себя эти обязанности?
Также подписываться на его события и, что более важно, в нужный момент отписываться от них. Вполне возможна ситуация, когда на одно и то же событие несколько раз подписывается один и тот же получатель. Это может привести к множеству повторных оповещений. Также возможна ситуация, когда на одно событие подписывается множество разных компонентов системы. При этом одно событие может вызвать серию лавинообразных событий.
Чтобы лучше контролировать событийную модель, можно вынести работу с событиями в конфигурацию и возложить обязанности управления событиями на контейнер приложения. Т.к. определённые события доступны только при определённых состояниях приложения, то разумно вынести в конфигурацию также и управление этим состоянием.
Контроллер, руководствуясь конфигурацией, подписывает соответствующие экраны на события в Event Bus в зависимости от текущего состояния системы.
Finite State Machines как раз и были спроектированы для этих целей. В них имеются и состояния, в рамках которых происходят события. И события, которые инициируют переходы между состояниями.
Преимущества использования Finite State Machines для конфигурации состояний приложения.
Конфигурация приложения в большинстве случаев задаётся статически. Настраивая приложение с помощью, например, dependency injection, мы задаём, как приложение будет структурировано при запуске. При этом забываем, что в процессе использования приложения, его состояние меняется. Изменение состояния зачастую жестко прописывается в коде приложения, что влечёт за собой трудности его изменения и дальнейшей поддержки.
Вынося переходы между состояниями в конфигурацию, мы получаем новый уровень гибкости при построении систем. И поэтому, на этапе создания составных элементов приложения, таких как формы, окна, панели, мы можем не беспокоиться, в какое состояние должно перейти приложение. Это можно сделать позднее, настроив поведение в конфигурации.
При этом все компоненты системы могут общаться, используя унифицированный механизм отправки событий — через контроллер (Statemachine).
Данный подход превращает все составные элементы приложения (формы, окна, панели) в повторно используемые компоненты, которыми можно легко управлять при помощи внешней конфигурации.
Как эффективно использовать Statemachine для конфигурации приложения мы опишем в следующей статье.
Если интересно, то можете посмотреть примеры конфигураций на сайте Enterprise Sampler — samples.lexaden.com/
Автор: hitmark