所謂WebMVC即Model2模型是目前Web開發領域的主流模型,Struts/Struts2框架是其典型實現。在概念層面上,這種程序組織模型是
怎樣建立起來的?與其他Web開發模型(如面向對象模型)具有怎樣的聯繫? 它未來可能的發展方向在哪裏?
結合Witrix開發平臺的具體實踐,基於級列設計理論我們可以看到一條概念發展的脈絡。http://canonical.iteye.com/blog/33824
1.
外部視角:原始的servlet規範提供了一個簡單的面向IO的程序響應模型。一次前臺訪問由一個特定的servlet負責響應,它從request中讀
取輸入流,在全局session中保持臨時狀態,向response中寫入輸出流。在此基礎上,JSP提供的模板概念翻轉了程序和輸出文本之間的相對地
位,簡化了文本輸出過程。至此,這種整體的程序模型基本上只是規範化了外部系統訪問Web服務器的響應模型,並沒有對後臺程序的具體實現制定明確的約束條
件。因此在最粗野的後臺實現中,讀取參數,業務處理,生成頁面等處理步驟是糾纏在一起的,很難理解,也很難重用。每一個後臺頁面都是一個不可分析的整體。
String paramA = request.getParameter( " paramA " );
ResultSet rsA =
%>
result = <%= rsA.getString( 0 ) %>
String paramB = request.getParamter( " paramB " );
ResultSet rsB =
<%
rsB.close();
rsA.close();
conn.close();
%>
2.
自發分離:在複雜的程序實踐中,我們會自發的對業務處理代碼和界面代碼進行一定程度的分離。因爲我們可以直觀的感受到這兩種代碼的穩定性並不匹配。例如不
同業務處理過程產生的結果都可以用一個html表格來展現,而同一個業務處理過程產生的結果頁面可能經常發生變化。一般我們傾向於將業務代碼寫在頁面上
方,而界面代碼寫在頁面下方,並使用一些原始的分解機制,例如include指令。這種分離是隨意的,缺乏形式邊界的。例如我們無法表達被包含的頁面需要
哪些參數,也難以避免全局變量名衝突。需要注意的是,分層的一般意義在於各個層面可以獨立發展,它的隱含假定是各層面之間的交互是規範化的,只使用確定的
數據結構,按照確定的方式進行交互。例如業務層和界面層通過標準的List/Map等數據結構交互,而不是使用具有無限多種樣式的特殊的數據結構。(在弱
類型語言環境中,實體對象的結構和Map是等價的).
List header =
List dataList =
%>
<% @ include file = " /show_table.jsp " %>
3.
規範分離:JSP所提供的useBean和tag機制,即所謂的Model1模型,是對程序結構分離的一種規範化。業務代碼封裝在java類中,一般業務
函數與web環境無關,即不使用request和response對象,
允許單元測試。tag機制可以看作是對include指令的增強,是一種代碼重用機制。tld描述明確了調用tag時的約束關係。調用tag時需要就地指
定調用參數,而include頁面所依賴的參數可能是在此前任意地方指定的,是與功能實現分離的。此外tag所使用的參數名是局部對象上的屬性名,從而避
免了對全局變量的依賴。很遺憾的是,jsp
tag所封裝的仍然是原始的IO模型,對程序結構缺乏精細的定義,在概念層面上只是對文本片段的再加工,難以支撐複雜的控件結構。早期jsp
tag無法利用jsp模板本身來構造,無法構成一個層層遞進的概念抽象機制,更是讓這種孱弱的重用模型雪上加霜。在其位卻無能謀其政,這直接造成了整個
j2ee前臺界面抽象層的概念缺失,以致很多人認爲一種前臺模板重用機制是無用的。在Witrix平臺中所定義的tpl模板語言,充分利用了xml的結構
特點,結合編譯期變換技術,成爲Witrix平臺中進行結構抽象的基本手段。實際上,xml能夠有效表達的語義比一般人所想象的要多得多。
<% List dataList = myBiz.process(paramA) %>
< ui:Table data = " <%= dataList %> " />
4.
框架分離:在Model1模型中,頁面中存在着大量的粘結性代碼,它們負責解析前臺參數,進行類型轉換和數據校驗,定位特定的業務處理類,設置返回結果,
控制頁面跳轉等。一種自然的想法是定義一個全局的程序框架,它根據集中的配置文件完成所有的粘結性操作。這也就是所謂面向action的WebMVC模
型。這一模型實現了服務器端業務層和界面層在實現上的分離,但是對於外部訪問者而言,它所暴露的仍然是原始的自動機模型:整個網站是一個龐大的自動機,每
次訪問都觸發一個action,在action中可能更改自動機的狀態(作爲全局狀態容器的session對象或者數據庫)。struts作爲面向
action框架的先驅,它也很自然的成爲了先烈。struts中所引入的FormBean,
鏈接管理等概念已經在實踐中被證明是無益的。一些新興的框架開始迴歸到通用的Map結構,直接指定跳轉頁面,或者利用CoC(Convention
Over Configuration)缺省映射.
public ActionForward perform (ActionMapping mapping,
ActionForm form,
HttpServletRequest req,
HttpServletResponse res)
{
RegisterForm rf = (RegisterForm) form;
return mapping.findForward( " success " );
}
5.
橫向延展:分層之後必然導向各個層面的獨立發展,我們的視野自然也會擴大到單個頁面之外,看到一個層面上更多元素之間的相互作用.在面嚮對象語言大行其道
的今天,繼承(inheritance)無疑是多數人首先想到的程序結構組織手段.後臺action可以很自然的利用java語言自身的繼承機制,配置文
件中也可以定義類似的extends或者parent屬性.但是對於前臺頁面一般卻很少有適用的抽象手段,於是便有人致力於前臺頁面的對象語言化:首先將
前臺頁面採用某種對象語言表達,然後再利用對象語言內置的結構抽象機制.放棄界面的可描述性,將其轉化爲某種活動對象,在我看來是一種錯誤的方向.而
JSF(JavaServerFace)規範卻似乎想在這個方向上越走越遠.JSF早期設計中存在的一個嚴重問題是延續了面嚮對象語言中的狀態與行爲綁定
的組織方式.這造成每次訪問後臺頁面都要重建整個Component Tree,
無法實現頁面結構的有效緩存.而Witrix平臺中的tpl模板語言編譯出的結構是無狀態的,可以在多個用戶之間重用.
6. 相關聚合:對象化首先意味着相關性的局域化,它並不等價於對象語言化.
當面對一個大的集合的時候,最自然的管理手段便是分組聚合:緊密相關的元素被分配到同一分組,相關性被局域化到組內.例如,針對某個業務對象的增刪改查操
作可以看作屬於同一分組. struts中的一個最佳實踐是使用DispatchAction,
它根據一個額外的參數將調用請求映射到Action對象的子函數上.例如/book.do?dispatchMethod=add.
從外部看來,這種訪問方式已經超越了原始的servlet響應模型,看起來頗有一些面向對象的樣子,但也僅僅侷限於樣子而已.
DispatchAction在struts框架中無疑只是一種權宜之計,它與form,
navigation等都是不協調的,而且多個子函數之間並不共享任何狀態變量(即不發生內部的相互作用),並不是真正對象化的組織方式.按照結構主義的
觀點,整體大於部分之和.當一組函數聚集在一起的時候,它們所催生的一個概念便是整體的表徵:this指針.Witrix平臺中的Jsplet框架是一個
面向對象的Web框架,其中同屬於一個對象的多個Action響應函數之間可以共享局部的狀態變量(thisObj),而不僅僅是通過全局的
session對象來發生無差別的全局關聯.http://canonical.iteye.com/blog/33873
需要注意的是,thisObj不僅僅聚集了後臺的業務操作,它同時定義了前後臺之間的一個標準狀態共享機制,實現了前後臺之間的聚合.而前臺的
add.jsp,
view.jsp等頁面也因此通過thisObj產生了狀態關聯,構成頁面分組.爲了更加明確的支持前臺頁面分組的概念,Witrix平臺提供了其他一些
輔助關聯手段.例如標準頁面中的按鈕操作都集中在std.js中的stdPage對象上,因此只需要一條語句stdPage.mixin
(DocflowOps);即可爲docflow定製多個頁面上的衆多相關按鈕操作.此外Witrix平臺中定義了標準的url構建手段,它確保在多個頁
面間跳轉的時候,所有以$字符爲前綴的參數將被自動攜帶.從概念上說這是一種類似於cookie,但卻更加靈活,更加面向應用的狀態保持機制.
IEntityDao entityDao;
String metaName;
public Object actQuery(){
thisObj.put( " pager " ,pager);
return success();
}
public Object actExport(){
Pager pager = (Pager)thisObj.get( " pager " );
return success();
}
}
7.
描述分離:當明確定義了Action所聚集而成的對象結構之後,我們再次回到問題的原點:如何簡化程序基元(對象)的構建?繼承始終是一種可行的手段,但
是它要求信息的組織結構是遞進式的,而很多時候我們實際希望的組織方式只是簡單的加和。通過明確定義的meta(元數據),從對象中分離出部分描述信息,
在實踐中被證明是一種有效的手段。同樣的後臺事件響應對象(ActionObject),同樣的前臺界面顯示代碼(PageGroup),配合不同的
Meta,可以產生完全不同的行爲結果, 表達不同的業務需求。http://canonical.iteye.com/blog/114066
從概念上說,這可以看作是一種模板化過程或者是一種複雜的策略模式 ProductWebObject =
DaoWebObject<ProductMeta>。當然限於技術實現的原因,在一般框架實現中,meta並不是通過泛型技術引入到Web
對象中的。目前常見的開發實踐中,經常可以看見類似BaseAction<T>,
BaseManager<T>的基類,它們多半僅僅是爲了自動實現類型檢查。如果結合Annotation技術,則可以超越類型填充,部分達
到Meta組合的效果。使用meta的另外一個副作用在於,meta提供了各個層面之間新的信息傳遞手段,它可以維繫多個層面之間的共變
(covariant)。例如在使用meta的情況下,後臺代碼調用requestVars(dsMeta.getUpdatableFields())
得到提交參數,前臺頁面調用forEach dsMeta.getViewableFields()來生成界面.
則新增一個字段的時候,只需要在meta中修改一處,前後臺即可實現同步更新,自動維持前後臺概念的一致性。有趣的是,前後臺在分離之後它們之間的關聯變
得更加豐富。
8. 切面分離: Meta一般用於引入外部的描述信息,很少直接改變對象的行爲結構。AOP(Aspect Oriented
Programming)概念的出現爲程序結構的組織提供了新的技術手段。AOP可以看作是程序結構空間中定位技術和組裝技術的結合,它比繼承機制和模板
機制更加靈活,也更加強大。http://canonical.iteye.com/blog/34941
Witrix平臺中通過類似AOP的BizFlow技術實現對DaoWebAction和前臺界面的行爲擴展,它可以在不擴展DaoWebAction類
的情況下,增加/修正/減少web事件響應函數,增加/修正/減少前臺界面展現元素。當前臺發送的$bizId參數不同的時候,應用到WebObject
上的行爲切片也不同,從而可以自然的支持同一業務對象具有多個不同應用場景的情況(例如審覈和擬製)。在BizFlow中定義了明確的實體化過程,前臺提
交的集合操作將被分解爲針對單個實體的操作。例如前臺提交objectEvent=Remove&id=1&id=2,將會調用兩次
<action id="Remove-default">操作。注意到AOP定位技術首先要求的就是良好的座標定義,
實體化明確定義了實體操作邊界,爲實體相關切點的構造奠定了基礎。http://canonical.iteye.com/blog/33784
9. 背景消除:在Witrix平臺中, (DaoWebAction + StdPageGroup + Meta +
BizFlow)構成完整的程序模型,因此一般情況下並不需要繼承DaoWebAction類,也不需要增加新的前臺頁面文件,而只需要在BizFlow
文件中對修正部分進行描述即可。在某種程度上DaoWebAction+StdPageGroup所提供的CRUD
(CreateReadUpdateDelete)模型成爲了默認的背景知識。如果背景信息極少泄漏,則我們可以在較高抽象層次上進行工作,而不再理會原
始的構造機制。例如在深度集成hibernate的情況下,很少會有必須使用SQL語句的需求。BizFlow是對實體相關的所有業務操作和所有頁面展現
的集中描述,在考慮到背景知識的情況下,它定義了一個完整的自給自足的程序模型。當我們的建模視角轉移到BizFlow模型上時,可以發展出新的程序構造
手段。例如BizFlow之間可以定義類似繼承機制的extends算子,可以定義實體狀態驅動的有限自動機,可以定義不同實體之間的鉤稽關係(實體A發
生變化的時候自動更新實體B上的相關屬性),也可以定義對Workflow的自然嵌入機制。從表面上看,BizFlow似乎迴歸到了前後臺大雜燴的最初場
景(甚至更加嚴重,它同時描述了多個相關頁面和多個相關操作),但是在分分合合的模型建立過程中,大量信息被分解到背景模型中,同時發展了各種高級結構抽
象機制, 確保了我們注意力的關注點始終是有限的變化部分。而緊緻的描述提高了信息密度,簡化了程序構造過程。http://canonical.iteye.com/blog/126467
< biz id ="my" >
< tpls >
< tpl id ="initTpl" >
< script src ="my_ops.js" ></ script >
< script >
stdPage.mixin(MyOps); // 引入多個頁面上相關按鈕對應的操作
</ script >
</ tpl >
</ tpls >
</ biz >
</ bizflow >