[修練營ASP.NET]使用Spring.Net實現切層的專案架構
前言
之前Danny叔叔已經發過一篇快速使用Spring.Net的Adotemplate了,詳情請見:Spring.DataQuickStart
不過這篇只把Data Access Layer跟網頁分開,使用了OR-mapping讓查詢到的資料,以object的方式回傳。
最近點部落上有不少篇很好的切層概念的文章,
例如:
還有小的一篇寫的很亂的:[工作心得]傳統程式架構到3-Layer架構的心路歷程,架構圖的部分則可以參考:[專案心得]After Action Review
切層,是一個專案考量彈性、可維護性、可擴充性等特點,在設計上無可避免的架構。
然而切層的implement方式也有相當多,
最常見的範例就是MS PetShop:http://msdn.microsoft.com/en-us/library/aa479070.aspx
下載網址:http://download.microsoft.com/download/8/0/1/801ff297-aea6-46b9-8e11-810df5df1032/Microsoft%20.NET%20Pet%20Shop%204.0.msi
然而,在Java的世界裡,Spring這個framework已經存在許久,相關的技術也相當成熟,
也因為Spring的確有它的好處,所以在ASP.NET的世界裡面,
也出現了Spring.Net的framework,由於Spring的framework涵蓋的模組相當多,
這邊要舉的範例是使用到Spring的Data.Adotemplate、Factory、Dependency injection、Transaction的控制以及OR-mapping的技術。
希望可以幫助大家對Spring與切層有更大的幫助。
Just Play
一、下載所需資源
這邊絕大部分可以參考Danny叔叔那一篇。
- 資料庫:
一樣是使用NorthWind,
下載網址:http://www.microsoft.com/downloads/details.aspx?FamilyID=06616212-0356-46A0-8DA2-EEBC53A68034&displaylang=en - Spring.Net:
可以使用1.2.0的版本,小的是抓1.3.0 RC1,不過用的dll仍然是1.2.0的,
下載網址:http://www.springframework.net/download.html
二、設定專案參考
安裝完上述的資料庫與Spring.Net後,接著我們要建立一個新的方案,
裡面有著網站(Presentation layer)與我們的類別庫(Business Layer+Data Access Layer),在我的範例裡,我的類別庫專案命名為Core。
接著將需要參考的Spring.Net的dll加入到Core的專案參考裡。
我這邊是還有加入log4net來方便記錄相關的log,下載網址:http://logging.apache.org/log4net/download.html
接著在網站上,把Core專案加入參考。
三、開始要玩設定檔了
先在Core專案裡,增加一個folder為「SpringConfig」,裡面新增一個xml檔,命名為object.xml,這個檔案我們會很常修改到,
他要用來存放一些Spring的相關設定,
例如
- DB的provider是用SQL還是Oracle
- 我們的DB connection相關設定
- 使用AdoTemplate的模組
- Row mapper的模組
- Transaction管理的method name Pattern
- IoC的設定
<!-- MS SQL Server 設定 -->
<object type="Spring.Objects.Factory.Config.PropertyPlaceholderConfigurer, Spring.Core">
<property name="ConfigSections" value="databaseSettings"/>
</object>
<db:provider id="DbProvider"
provider="SqlServer-2.0"
connectionString="Server=${db.datasource};Database=${db.database};User ID=${db.user};Password=${db.password};"/>
<object id="adoTemplate" type="Spring.Data.Core.AdoTemplate, Spring.Data">
<property name="DbProvider" ref="DbProvider"/>
<property name="DataReaderWrapperType" value="Spring.Data.Support.NullMappingDataReader, Spring.Data"/>
</object>
這個區塊,要更動到的應該只有根據DBMS,會動到provider="SqlServer-2.0",
詳情可參考:http://www.springframework.net/doc-latest/reference/html/dbprovider.html
其他的部分在套用到別的專案上,應該都不需要修改。
值得注意的是connection stirng的設定部分,會mapping到webconfig上的databaseSettings區塊。
<databaseSettings>
<add key="db.datasource" value="Your DB"/>
<add key="db.user" value="Your userName"/>
<add key="db.password" value="Your passWord"/>
<add key="db.database" value="Northwind"/>
</databaseSettings>
而object.xml的路徑,則也需要在webconfig指定路徑,所以NameSpace會影響到路徑的名稱。
<resource uri="assembly://Core/Core.SpringConfig/objects.xml"/>,指的就是會去讀取Core.dll裡面的Core.SpringConfig裡面的object.xml設定檔。
<spring>
<parsers>
<parser type="Spring.Data.Config.DatabaseNamespaceParser, Spring.Data"/>
</parsers>
<context>
<resource uri="assembly://Core/Core.SpringConfig/objects.xml"/>
</context>
</spring>
Webconfig上額外要增加與Spring相關的設定只剩下如何使用Spring.Core的部分。
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/>
<section name="parsers" type="Spring.Context.Support.NamespaceParsersSectionHandler, Spring.Core"/>
</sectionGroup>
再來要介紹的Spring裡面很方便的Transaction Management,在object.xml裡面,新增這樣的設定。
<object id="TxProxyConfigurationTemplate" abstract="true"
type="Spring.Transaction.Interceptor.TransactionProxyFactoryObject, Spring.Data">
<property name="PlatformTransactionManager" ref="TransactionManager"/>
<property name="TransactionAttributes">
<name-values>
<!-- 你要把哪些 method(s) 納入交易控管 -->
<add key="Insert*" value="PROPAGATION_REQUIRED" />
<add key="*Update*" value="PROPAGATION_REQUIRED" />
<add key="Delete*" value="PROPAGATION_REQUIRED" />
<add key="Create*" value="PROPAGATION_REQUIRED" />
<add key="Save*" value="PROPAGATION_REQUIRED"/>
<add key="Copy*" value="PROPAGATION_REQUIRED"/>
<add key="Afresh*" value="PROPAGATION_REQUIRED"/>
<add key="TransferCase" value="PROPAGATION_REQUIRED"/>
</name-values>
</property>
</object>
如同註解所說,要把哪一些符合Naming Pattern的加入transaction的控管,
例如method名稱為"Insert*",Insert開頭的Method要啟動transaction。"Delete*"指的是名稱Delete開頭的method要啟動transaction。
我們範例的設計是,當網頁呼叫Service或Domain object的Method時,該操作如果會更新DB的話,method名稱需符合config檔設定的Method Naming Pattern。
這樣transaction的管理就變得很單純,是以每一個service的操作為一個交易,也更符合了現實世界的抽象邏輯。
一個交易的開始,是網頁呼叫了會更新DB資料的Service method,啟動交易。
不論在這個Service裡面更新了多少資料,只要其中造成了錯誤,這個service就不夠完整,交易就算失敗,
所有資料便會rollback,而傳exception回Presentation layer。
詳細的說明可以參考:http://www.springframework.net/doc-latest/reference/html/tx-quickstart.html
再來object.xml裡面,最常新增的區塊,也就是IoC的部分,
原理請參考:http://martinfowler.com/articles/injection.html
用法請參考:http://www.springframework.net/doc-latest/reference/html/quickstarts.html
簡單的講,在用法上只需要看成哪個Service需要用到哪一些Dao,Dao則需要使用到AdoTemplate,例如:
<object id="Region" type="Core.Domain.Region, Core">
<property name="DaoRegion" ref="DaoRegion"/>
</object>
<object id="DaoRegion" type="Core.Dao.DataAccess.DaoRegion, Core">
<property name="AdoTemplate" ref="adoTemplate" />
</object>
這樣的寫法,可以想像成Core.Domain.Region這個物件裡面,要用到DaoRegion這個物件。
DaoRegion則透過AdoTemplate去DB取值。
以上,就是痛苦的Config檔設定煉獄!
其實,熟悉了以後,會發現設定config沒什麼痛苦的,反而體會到了改設定不用改code是件很快樂的事情。
四、Interface、OR-mapping與AdoTemplate
Interface的目的,是為了implement Strategy pattern,搭配Spirng.net的IoC,
未來要抽換執行邏輯與演算法的時候,只需要額外新增另一個object來implement該interface即可。
例如一個interface可能是提款機的「提款」method,我們建了一個屬於中X信託的class,這邊假設叫做「中X提款」class來實作這個提款的method,
裡面可能有著該銀行對手續費的定義、演算法。
然而,當我們的程式拿到國X世華時,他們一樣有提款機「提款」的method,
假設其他的部分都跟中X信託一樣不需要改變,只有該method的邏輯不同。
這個時候我們可以增加一個叫做「國X提款」的class來實作這個提款的method,接著在設定檔把提款method實作指定為國X提款即可。
這樣子可以讓擴充性和可維護性提升許多。
OR-mapping的部分,則是讓原本的資料集合,更加的貼近現實世界的「物件觀念」。
把資料集合物件化,甚至物件與物件之間能夠互動,(一堆資料是沒法子互相互動的),
物件有著描述自己的屬性,具備著行為,對分析與實作維護上,都會更加抽象。
AdoTemplate則可以省掉一些Ado.Net語法,包括惱人的SQL injection,
也不需要每次執行SQL statement都要自己起一個connection。
搭配Nhibernate模組的話,甚至可以不用考慮DBMS的差異,可以參考:http://www.springframework.net/doc-latest/reference/html/nh-quickstart.html
我們先將該建立的folder建立好:
- Dao
- DataAccess:用來存放實際要執行的SQL statement
- Interface:用來定義DataAccess物件的介面
- RowMapper:用來讓回傳的資料集合為物件型態
- Domain
- Interface:用來定義Domain object的屬性與行為的介面
- Service
- Interface:用來定義Service提供了哪些方法的介面
我們接下來的例子,以單表維護為例,維護的為NorthWind裡面的Region這張表,按照一般開發的邏輯,來介紹上述這些東西怎麼設計。
首先,Region裡有著RegionID與RegionDescription兩個欄位,分別是int與nchar(50),
所以我們先來設計一下,我們的Domain object的interface。
新增一個interface,叫做IRegion,裡面有兩個屬性,分別為
- Description:型態為String,對應欄位型態nchar。
這邊可以發現屬性的名稱是用來描述現實世界這個Region物件的特性,而不是完全按照DB Column name來取。 - ID:型態為int?,對應欄位型態為int,雖然這個欄位不能null,但是宣告成int?,可以方便我們在DAO時作判斷。
由於我們這邊的例子,是希望透過Region這個物件,就可以作單表維護,所以我們的Region要能提供這樣的行為。
新增四個method,分別為
- void DeleteRegion();
- IList GetRegion();
- void InsertRegion(IRegion objRegion);
- void UpdateRegion(IRegion objRegion);
namespace Core.Domain.Interface
{
public interface IRegion
{
#region Data Members (2)
string Description {get;set;}
int? Id {get;set;}
#endregion Data Members
#region Operations (4)
/// <summary>
/// Deletes the region.
/// </summary>
void DeleteRegion();
/// <summary>
/// Gets the region.
/// </summary>
/// <returns></returns>
IList GetRegion();
/// <summary>
/// Inserts the region.
/// </summary>
/// <param name="objRegion">The obj region.</param>
void InsertRegion(IRegion objRegion);
/// <summary>
/// Updates the region.
/// </summary>
/// <param name="objRegion">The obj region.</param>
void UpdateRegion(IRegion objRegion);
#endregion Operations
}
}
有了Region的interface後,我們在Domain的folder下新增一個類別,叫做Region,實作IRegion。
新增完只需要在:IRegion的部分,用滑鼠右鍵選實作介面即可。
接著請跟上我的tempo,
因為我們需要針對Region這張表作增刪改查的動作,所以我們必須新增一個Dao來幫我們存取DB的動作。
所以我們先到DAO的Interface底下,新增一個IDaoRegion的介面,提供基本的增刪改查的method。
namespace Core.Dao.Interface
{
interface IDaoRegion
{
IList GetReigon(int? ID);
void UpdateRegion(IRegion objRegion);
void DeleteRegion(int? ID);
void InsertRegion(IRegion objRegion);
}
}
這一層Interface,則是提供未來DB來源可能是呼叫Web service,可能是讀文字檔、XML、Excel等等格式,
可以達到抽換的目的。
接著我們一樣在DataAccess的folder底下,新增一個DaoRegion的類別,實作IDaoRegion介面。
這邊要注意的地方,我們的DAO class除了要實作IDaoRegion外,還要繼承AdoDaoSupport,才能使用AdoTemplate。
Parameter的部分,就跟Ado.NET的用法差不多,使用@為偵測字元,代表parameter,重複出現,只需要指定一次值即可。
要記得留意DbType的部分。
namespace Core.Dao.DataAccess
{
public class DaoRegion : AdoDaoSupport, Interface.IDaoRegion
{
#region IDaoRegion 成員
public IList GetReigon(int? ID)
{
string whereSQL = ID == null ? string.Empty : " where RegionID= @RegionID";
string sql = @"select * from Region " + whereSQL;
IDbParameters objParameters = CreateDbParameters();
if (ID != null)
{
objParameters.Add("RegionID", DbType.Int16).Value = ID;
}
return AdoTemplate.QueryWithRowMapper(CommandType.Text, sql, new RegionRowMapper(), objParameters);
}
public void UpdateRegion(IRegion objRegion)
{
string sql = @"Update Region
SET RegionDescription=@Description
Where RegionID=@RegionID";
IDbParameters objParameters = CreateDbParameters();
objParameters.Add("Description", DbType.String).Value = objRegion.Description;
objParameters.Add("RegionID", DbType.Int16).Value = objRegion.Id;
int ExecCount = AdoTemplate.ExecuteNonQuery(CommandType.Text, sql, objParameters);
if (ExecCount == 0)
{
throw new Exception("修改失敗");
}
}
#endregion
#region IDaoRegion 成員
public void DeleteRegion(int? ID)
{
string sql = @"Delete Region Where RegionID=@RegionID";
IDbParameters objParameters = CreateDbParameters();
objParameters.Add("RegionID", DbType.Int16).Value = ID;
int ExecCount = AdoTemplate.ExecuteNonQuery(CommandType.Text, sql, objParameters);
if (ExecCount == 0)
{
throw new Exception("刪除失敗");
}
}
#endregion
#region IDaoRegion 成員
public void InsertRegion(IRegion objRegion)
{
string sql = @"Insert into Region(RegionID,RegionDescription) values(@RegionID,@RegionDescription)";
IDbParameters objParameters = CreateDbParameters();
objParameters.Add("RegionID", DbType.Int16).Value = objRegion.Id;
objParameters.Add("RegionDescription", DbType.String).Value = objRegion.Description;
int ExecCount = AdoTemplate.ExecuteNonQuery(CommandType.Text, sql, objParameters);
if (ExecCount == 0)
{
throw new Exception("新增失敗");
}
}
#endregion
}
}
接著眼尖的人可以發現,在GetRegion的部分,有使用到AdoTemplate.QueryWithRowMapper(),
這個method理的第三個參數需要用到RowMapper,也就是將篩選回來的資料集合,與物件結合。
所以我們可以去RowMapper的folder底下,新增一個RegionRowMapper類別,
記得rowmapper的class要實作IRowMapper的介面,接著只需要把IDataReader讀到欄位的值,餵給object的屬性即可。
namespace Core.Dao.RowMapper
{
public class RegionRowMapper:IRowMapper
{
public object MapRow(IDataReader reader, int rowNum)
{
IRegion region = new Region();
region.Id = CovertType.DbToInt(reader["RegionID"]);
region.Description = CovertType.DbToStr(reader["RegionDescription"]);
return region;
}
}
}
請注意reader[欄位名稱]是對應到DB schema的column name,OR-mapping的過程就在這邊。
接著,我們要在region的method裡面,去使用這些DAO,來達到目的。
namespace Core.Domain
{
public class Region:Interface.IRegion
{
#region Fields (1)
private IDaoRegion daoRegion = null;
#endregion Fields
#region Properties (2)
public string Description
{
get;
set;
}
public int? Id
{
get;
set;
}
#endregion Properties
#region Methods (1)
// Public Methods (1)
/// <summary>
/// Gets the region.
/// </summary>
/// <returns></returns>
public IList GetRegion()
{
return daoRegion.GetReigon(this.Id);
}
#endregion Methods
#region IRegion 成員
/// <summary>
/// Updates the region.
/// </summary>
/// <param name="objRegion">The obj region.</param>
public void UpdateRegion(Interface.IRegion objRegion)
{
daoRegion.UpdateRegion(objRegion);
}
/// <summary>
/// Deletes the region.
/// </summary>
public void DeleteRegion()
{
daoRegion.DeleteRegion(this.Id);
}
/// <summary>
/// Inserts the region.
/// </summary>
/// <param name="objRegion">The obj region.</param>
public void InsertRegion(Interface.IRegion objRegion)
{
daoRegion.InsertRegion(objRegion);
}
#endregion
}
}
我們宣告了一個daoRegion來操作,請注意,這邊Domain object的行為,就具備了domain object的邏輯,
也就是我們的Domain object的行為是屬於Business Logic layer,
Dao只會單純的對DB做存取操作,而不會包含太多邏輯,邏輯的部分應該交由BLL這層來負責。
YA,如此一來,大功告成。我們的Service與DAO與Domain object的基本架構就成型了。
(Service的部分就更單純,只會有method,一樣宣告要使用到的dao,呼叫dao)
不過還沒結束,還有個東西還沒介紹到,
這邊我們還要使用到Spring裡面的ContextRegistry.GetContext(),來幫我們讀取在設定檔上定義的物件。
我們把這段動作抽到RepositoryFactory.cs與WebUtility.cs兩支程式裡面,
(不過剛跟同事聊了一下,他的建議是讀取物件應該要每一個物件都開一個function出來,Core才能跟網頁實際切開,
這樣未來要替換名字的時候,才不用改到網頁的code)
這邊其實基本觀念我也還不是很熟,所以列出多一點ref供大家參考:
http://www.springframework.net/doc-latest/reference/html/quickstarts.html
http://tech.ddvip.com/2009-01/1231914502105819.html
http://marscommentary.blogspot.com/2008/06/springnetioc.html
最後,就是要怎麼在網頁上操作domain object跟service,去存取DB啦。
五、在網頁使用Domain object
網頁這邊的code,就相對簡單很多了,
只需要知道呼叫哪個service或是怎麼設定domain object屬性,怎麼呼叫domain object的operation,
其他精神就只要放在怎麼控制畫面的呈現。
首先,我們宣告一個IRegion來使用。在Page_load的時候初始化該物件。
private Core.Domain.Interface.IRegion region;
protected void Page_Load(object sender, EventArgs e)
{
region = (Core.Domain.Interface.IRegion)Core.WebUtility.Repository.Domain("Region");
}
接著要增刪改查,只需要呼叫region的method即可。
例如:
查詢GridView資料
private void GridViewBinding()
{
IList regionList = region.GetRegion();
this.GridView1.DataSource = regionList;
this.GridView1.DataBind();
}
刪除GridView資料
protected void GridView1_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
GridViewRow oRow = ((GridView)(sender)).Rows[e.RowIndex];
OrderedDictionary oFieldValues = GridViewFunctions.ExtractRowValues(GridView1.Columns, oRow);
region.Id = Convert.ToInt16(oFieldValues["Id"]);
try
{
region.DeleteRegion();
ShowMessage(Getmessage("DelSuccess"));
region.Id = null;
GridViewBinding();
}
catch (Exception ex)
{
logger.Warn(Getmessage("DelFailed"), ex);
ShowMessage(Getmessage("DelFailed"));
}
}
修改與新增Region資料
Region DTOregion = new Region();
DTOregion.Id = Convert.ToInt16(this.txtEditRegionID.Text);
DTOregion.Description = this.txtEditRegionDescription.Text;
switch ((status)this.ViewState["status"])
{
case status.Query:
break;
case status.Insert:
try
{
region.InsertRegion(DTOregion);
ShowMessage(Getmessage("InsertSuccess"));
GridViewBinding();
}
catch (Exception ex)
{
logger.Warn(Getmessage("InsertFailed"), ex);
ShowMessage(Getmessage("InsertFailed"));
this.txtEditRegionDescription.Text = string.Empty;
this.txtEditRegionID.Text = string.Empty;
}
break;
case status.Update:
try
{
region.UpdateRegion(DTOregion);
ShowMessage(Getmessage("UpdSuccess"));
GridViewBinding();
}
catch (Exception ex)
{
logger.Warn(Getmessage("UpdFailed"), ex);
ShowMessage(Getmessage("UpdFailed"));
}
break;
case status.Delete:
break;
default:
break;
}
訊息歷程
以上,就是很簡單的單表維護,使用Spring.Net來輔助切層,以NorthWind資料庫為底,以jQuery和AJAX control toolkit來強化client端呈現的基本例子。
希望各位前輩可以給點指導的意見,因為其實小的對一些概念也還不是瞭解的很透徹,只是個Spring.Net的初學者。
也希望可以對害怕切層,不知道切層是什麼,不知道怎麼實作切層的人有幫助。
隨文附上Source Code:SpringSample.rar
註1:2009/09/19,公司Tony大師指出錯誤,再透過讀取Spring設定檔物件時,應使用interface,而非實體物件,故修正為
region = (Core.Domain.Interface.IRegion)Core.WebUtility.Repository.Domain("Region");
註2:domain object的operation,應透過service layer來操縱Dao,而非直接下邏輯判斷操作Dao。好處是即便operation實作方式改變,但仍不會影響到domain object的domain concept
blog 與課程更新內容,請前往新站位置:http://tdd.best/