随着门户在 Web 上的日益流行,一些组织继续选择 IBM® WebSphere® Portal 作为它们的主要平台,作为一个社区,我们需要快速开发一套使我们能够高效率地设计和开发门户应用程序的可重用资产和工作产品。设计师和开发者同样都在为解决关于构建应用程序的设计问题而努力,以便最大程度地利用 WebSphere Portal 提供的框架。
我经常想,我们需要一系列模型或模式用来描述 portlet 以及它们在应用程序中的使用。本文阐明了一种使用统一建模语言(Unified Modeling Language,UML)来设计门户与 portlet 的方法。使用基本的门户设计方法和 UML,我为简单的 Portlet-MVC 设计(根据这种设计,您可以构建自己的 portlet 应用程序)提供了设计框架。这个框架有希望成为一组 portlet 设计模型的起点,根据这些模型,我们可以开始构建一组可重用的模式。
本文不是关于如何编写 portlet 的。虽然我提供了一些代码样本来帮助阐明 UML 模型与设计,但大多数代码都被缩减到只有关于设计本身的最基本的部分。任何健壮的应用程序所需要的错误检查和异常处理都会比这里提供的要多得多。
为什么要为门户应用程序建模?问得好。通常,我们会把门户看作一组小而严谨的功能或内容片段。我不需要那么麻烦地为它们建模,是吗?营销宣传告诉我们,构建 portlet 很容易,只要花费几小时到几天就可以构建出简单的 portlet。如果是这样的话,我们就几乎不需要在主要的需求和设计阶段上花费时间了,是不是?是的,的确如此!如果门户真的只需要一两个提供一些严谨的功能或对旧系统的访问的 portlet,那么可能就没必要在早期投入大量的时间和人力来完成完整的设计。几天到一周的设计时间可能就足够了。或者您也可以考虑另一种方法,比如使用改进的原型(prototype)或极端编程。
不幸的是,虽然 WebSphere Portal 框架的确大大减少了设计和开发时间,并给我们提供了一套可扩展的功能,但它不是一击中的的方法。大多数主要的门户应用程序需要的不仅仅是一、两个简单的 portlet。至少要有一“套” portlet,或者甚至是几套来提供所需的功能。另外,您还需要实用程序类、单子程序(singleton)、标记库和一些服务(它们提供对各种服务与系统的底层访问)。我们也不要忘记用于大容量事务系统的 EJB。这种更大的系统需要一个遵守经过验证的设计方法的专门设计周期。本文中推荐的一个使用 UML 的设计周期仍在发展中,但即便是 WebSphere Portal 本身也在不断地发展。
这里提供的案例相当简单。我将描述一个简单的 portlet,并为其建模,该 portlet 针对给定的问题提供了基本的 CRUD(创建(Create)、读(Read)、更新(Update)、删除(Delete))功能。所采用的方法相当普通。这种方法旨在展示如何使用 UML 为这类系统建模的基本原理,并不为您提供太多的功能示例。大多数设计师和设计人员在他们的职业生涯中都会遇到这类 portlet 功能的许多示例,应该能够很容易地将这里提供的思想与实际情况联系在一起。
记住这一点,我们可以假装是在对一个“Item”类型的简单对象(或者也可能是一列“Items”)进行操作。我们并不真正关心“Item”到底是什么。它可以是一组用户、客户、帐户、房子、计算机或任何东西。
使用用例来收集需求可能比较棘手。只有那些在用例建模方面很有经验的高手才能完成这项工作。通常情况下,理解 UML 和用例建模的技术人员在从用户的角度看用例时都会有麻烦。他们一直尝试在收集需求的同时设计系统。理解如何构建有效用例的非技术人员(比如顾问和业务分析员)是凤毛麟角。这些人会说客户的语言,能够建立真实反映业务需求的有效用例。
对于设计来说,CRUD 功能并不总是最重要的。通常,基本的表维护对应用程序并不具有主要的影响,所以它好象不能给操作者带来多大的价值,经常不被看作是主要的用例。但我认为,这是您将遇到的最常见的功能类型之一,并且很容易理解。熟悉需求将帮助您免于在需求细节上花费过多时间和精力,使您能够将重点放在模型和设计方法上。
尝试达到用例的正确粒度级别也是个问题。在一个粒度非常粗的系统中,我们可以创建一个名为“Manage Item”的用例。它可能有点太过简单,不足以提供任何有用的分析。将它分解为一个更完整的用例集可能会更好。为演示我们的样本 portlet,我已经创建了一个简单的名为“ManageItems”的包,它包含下面四个用例:
图 1 用一个用例图阐明了这些用例。这里提供的操作者是一个简单用户,他与全部四个用例都进行交互。我还提供了一些额外的详细信息,比如所有其它用例中包括的登录案例。我还没有为样本用例提供太多详细信息;它们自身就能够很清楚地描述自己的功能。
下一步是结束关于每个用例的讲述并开始进一步为系统建模。您可以根据收集的功能识别名词和动词,并设计交互和操作图。对于我们的示例,我们将只研究其中一个示例,并详细考察它。我们从 Search Items 用例开始。由于所有的用例都已经显示在模型中,在结束 Search Items 用例后我们可以返回到这个视图,并接着为下一个用例建模。
可以用来描述用户与系统间操作的方法有好几种。图 2 显示了 SearchItems 用例的一张操作图。这张图是从与系统交互的用户的角度提供的。这张图,也称为“泳道”图,是确定系统中组件的起点。在这张图中,我们把“系统”看作一个单独的组件。对它进行进一步的分解,我们就可以开始确定可以建模为类的额外对象和设计中的其它对象。
与系统的进一步交互(不管是通过交互图还是协作图)都可以用来扩展设计,并且有助于派生模型中使用的额外对象和交互(名词和动词)。这种类型的建模和分解需要深厚的背景知识,并且要理解 UML 及其使用。本文只尝试把 UML 放到用来设计 portlet 的适当上下文中。
幸运的是,我们准备构建的是一个 WebSphere Portal portlet。因为这个原因,也由于 WebSphere Portal 提供的框架,为最终的 portlet 派生组件布局相当容易。在设计时我们要聪明些,要确保正确而又充分地利用框架。不过 portlet 设计也可以在其它优秀设计者的工作的基础上进行,并遵守为 WebSphere Portal 发布的 API。
为展示我们的 portlet 必需的类,我们可以遵循这个框架推荐的基本模型-视图-控制器( Model-View-Controller)模式。图 3 显示了 Manage Items portlet 的基本类图。图 3 中的类为此提供了一个基本布局和许多其它的 portlet,在构建自己的门户应用程序时您可能会遇到这些 portlet。以这种方式配置的 Manage Items portlet 可以结合进入到先前描述的用例图中的所有用例。
原始图规定了三个基本类。我们来通过一些代码示例按顺序看一下每个类。
这是 portlet 的一个主要的类,是从 WebSphere Portal API 提供的 PortletAdaptor 类继承过来的。这个类采用一种轻量级的控制器方法来设计 portlet,所以它主要是一个轻量级的对象,包含有极少的业务逻辑。这个类中的方法是所有的 portlet 控制器类中概括的标准方法。
package com.ibm.wps.manageitems; import java.io.*; import java.util.*; import com.ibm.wps.engine.*; import org.apache.jetspeed.portlet.*; import org.apache.jetspeed.portlet.event.*; import org.apache.jetspeed.portlets.*; public class ManageItemsPortletHTMLController extends PortletAdapter implements PortletTitleListener, ActionListener { public void init(PortletConfig portletConfig) throws UnavailableException { super.init(portletConfig); } public void doTitle(PortletRequest request, PortletResponse response) { }
actionPerformed() 方法执行了这个 portlet 的许多控制器逻辑。在这个示例中,该方法是一个 if 语句序列,这些语句将控制权转移到我们的实用程序类中的不同位置。
public void actionPerformed(ActionEvent event) { PortletRequest request = event.getRequest(); PortletSession session = request.getPortletSession(); PortletAction _action = event.getAction(); DefaultPortletAction action; if (_action instanceof DefaultPortletAction) { action = (DefaultPortletAction)_action; // Handle ACTION events if (action.getName().equals(ManageItemsUtil.ACTION_SEARCH)) { bean = ManageItemsUtil.searchItems(this, request); session.setAttribute(ManageItemsUtil.TARGET_PAGE, ManageItemsPortletUtil.JSPSEARCHRESULTS); session.setAttribute("ManageItemsBean", bean); } else if (action.getName().equals(ManageItemsUtil.ACTION_CREATE)) { bean = ManageItemsUtil.createItem(this, request); session.setAttribute(ManageItemsUtil.TARGET_PAGE, ManageItemsUtil.JSPConfirm); session.setAttribute("ManageItemsBean", bean); } else if (action.getName().equals(ManageItemsUtil.ACTION_SAVE)) { bean = ManageItemsUtil.saveItem(this, request); bean.clearErrors(); bean.clearValues(); session.setAttribute(ManageItemsUtil.TARGET_PAGE, ManageItemsUtil.JSPSaveResults); session.setAttribute("ManageItemsBean", bean); } } }
doView()
方法显示了我们的 portlet 中用来接收用户输入和显示结果的各种 JSP。它充当一个设置并显示正确 JSP 的控制器方法。要显示哪个 JSP 由上面的 actionPerformed()
方法决定。
public void doView(PortletRequest request, PortletResponse response) throws PortletException, IOException { PrintWriter writer = response.getWriter(); PortletContext context = getPortletConfig().getContext(); PortletSession session = request.getPortletSession(); ManageItemsBean bean = null; // Set initial jspName to JSPMain. // Use this as the default if TARGET_JSP String displayJsp = null; String jspPrefix = "/jsp/"; String jspName = ManageItemUtil.JSPMain; String tempJsp = (String) session.getAttribute(ManageItemsUtil.TARGET_PAGE); if (tempJsp != null) { displayJsp = jspPrefix + tempJsp; } else { displayJsp = jspPrefix + jspName; } try { // Extract ManageItemsBean from session bean = (ManageItemsBean) session.getAttribute("ManageItemstBean"); // Keep the bean from session if it exists if (bean != null) { } else { // Instantiate a new bean if it doesn't already exist } bean = ManageItemsUtil.initItemsBean(this, request); } // Put the bean back into session session.setAttribute("ManageItemsBean", bean); // Delegate the rendering to displayJsp context.include(displayJsp, request, response); // Log debug information if (getPortletLog().isDebugEnabled()) { getPortletLog().debug("++++ Set display JSP to " + displayJsp); } } catch (Exception ex) { getPortletLog()Debug(ex.getMessage()); ex.printStackTrace(System.out); } } public void doHelp(PortletRequest request, PortletResponse response) throws PortletException, IOException { } public void doEdit(PortletRequest request, PortletResponse response) throws PortletException, IOException { } public void doConfigure(PortletRequest request, PortletResponse response) throws PortletException, IOException { } }
manageItemsUtil
类实现了其它类和一些 JSP 所需要的几个基本的实用函数。它还抽象了 portlet 所需的大多数业务逻辑。它将通过在自己的方法中包含业务逻辑本身或者将逻辑转交给另一个助手类、portlet 服务或 EJB 完成抽象工作。
package com.ibm.wps.manageitems; import java.io.*; import java.util.*; Import java.text.*; Import org.apache.jetspeed.portlet.*; Import org.apache.jetspeed.portlet.event.*; Import org.apache.jetspeed.util.*; public class ManageItemPortletUtil { public final static String ACTION_CREATE = "createItemAction"; public static final String ACTION_DELETE = "deleteItemAction"; public static final String ACTION_EDIT = "editItemAction"; public static final String ACTION_SEARCH = "searchItemAction"; public static final String ACTION_SAVE = "saveItemAction"; public static final String JSP_ADD = "AddItem.jsp"; public static final String JSP_CONFIRM = "ConfirmItem.jsp"; public static final String JSP_EDIT = "EditItem.jsp"; public static final String JSP_MAIN = "Index.jsp"; public static final String JSP_SEARCH_RESULTS = "SearchResults.jsp"; public static final String JSP_SAVE_RESULTS = "SaveResults.jsp"; public static final String CALLING_PAGE = "callingPage"; public static final String TARGET_PAGE = "targetPage";
getNewActionURI()
方法主要被 portlet 中的 JSP 用来创建 JSP 页面内的返回和操作 URI。这个方法有两个版本:首先显示的是单 URI 版本,第二个版本使一个参数和 URI 一起被传递。
/** * Method: getNewActionURI(PortletResponse, String) * Return: String * Description: Build an action URI */ public static String getNewActionURI(PortletResponse request, String actionName) { PortletURI portletURI = request.createReturnURI(); DefaultPortletAction action = new DefaultPortletAction(actionName); portletURI.addAction(action); return portletURI.toString(); } /** * Method: getNewActionURI(PortletResponse, String, String, String ) * Return: String * Description: Build an action URI with additional parameters */ public static String getNewActionURI(PortletResponse request, String actionName, String Param, String Value) { PortletURI portletURI = request.createURI(); PortletAction portletAction = new DefaultPortletAction(actionName); portletURI.addAction(portletAction); portletURI.addParameter(Param, Value); return portletURI.toString(); }
initManageItemsBean()
方法是一个可选的方法,如果需要的话,可以用它来初始化 portlet bean。如果您的 bean 非常简单,那就不需要这个方法。
Public static ManageItemsBean initManageItemsBean(PortletAdapter portlet, PortletRequest request) { ManageItemsBean bean = new ManageItemsBean(); //initialize bean here if necessary return bean; }
这里提供的 searchItems()
方法是作为一个示例,说明业务逻辑可以放置在这类 portlet 中。这个想法是为了使控制器类尽可能保持简单以允许业务逻辑被抽象为助手类。这个方法可以实现业务逻辑本身,或者,如果需要的话,它也可以访问 portlet 服务或 EJB。portlet 要实现全部功能还需要这种类型的其它方法。
Public static ManageItemsBean searchItems(PortletAdapter portlet, PortletRequest request) { ManageItemsBean bean = new ManageItemsBean(); //business logic goes here! return bean; } }
这个 bean 被用作我们的主存储器和与 JSP 进行通信的通信设备。在 portlet 实例的生命期内,我们将该 bean 高速缓存到 PortletSession 中,当需要时 portlet 会更新该 bean 内的数据。由于 portlet 是用 Model 2 JSP 体系架构设计的,所以该 bean 在我们的设计中起着很重要的作用。在上面的图 3 中,我已经包含了一组 getter 和 setter 来模拟实现我们正在讨论的搜索功能时可能需要的方法。您应该为自己的实现修改那些 getter 和 setter。
布置好核心类和交互作用后,我们可以看一下完成我们的 portlet 所需的表示层。先前已经提到过,该示例遵守我们熟悉的模型-视图-控制器方法,在开发 Web 应用程序时充分利用 J2EE 最佳方法。JSP 在这种方法中起着重要作用。
由于 JSP 是 HTML 与 Java 语言的混合物,它很难把表示层完全分离出来。我们经常会有一些机会把一些业务逻辑混入到 JSP 中。并没有完美的方法可以在开发工作中避开这一点。一个好的设计可以帮助避开这一点,但即便是最好的设计也无法应对所有可能的情况或作用域变化。理解正确开发的概念以及语言本身的有经验的开发者可以帮助确保 JSP 内出现的任何业务逻辑都是有限的,并且是设计正确的。
我们将为自己的示例添加两个 JSP 页面。注意,在图 4 中,对于添加到模型中的每个 JSP,我们还为其添加了一个相关的 HTML 页面。这种关联使我们可以在我们的模型中演示服务器端交互与客户机端交互之间的分离。对于 Manage Items portlet,我们已经为其添加了下列页面:
- Main.jsp
- 这是缺省情况下显示的主 JSP。
doView()
方法的样本代码显示了这样一个示例,如果没在actionHandler()
中设置其它页面的话,该示例将设置TARGET_PAGE
来显示该页面。- Main.html
- 这是
Main.jsp
的客户机端版本。这张页面包含的是一个触发搜索操作的搜索表单。这张页面中的操作触发actionPerformed()
方法开始搜索。- SearchResults.jsp
- 这个 JSP 以 Java bean 的方式获得搜索操作的结果,并格式化相应的 HTML 页面来显示搜索结果。
- SearchResults.html
- 这是搜索结果的客户机端显示。
通过扩展原始类图来使用这些新功能使我们可以更好地了解我们的 portlet 的整体功能。在新的类图(图 4)中,我们看到了一个完整的图,这张图中有用于我们的 portlet 的搜索功能的所有组件。
虽然类图使我们可以从组件视图看到对象,但它并不提供将在我们的 portlet 中发生的事件序列的完整视图。使用显示搜索操作如何发生的序列图,我们可以看到搜索功能的更多详细信息。图 5 描述了我们的组件将如何在搜索过程中实际进行交互。注意:我们使用方法名称、stereotype 和通用英语描述对象间的交互。
英语文本交互(比如“Determine Action”)可以分解得非常细,并被捕获到第二个设计文档或工作产品中,或者在实现过程中交付给开发者处理。关于您的模型必须遵守哪种级别的详细程度,并没有这方面的规则,涉及到的努力和详细程度级别应该反映功能的性质和工程的规模大小。
在设计 JSP 时,我们应该利用我们用来构建自己的 portlet 控制器与其它类的框架。在我们的设计中,我们依赖 Java bean 为我们提供需要在 JSP 中显示的任何信息。假设了这一点后,我们就可以依靠 bean 了。在为 JSP 编码时,开始要先导入将需要的任何包。这将包括添加 portlet 包,因为我们可能要访问 manageItemsUtil()
类。另外,这里还应该包括任何所需的标记库。
在您的类图中,可以用类似于为 Java bean 和实用程序类建模的方法为标记库和 portlet 服务建模。如果您用 JSP 组件已经开发了您想包含和建模的定制标记库,那么就可以用一种包括的 stereotype 在类图中设置它们。在开始使用这些项之前,您应该看一下您所有的 portlet 并将类似的类合并到服务与库中。
<%@ page import="org.apache.jetspeed.portlet.service.*" %> <%@ page import="org.apache.jetspeed.portlet.*" %> <%@ page import="org.apache.jetspeed.portletcontainer.PortletRequestImpl" % > <%@ page import="org.apache.jetspeed.portlet.*" %> <%@ page import="com.ibm.wps.engine.RunData" %> <%@ page import="com.ibm.wps.manageitems.*" %> <%@ page import="com.ibm.wps.services.authorization.*, com.ibm.wps.puma.*,com.ibm.wps.util.*" %> <%@ taglib uri="/WEB-INF/tld/portlet.tld" prefix="portletAPI" %> <portletAPI:init/> <jsp:useBean id="ManageItemsBean" class="com.ibm.wps.manageitems.ManageItemsBean" scope="session" />
如果您已经在 JSP 中创建了页头,您可以创建一个类似于下面示例的操作处理程序 URI。这个示例创建了一个 ACTION_CREATE
链接。在调用这个示例时,它触发被编写来识别这个参数的 actionPerformed()
方法片段。然后,操作处理程序将执行任何需要执行的操作,并将 TARGET
设置为下一个要显示的 JSP。
<a href="<%= ManageItemsUtil.getNewActionURI(portletResponse, ManageItemsUtil.ACTION_CREATE) %>">Add Item</a>
您可以使用下列代码样本创建模型中的搜索表单。它还使用一个操作处理程序 URI,并使用 portlet 标记库为表单参数编码。这个库不在我们的系统中建模,因为它是为 WebSphere Portal 提供的 API 的一部分。为已经在 WebSphere Portal 中提供的组件建模是可能的,但这样会增加模型的复杂程度。您应该只在真正想显示关于使用特殊 API 的特殊详细信息时,才使用这种级别的详细程度。
<form name="itemsearchform" action="<%= ManageItemUtils.getNewActionURI(portletResponse, ManageItemsUtil.ACTION_SEARCH) %>" method="post"> <table border="0" cellpadding="3" cellspacing = "3"> <tr> <td align="left">Customer ID:</td> <td align="left"> <input type="text" name="PORTLETAPI:ENCODENAMESPACE value="itemname">" value="<%= ManageItemBean.getItemName() %>"> </td> </tr> <tr> <tr> <td align="right"> <input type="submit" name="" value="" />"> <br> </td> </tr> </table> </form>
即便是只有一个模型就绪,移到这块功能的微型设计和开发过程也会容易得多。毕竟系统(或者,在我们的例子中是子系统)中的主要组件都是完全设计好并且接合在一起的,开发者不仅可以看到应该把哪些对象实现得最好,还可以看到如何把这些对象实现得最好。开发期间识别出的更改可以在模型中被改编,并在整个系统中被反映出来。
在图 6 中,我已经展示了对象的包结构(在建模工具中),这种结构与我希望在开发过程中创建的包结构类似。这种方法有助于把相互连接的项以与它们的开发方法,以及最后部署到目标系统的方法类似的格式组合在一起。
与其它工作产品(比如线框模型(wire frame model)和设计文档)一起使用时,UML 会成为一种向开发者传达需求和设计的强大方法。上面提供的代码样本向您提供了一个演示如何为对象建模的示例。它们还有望提供一些使用 JSP 和实用程序类开发 portlet 的最佳方法的示例。最后,我希望已经为您提供了足够信息,使您可以在设计自己的门户应用程序时开始使用 UML。在将来的文章中,我将尝试这里提供的内容,并以它们为基础来包括建模 portlet 服务,并扩展 portlet 类来利用这些服务。