В крупных проектах по разработке программного обеспечения сервис-ориентированная архитектура очень распространена, поскольку она предоставляет функциональный интерфейс, который может использоваться различными группами или отделами. Те же принципы должны применяться при создании пользовательских интерфейсов.
В случае крупной компании, в которой, помимо прочего, есть отдел выставления счетов и отдел управления клиентами, организационная структура может выглядеть следующим образом:
Если отдел биллинга хочет разработать новый диалог для создания счетов, он может выглядеть так:
Как видите, на приведенном выше экране указана ссылка на клиента в верхней части. Нажав кнопку «..» прямо за текстовым полем короткого имени, откроется диалоговое окно, приведенное ниже, которое позволяет пользователю выбрать клиента:
После нажатия «Выбрать» данные клиента отображаются в форме счета.
Также можно выбрать клиента, просто введя номер клиента или введя короткое имя в текстовые поля на экране счета. Если введено уникальное короткое имя, диалог выбора вообще не появляется. Вместо этого данные клиента отображаются напрямую. Только двусмысленное короткое имя приводит к открытию экрана выбора клиента.
Функциональность клиента будет обеспечиваться разработчиками, которые входят в команду управления клиентами. Типичный подход заключается в том, что команда разработчиков управления клиентами предоставляет некоторые услуги, а разработчики биллингового отдела создают пользовательский интерфейс и вызывают эти службы.
Однако этот подход предполагает более тесную связь между этими двумя различными отделами, чем на самом деле необходимо. Счет-фактура требует только уникальный идентификатор для ссылки на данные клиента. Разработчики, создающие диалоговое окно счета-фактуры, на самом деле не хотят знать, как запрашиваются данные клиента или какие услуги используются в фоновом режиме для получения этой информации.
Разработчики по управлению клиентами должны предоставить полную часть пользовательского интерфейса, которая отображает идентификатор клиента и обрабатывает выбор клиента:
Используя JSF 2, этого легко достичь с помощью составных компонентов. Логический интерфейс между отделом управления клиентами и отделом биллинга состоит из трех частей:
- Композитный компонент (XHTML)
- Подложка для составного компонента
- Интерфейс слушателя для обработки результатов выбора
Провайдер (отдел управления клиентами)
Композитный компонент:
<!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:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:composite="http://java.sun.com/jsf/composite" xmlns:ice="http://www.icesoft.com/icefaces/component" xmlns:ace="http://www.icefaces.org/icefaces/components" xmlns:icecore="http://www.icefaces.org/icefaces/core"> <ui:composition> <composite:interface name="customerSelectionPanel" displayName="Customer Selection Panel" shortDescription="Select a customer using it's number or short name"> <composite:attribute name="model" type="org.fuin.examples.soui.view.CustomerSelectionBean" required="true" /> </composite:interface> <composite:implementation> <ui:param name="model" value="#{cc.attrs.model}"/> <ice:form id="customerSelectionForm"> <icecore:singleSubmit submitOnBlur="true" /> <h:panelGroup id="table" layout="block"> <table> <tr> <td><h:outputLabel for="customerNumber" value="#{messages.customerNumber}" /></td> <td><h:inputText id="customerNumber" value="#{model.id}" required="false" /></td> <td> </td> <td><h:outputLabel for="customerShortName" value="#{messages.customerShortName}" /></td> <td><h:inputText id="customerShortName" value="#{model.shortName}" required="false" /></td> <td><h:commandButton action="#{model.select}" value="#{messages.select}" /></td> </tr> <tr> <td><h:outputLabel for="customerName" value="#{messages.customerName}" /></td> <td colspan="5"><h:inputText id="customerName" value="#{model.name}" readonly="true" /></td> </tr> </table> </h:panelGroup> </ice:form> </composite:implementation> </ui:composition> </html>
Подложка для составного компонента:
package org.fuin.examples.soui.view; import java.io.Serializable; import javax.enterprise.context.Dependent; import javax.inject.Inject; import javax.inject.Named; import org.apache.commons.lang.ObjectUtils; import org.fuin.examples.soui.model.Customer; import org.fuin.examples.soui.services.CustomerService; import org.fuin.examples.soui.services.CustomerShortNameNotUniqueException; import org.fuin.examples.soui.services.UnknownCustomerException; @Named @Dependent public class CustomerSelectionBean implements Serializable { private static final long serialVersionUID = 1L; private Long id; private String shortName; private String name; private CustomerSelectionListener listener; @Inject private CustomerService service; public CustomerSelectionBean() { super(); listener = new DefaultCustomerSelectionListener(); } public Long getId() { return id; } public void setId(final Long id) { if (ObjectUtils.equals(this.id, id)) { return; } if (id == null) { clear(); } else { clear(); this.id = id; try { final Customer customer = service.findById(this.id); changed(customer); } catch (final UnknownCustomerException ex) { FacesUtils.addErrorMessage(ex.getMessage()); } } } public String getShortName() { return shortName; } public void setShortName(final String shortNameX) { final String shortName = (shortNameX == "") ? null : shortNameX; if (ObjectUtils.equals(this.shortName, shortName)) { return; } if (shortName == null) { clear(); } else { if (this.id != null) { clear(); } this.shortName = shortName; try { final Customer customer = service .findByShortName(this.shortName); changed(customer); } catch (final CustomerShortNameNotUniqueException ex) { select(); } catch (final UnknownCustomerException ex) { FacesUtils.addErrorMessage(ex.getMessage()); } } } public String getName() { return name; } public CustomerSelectionListener getConnector() { return listener; } public void select() { // TODO Implement... } public void clear() { changed(null); } private void changed(final Customer customer) { if (customer == null) { this.id = null; this.shortName = null; this.name = null; listener.customerChanged(null, null); } else { this.id = customer.getId(); this.shortName = customer.getShortName(); this.name = customer.getName(); listener.customerChanged(this.id, this.name); } } public void setListener(final CustomerSelectionListener listener) { if (listener == null) { this.listener = new DefaultCustomerSelectionListener(); } else { this.listener = listener; } } public void setCustomerId(final Long id) throws UnknownCustomerException { clear(); if (id != null) { clear(); this.id = id; changed(service.findById(this.id)); } } private static final class DefaultCustomerSelectionListener implements CustomerSelectionListener { @Override public final void customerChanged(final Long id, final String name) { // Do nothing... } } }
Интерфейс слушателя для обработки результатов:
package org.fuin.examples.soui.view; /** * Gets informed if customer selection changed. */ public interface CustomerSelectionListener { /** * Customer selection changed. * * @param id New unique customer identifier - May be NULL. * @param name New customer name - May be NULL. */ public void customerChanged(Long id, String name); }
Пользователь (отдел биллинга)
Bean-объект invoice просто использует bean-компонент выбора клиента, внедряя его, и подключается к нему с помощью интерфейса слушателя:
package org.fuin.examples.soui.view; import java.io.Serializable; import javax.annotation.PostConstruct; import javax.enterprise.context.SessionScoped; import javax.enterprise.inject.New; import javax.inject.Inject; import javax.inject.Named; @Named("invoiceBean") @SessionScoped public class InvoiceBean implements Serializable { private static final long serialVersionUID = 1L; @Inject @New private CustomerSelectionBean customerSelectionBean; private Long customerId; private String customerName; @PostConstruct public void init() { customerSelectionBean.setListener(new CustomerSelectionListener() { @Override public final void customerChanged(final Long id, final String name) { customerId = id; customerName = name; } }); } public CustomerSelectionBean getCustomerSelectionBean() { return customerSelectionBean; } public String getCustomerName() { return customerName; } }
Наконец, в счете-фактуре XHTML используется составной компонент, связанный с внедренным компонентом поддержки:
<!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:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:fuin="http://fuin.org/examples/soui/facelets" xmlns:customer="http://java.sun.com/jsf/composite/customer"> <ui:composition template="/WEB-INF/templates/template.xhtml"> <ui:param name="title" value="#{messages.invoiceTitle}" /> <ui:define name="header"></ui:define> <ui:define name="content"> <customer:selection-panel model="#{invoiceBean.customerSelectionBean}" /> </ui:define> <ui:define name="footer"></ui:define> </ui:composition> </html>
Резюме
В заключение, за части пользовательского интерфейса, которые ссылаются на данные из других отделов, должен отвечать отдел, который доставляет данные. Любые изменения в коде обеспечения могут быть легко сделаны без каких-либо изменений в коде использования. Еще одним важным преимуществом этого метода является гармонизация пользовательского интерфейса приложения. Элементы управления и панели, отображающие одни и те же данные, всегда выглядят одинаково. Каждый отдел может также создать репозиторий своих предоставленных компонентов пользовательского интерфейса, что делает процесс разработки нового диалога столь же простым, как сбор правильных компонентов.