前一篇中,我們討論了Framework的觀念及設計時的注意事項,不過你我都明白,僅靠這些簡短的敘述,是不可能設計出Framework,更不用談實作部份了。
相對於撰寫應用程式,設計Framework通常需要更多的軟體架構及經驗,在看軟體架構的高度也與一般撰寫應用程式不同,通常設計Framework時,架構師必須要以很高的高度來看整個系統架構,然後逐步地往細節走,一開始,是看整棟大樓的位置,周邊,接著細看到每個房間的佈局,越往細節,所造出的限制就越多,這就是設計Framework的基礎觀念之一。
The Framework Designing(2)- Writing Extensible Application
文/黃忠成
Designing Framework ??
前一篇中,我們討論了Framework的觀念及設計時的注意事項,不過你我都明白,僅靠這些簡短的敘述,是不可能設計出Framework,更不用談實作部份了。
相對於撰寫應用程式,設計Framework通常需要更多的軟體架構及經驗,在看軟體架構的高度也與一般撰寫應用程式不同,通常設計Framework時,架構師必須要以很高的高度來看整個系統架構,
然後逐步地往細節走,一開始,是看整棟大樓的位置,周邊,接著細看到每個房間的佈局,越往細節,所造出的限制就越多,這就是設計Framework的基礎觀念之一。
有經驗的架構師,在設計Framework或是應用程式架構時,會直接跳過許多嘗試階段,以網狀方式採用多種Design Patterns來設計,但對於較沒有經驗的設計師來說,沒有經過這些嘗試階段,
說實話很難理解為何架構師要這樣設計,明明很簡單的事,卻要繞幾個圈,明明可以用一個專案(Project)做完的事,偏偏要搞出4-5個專案,光是搞清楚彼此間的關係就夠頭大的了,何況還要在這個架構下實作。
所以架構師很容易遭遇來自於程式設計師的質疑,例如這樣做會影響效率,這樣做很麻煩,這樣做很奇怪等等,殊不知架構師這樣的設計,是在為以後軟體架構變動留下後路,避免頻繁的重造輪子之事發生。
設計Framework或是可延展的應用程式時,必須明白一件事,那就是可延展與高效率是無法畫上等號的,就像你以Parameter方式來對SQL Server新增1000筆資料,比起把1000次的INSERT放在一個命令(SqlCommand)
執行來得慢,但這不該是捨Parameter而選用後者的原因是吧?
從一個ASP.NET應用程式開始
在開始進入設計Framework正題前,得先學會如何設計一個具延展性的應用程式,所謂延展性,就是指當需求變動時,應用程式能以最小的改動及最低的影響適應變動,當然! 前提是這個需求變動是當初可預期的。
本文中以一個簡單的ASP.NET應用程式開始,這個程式很簡單,透過SqlDataSource連結SQL Server,經由FormView讓使用者新增、修改即刪除資料。
Default.aspx |
<%@Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="FirstDemo.Default" %>
<!DOCTYPEhtml PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<htmlxmlns="http://www.w3.org/1999/xhtml"> <headrunat="server"> <title></title> </head> <body> <form id="form1" runat="server">
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>" DeleteCommand="DELETE FROM [Customers] WHERE [CustomerID] = @CustomerID" InsertCommand="INSERT INTO [Customers] ([CustomerID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [NOTES]) VALUES (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, @Region, @PostalCode, @Country, @Phone, @Fax, @NOTES)" SelectCommand="SELECT * FROM [Customers]" UpdateCommand="UPDATE [Customers] SET [CompanyName] = @CompanyName, [ContactName] = @ContactName, [ContactTitle] = @ContactTitle, [Address] = @Address, [City] = @City, [Region] = @Region, [PostalCode] = @PostalCode, [Country] = @Country, [Phone] = @Phone, [Fax] = @Fax, [NOTES] = @NOTES WHERE [CustomerID] = @CustomerID"> …………… </asp:SqlDataSource> <asp:FormView ID="FormView1" runat="server" AllowPaging="True" BackColor="White" BorderColor="#999999" BorderStyle="None" BorderWidth="1px" CellPadding="3" DataKeyNames="CustomerID" DataSourceID="SqlDataSource1" GridLines="Vertical"> <EditItemTemplate> CustomerID: <asp:Label ID="CustomerIDLabel1" runat="server" Text='<%# Eval("CustomerID") %>' /> <br /> CompanyName: <asp:TextBox ID="CompanyNameTextBox" runat="server" Text='<%# Bind("CompanyName") %>' /> <br /> ContactName: <asp:TextBox ID="ContactNameTextBox" runat="server" Text='<%# Bind("ContactName") %>' /> <br /> ContactTitle: <asp:TextBox ID="ContactTitleTextBox" runat="server" Text='<%# Bind("ContactTitle") %>' /> <br /> Address: <asp:TextBox ID="AddressTextBox" runat="server" Text='<%# Bind("Address") %>' /> <br /> City: <asp:TextBox ID="CityTextBox" runat="server" Text='<%# Bind("City") %>' /> <br /> Region: <asp:TextBox ID="RegionTextBox" runat="server" Text='<%# Bind("Region") %>' /> <br /> PostalCode: <asp:TextBox ID="PostalCodeTextBox" runat="server" Text='<%# Bind("PostalCode") %>' /> <br /> Country: <asp:TextBox ID="CountryTextBox" runat="server" Text='<%# Bind("Country") %>' /> <br /> Phone: <asp:TextBox ID="PhoneTextBox" runat="server" Text='<%# Bind("Phone") %>' /> <br /> Fax: <asp:TextBox ID="FaxTextBox" runat="server" Text='<%# Bind("Fax") %>' /> <br /> NOTES: <asp:TextBox ID="NOTESTextBox" runat="server" Text='<%# Bind("NOTES") %>' /> <br /> <asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="Update" Text="Update" /> <asp:LinkButton ID="UpdateCancelButton" runat="server" CausesValidation="False" CommandName="Cancel" Text="Cancel" /> </EditItemTemplate> …………. </asp:FormView> <div> </div> </form> </body> </html> |
就想達到的目的而言,這個程式並沒有太大的問題,使用者可以透過這個程式來編修Customers資料表中的資料,也能新增新的客戶。
但若細細檢視這個應用程式,會發現其有以下的缺點:
- 使用SqlDataSource,所以綁死在SQL Server
- 沒有考慮延展性,所以當客戶要求新增資料時,如果CompanyName是空白,就直接以CustomerID寫入,未來只能依賴Trigger。
- 沒有考慮到Validation的延展機制,例如假設客戶要求CompanyName不能與CustomerID相同,那麼也只能夠依賴Trigger處理。
- ………………………………..
第一次嘗試挖洞遊戲
個人常比喻,設計一個具延展性的應用程式或Framework,就像玩挖洞遊戲一樣,首先得先預想完成後的成品,然後在這個成品中可能會被延展的部位挖上幾個洞,讓未來延展時有洞可鑽。
以前例而言,第一點可以使用其它現成的Framework,例如ADO.NET Entity Framework來解決,如非必要,實在不需勞駕自己來設計(雖說如此,但之後的文章會討論一個簡單的,可適用於多種資料庫的方法),
因此先對2、3點解決,這個問題出現的主因是未預想到客戶未來可能提出的需求變更,一旦預知其可能發生之後,便能在儲存資料的部分挖上一個洞,提供未來的延展性。
那洞該挖在哪呢?2、3點都是發生在資料儲存前,以前例而言,只要掛載FormView的Inserting及Updating事件即可,但問題在於現今無法準確的預知客戶未來只需要預設CompanyName或是避免
CustomerID與CompanyName重複的問題,他們有可能會有更複雜的邏輯運算,所以無法直接以上述需求寫死在程式中,最好的辦法是,在FormView的Inserting、Updating事件中動態的載入一個Assembly,
然後動態的建立物件,接著呼叫其特定的成員函式,也就是說,將驗證儲存的動作由主程式中抽離。
圖1
在圖1中加入了一個FormViewExt的類別,這個類別負責由web.config中的設定來讀取指定的Assembly,並掛載事件到指定的FormView中的Inserting、Updating。
FormViewExt.cs |
|
以下為Default.aspx.cs使用FormViewExt的程式碼。
Default.aspx.cs |
|
FormViewExt會透過web.config中AppSetting區段所指定的Assembly String來動態讀入Assembly並建立指定的物件,並在Inserting、Updating事件中呼叫其BeforeSave函式,此時會傳入一個IOrderDictionary,
這裡面包含了Inserting、Updating時的更新資料,由指定的物件來決定該怎麼處理,以下為此例的web.config。
web.config |
<?xmlversion="1.0"?>
<!-- For more information on how to configure your ASP.NET application, please visit http://go.microsoft.com/fwlink/?LinkId=169433 -->
<configuration> <connectionStrings> <addname="NorthwindConnectionString"connectionString="Data Source=127.0.0.1;Initial Catalog=Northwind;.... " providerName="System.Data.SqlClient"/> </connectionStrings> <appSettings> <addkey="Customers"value="SaveExt.CustomerExt, SaveExt"/> </appSettings> <system.web> <compilationdebug="true"targetFramework="4.0"/> </system.web>
</configuration> |
預設,當web.config的AppSetting區段有Customers定義時,FormViewExt才會動態載入指定的Assembly及物件,否則就直接略過。
本例中指定動態載入的Assembly為SaveExt,動態建立物件的類別為SaveExt.CustomerExt。
SaveExt.cs(in SaveExt Project) |
|
注意,SaveExt Project(一個類別庫專案,Class Library)的output目錄必須設在ASP.NET應用程式的bin目錄,也就是說,SaveExt Project所產出的DLL必須放在ASP.NET應用程式的bin目錄。
執行此應用程式,進行任何修改或新增並按下儲存後,會發現當未輸入CompanyName欄位時,其會預設為”Empty In save”。
這個架構給與了此程式初步的延展性,但這作法還不是很恰當,接下來讓我們更精煉化這個想法。
切出可重用部分-Simple Framework V1
在前例中雖做出了延展性,但若仔細思考,會發現FormViewExt中讀取AppSetting區段及動態載入Assembly、建立物件的部分,可以用在很多地方,現在是僅用在FormView上,改天可能GridView
或是其它地方都會有使用這個技巧的需求,所以這個部份可以往上提升,成為Framework的基底類別之一。
圖2
TypeLoader.cs(in SimpleFramework Project- a class library) |
|
FormViewExt.cs(in ASP.NET Project) |
|
這個步驟主要在於提取未來可共用的部分出來,形成Framework的一部份,圖3為現在方案的結構,注意! ASP.NET Project必須將SimpleFramework加為參考。
圖3
再次重構,切出更多可重用部分及定義延展規範-Simple Framework V2
前例中還是有一些精煉化的空間存在,首先,我們使用了Reflection來呼叫BeforeSave,這可能會造成未來的困擾,因為沒有規範,如果文件未載明,那麼只要字打錯就掛了,所以較好的做法
是在FormViewExt及被呼叫的類別間建立一個協定,介面是完成這個任務最快的方式。
另外,FromViewExt也存在被重用的價值,所以一併將其移往SimpleFramework專案中。
圖4
IDataExtension.cs(In SimpleFramework Project) |
|
FormViewExt.cs(in SimpleFramework Project) |
|
實作延展的CustomerExt也得依照規範實作IDataExtension介面。
CustomerExt.cs(in SaveExt Project) |
|
圖5為目前的方案結構。
去除相依性– Simple Framework V3
基本上,Simple Framework V2還算不錯,但其仍有精煉化的空間,因為TypeLoader其實可以用在非ASP.NET的專案上面,例如Windows Form或是WPF,但因為Simple Framework中包含了FormViewExt,
所以當Windows Form或是WPF想使用TypeLoader時,就需要System.Web存在,這雖然不會造成困擾,但許多設計者(包含我)都有種潔癖,沒用到的東西,就不需要載入(當使用於非ASP.NET專案時,FormViewExt是無用的)。
因此,這階段我們進行去除相依性的部分。
圖6
此例中將FormViewExt移往另一個Assembly(Class Library)SimpleFramework.Web中,當需要將TypeLoader用於Windows Form、WPF時,只需要引用SimpleFramework,
而不需要引用多餘且無用的SimpleFramework.Web。
FormViewExt.cs(in SimpleFramework.Web) |
|
圖7為目前方案結構。
圖7
真的可重用嗎?延展Simple Framework V3令其可作用於Windows Form上
設計是這樣設計,但真的可以沿用在Windows Form上嗎?請看圖8。
圖8
圖8中除了原本的SimpleFramework及SimpleFramework.Web(省略)之外,還多設計了一個SimpleFramework.WinForms,主要在於提供GridView類似FormView的機制,讓設計師可以透過同樣的機制來延展
資料儲存的驗證,注意!我們並未動到原本的SimpleFramework、SimpleFramework.Web,只是單純的加入SimpleFramework.WinForms而已。
GridViewExt.cs(in SimpleFramework.WinForms) |
|
接著只要建立一個WinForm專案,並放置GridView及BindingSource後,透過同樣的流程即可使用GridViewExt。
Form1.cs(in Windows Form Project) |
|
app.config |
<?xmlversion="1.0"encoding="utf-8"?> <configuration> <configSections> </configSections> <connectionStrings> <addname="WindowsFormsApplication1.Properties.Settings.NorthwindConnectionString" connectionString="Data Source=127.0.0.1;Initial Catalog=Northwind…" providerName="System.Data.SqlClient"/> </connectionStrings> <appSettings> <addkey="Customers"value="SaveExt.CustomerExt, SaveExt"/> </appSettings> </configuration> |
此程式的執行效果與ASP.NET差不多,差別只在於一個是ASP.NET、一個是Windows Form。
另外,此例證明了我們可以重用SimpleFramework在ASP.NET及Windows Form,且也可以透過SimpleFramework原本設計好的延展性來加入新功能,還有值得一提的是,同時也重用了SaveExt Project。
圖9
正名– 為類別及延展規範取個樣式(Patterns)名稱
好了,現在讀者們應該了解,為何有些應用程式開起來會有一堆Project的原因了,也了解了如何打造具延展性的應用程式,雖然還很陽春,但Simple Framework已經有一個Framework的雛型,未來的文章中
將繼續來精煉並加入更多功能。
文末,必須一提的是,Simple Framework其實使用了一個設計樣式(Design Pattern),名為Handler,Handler指的是當事件發生時的處理者,IDataExtension就是一個Handler,所以,建議讀者們可以將IDataExtension
改名為IDataHandler,或是IDataProcessHander,更貼近於軟體工程中的用語。
圖10
設計一個Framework或具延展性的應用程式這麼麻煩嗎?
恩,的確,過程很麻煩,但當你了解後,便可以在腦中走過前面的幾個嘗試,直接畫出圖10或圖6來,並直接進入Simple Framework V3或是V10的設計及實作,這是許多架構師本身就具備的能力,
也是應該要具備的能力。
是最終版本嗎? Simple Framework V3
不是,Simple Framework V3還稱不上是最終版本,因為以目的而言,這個延展點(Handler)應該放在資料層,而不是UI層,只是當這麼做時,將會引導出Data Access Layout的設計,讓本文變得
相對複雜及難懂,所以我特意讓其停留在UI層,未來的文章將持續改良。
不過,Simple Framework V3從UI延展的設計概念還是有用的,因為即使將延展點移往Data Access Layout,使用的手法還是一樣的。
範例下載: