隨著門戶在 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 用一個用例圖闡明了這些用例。這里提供的操作者是一個簡單用戶,他與全部四個用例都進行交互。我還提供了一些額外的詳細信息,比如所有其它用例中包括的登錄案例。我還沒有為樣本用例提供太多詳細信息;它們自身就能夠很清楚地描述自己的功能。
圖 1. 用例
下一步是結束關于每個用例的講述并開始進一步為系統建模。您可以根據收集的功能識別名詞和動詞,并設計交互和操作圖。對于我們的示例,我們將只研究其中一個示例,并詳細考察它。我們從 Search Items 用例開始。由于所有的用例都已經顯示在模型中,在結束 Search Items 用例后我們可以返回到這個視圖,并接著為下一個用例建模。
可以用來描述用戶與系統間操作的方法有好幾種。圖 2 顯示了 SearchItems 用例的一張操作圖。這張圖是從與系統交互的用戶的角度提供的。這張圖,也稱為“泳道”圖,是確定系統中組件的起點。在這張圖中,我們把“系統”看作一個單獨的組件。對它進行進一步的分解,我們就可以開始確定可以建模為類的額外對象和設計中的其它對象。
圖 2. 帶泳道的操作圖
與系統的進一步交互(不管是通過交互圖還是協作圖)都可以用來擴展設計,并且有助于派生模型中使用的額外對象和交互(名詞和動詞)。這種類型的建模和分解需要深厚的背景知識,并且要理解 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 可以結合進入到先前描述的用例圖中的所有用例。
圖 3. 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 的搜索功能的所有組件。
圖 4. Search 類圖
雖然類圖使我們可以從組件視圖看到對象,但它并不提供將在我們的 portlet 中發生的事件序列的完整視圖。使用顯示搜索操作如何發生的序列圖,我們可以看到搜索功能的更多詳細信息。圖 5 描述了我們的組件將如何在搜索過程中實際進行交互。注意:我們使用方法名稱、stereotype 和通用英語描述對象間的交互。
圖 5. Search 序列圖
英語文本交互(比如“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 中,我已經展示了對象的包結構(在建模工具中),這種結構與我希望在開發過程中創建的包結構類似。這種方法有助于把相互連接的項以與它們的開發方法,以及最后部署到目標系統的方法類似的格式組合在一起。
圖 6. 模型瀏覽器
與其它工作產品(比如線框模型(wire frame model)和設計文檔)一起使用時,UML 會成為一種向開發者傳達需求和設計的強大方法。上面提供的代碼樣本向您提供了一個演示如何為對象建模的示例。它們還有望提供一些使用 JSP 和實用程序類開發 portlet 的最佳方法的示例。最后,我希望已經為您提供了足夠信息,使您可以在設計自己的門戶應用程序時開始使用 UML。在將來的文章中,我將嘗試這里提供的內容,并以它們為基礎來包括建模 portlet 服務,并擴展 portlet 類來利用這些服務。
原文轉自:http://www.anti-gravitydesign.com