Статьи

Сервис-ориентированный интерфейс с JSF

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

Если отдел биллинга хочет разработать новый диалог для создания счетов, он может выглядеть так:

Как видите, на приведенном выше экране указана ссылка на клиента в верхней части. Нажав кнопку «..» прямо за текстовым полем короткого имени, откроется диалоговое окно, приведенное ниже, которое позволяет пользователю выбрать клиента:

После нажатия «Выбрать» данные клиента отображаются в форме счета.

Также можно выбрать клиента, просто введя номер клиента или введя короткое имя в текстовые поля на экране счета. Если введено уникальное короткое имя, диалог выбора вообще не появляется. Вместо этого данные клиента отображаются напрямую. Только двусмысленное короткое имя приводит к открытию экрана выбора клиента.

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

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

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

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

  • Композитный компонент (XHTML)
  • Подложка для составного компонента
  • Интерфейс слушателя для обработки результатов выбора


Провайдер (отдел управления клиентами)

Композитный компонент:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 
 
<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>&nbsp;</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>

Подложка для составного компонента:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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...
  }
 
 }
 
}

Интерфейс слушателя для обработки результатов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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-компонент выбора клиента, внедряя его, и подключается к нему с помощью интерфейса слушателя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 используется составной компонент, связанный с внедренным компонентом поддержки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 
      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: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>

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

Ссылка: Сервис-ориентированный пользовательский интерфейс от нашего партнера по JCG Майкла Шнелла в блоге A Java Developer’s Life .