Статьи

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

 

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

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

Крупная компания

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

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

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

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

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

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

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

Используя 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>

Резюме

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