最近要做一個項目,和國外的架構師聊過之后。對方提到了他准備采用asp.net mvc, jquery, Unity 等技術來代替老的技術: 如asp.net web form. 他請我幫他想一些關於架構的東西。一直以來,關於asp.net mvc應用的架構,有一些想法。正好借這個機會寫出來。資深的人士可能已經知道了,就當是復習吧。歡迎發表意見。指出不足。
Unity的應用
Unity出來已經有幾年了。早幾年的1.2版就可以實現這里所說的功能。目前最新穩定版是2.1。正在開發的3.0也許會給我們帶來更強大的功能。這里寫的是如何利用Unity實現aop, 降低代碼的耦合程度。高層的代碼依賴於一個抽象的接口,具體實現的代碼是由Unity容器里的具體實現類來完成的。因此Unity容器負責創建具體實現類的實例,將其映射到抽象接口上。這樣做是為了降低代碼的耦合度。另外我們的應用程序里面有很多公共的部分,如logging, Exception處理,數據庫事務處理等都可以通過aop的方式來實現。試想每一個商業方法里都得寫調用logging, Exception處理的代碼,我們開發人員一定會累的夠嗆。通過Unity實現了aop,將這些公共的代碼從每一個商業方法里抽取出來,讓我們的代碼看上去更清爽。
用Unity降低代碼的耦合度
為了降低代碼的耦合度,需要讓高層代碼依賴於一個抽象的接口。然后具體的實現類就實現這個抽象接口,在Unity容器里注冊抽象接口到實現類的映射。即指定了抽象接口映射到某個具體實現類。
具體的實現代碼如下:
<register type="BusinessLogic.IBLLUserForm, BusinessLogic" mapTo="BusinessLogic.BLLUserForm, BusinessLogic"> </register> <register type="BusinessLogic.IBLLUserMenu, BusinessLogic" mapTo="BusinessLogic.BLLUserMenu, BusinessLogic"> </register>
這里“BusinessLogic"是名稱空間。IBLLUserForm和IBLLUserMenu是抽象接口。其名稱空間是BusinessLogic。大家可以發現,具體實現類BLLUserForm和BLLUserMenu也是在BusinessLogic名稱空間里,這個並不重要,抽象接口和具體實現類可以在同一個名稱空間也可以不同名稱空間。關鍵的是抽象類與具體實現類的映射是通過Unity容器來管理的。通過register這個方式向Unity容器表明這個抽象接口和具體實現類的映射關系。這樣的映射關系可以在需要的時候改變,比如將來因業務需求出現新的情況,需要一個新的具體實現類叫BLLUserForm_blabla.那么可以用BLLUserForm_blabla代替原來的BLLUserForm, 就只需要改一下配置文件就可以了。原來的代碼甚至不用重新編譯。試想不用Unity,那么高層代碼必須得依賴於具體實現類,因為,即使有抽象接口,抽象接口本身是無法直接創建實例的。高層代碼必須得用new操作符來創建一個具體實現類的實例,這樣就產生了對具體實現類的直接依賴。這樣代碼的耦合度就變高了。Unity是一個容器,是一個制造對象的工廠,你需要多少對象,它就制造多少對象。同時能管理這些抽象接口與具體實現類的映射關系。這就是人們常說的依賴於抽象的好處。
現實當中,也許會出現抽象接口IBLLUserForm等也得改的情況,這種情況的出現是由於OOA/OOD做的不到位,沒有正確地發現抽象。得重新審視當時做的分析和設計。
用Unity實現aop,讓代碼清爽
商業方法中訪問數據庫,做業務處理難免會遇到未預見的問題,當出現問題時,不希望將此問題信息直接拋給用戶,而是希望將問題的詳細技術信息寫入log。這是一個公用的做法。每一個商業方法都來寫try catch,再調用做logging的類來寫log。這樣就會出現很多的重復代碼。好在Unity可以幫我們省去這些重復代碼。看看如何做:
這是代碼1, 實現Microsoft.Practices.Unity.InterceptionExtension.ICallHandler接口。實現Invoke方法。"var retvalue = getNext()(input, getNext);"是調用被攔截的方法。if (retvalue.Exception != null) 判斷retvalue.Exception是判斷被攔截方法是否有Exception。如果有就將Exception寫log。
using System; using System.Data; using System.Data.Common; using System.Collections.Generic; using Microsoft.Practices.Unity.InterceptionExtension; using DataAccessCommon; using CommonUtil; namespace BusinessLogic { public class MyDBHandler : ICallHandler { private int iOrder = 0; public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext) { var retvalue = getNext()(input, getNext); // call the intercepting method if (retvalue.Exception != null) { SysLog.GetInstance().LogError(retvalue.Exception); } return retvalue; } public int Order { get { return iOrder; } set { iOrder = value; } } } }
這是代碼2, 是一個handler attribute, 這個attribute用來標記是否要Unity進行攔截。所有需要攔截的抽象接口都要加上這個attribute. 在下面代碼4配置文件里的攔截policy會看抽象接口上是否有這個attribute,有的話就會進行攔截。
using System; using System.Collections.Generic; using Microsoft.Practices.Unity; using Microsoft.Practices.Unity.InterceptionExtension; namespace BusinessLogic { public class MyDBHandlerAttribute : HandlerAttribute { public override ICallHandler CreateHandler(IUnityContainer container) { return new MyDBHandler(); } } }
代碼3, 這是一個很簡單的輔助類,用來幫助在代碼4的攔截配置里指定一個自定義attribute的類型。
using System; using System.Collections.Generic; using Microsoft.Practices.Unity; using Microsoft.Practices.Unity.InterceptionExtension; namespace BusinessLogic { public class GetTypeConverter : System.ComponentModel.TypeConverter { public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { return Type.GetType(value.ToString()); } } }
代碼4, 這是一個攔截配置,其關鍵點在callHandler和matchingRule上。callHandler指定了用BusinessLogic.MyDBHandler類型來進行攔截處理。MyDBHandler已經在代碼1里定義好(見上面)。matchingRule,這里用的是CustomAttributeMatchingRule,即自定義的Attribute作為攔截條件。CustomAttributeMatchingRule要求兩個構造函數參數,一個是Type, 一個是否使用繼承的類型. 在指定Type時,用的是一個字符串:"BusinessLogic.MyDBHandlerAttribute, BusinessLogic", 所以需要typeConverter將其轉換成Type。
<interception> <policy name="mypolicy"> <callHandler name="myHandler1" type="BusinessLogic.MyDBHandler, BusinessLogic"></callHandler> <matchingRule name="myrule" type="CustomAttributeMatchingRule"> <constructor> <param name="attributeType" type="System.Type, mscorlib"> <value value="BusinessLogic.MyDBHandlerAttribute, BusinessLogic" typeConverter="BusinessLogic.GetTypeConverter, BusinessLogic" /> </param> <param name="inherited" type="bool"> <value value="true" /> </param> </constructor> </matchingRule> </policy> </interception>
代碼5, 相應地在類型注冊當中也加入攔截器和policyinjection的設定。這里用的是InterfaceInterceptor。如下:
<register type="BusinessLogic.IBLLUserForm, BusinessLogic" mapTo="BusinessLogic.BLLUserForm, BusinessLogic"> <interceptor name="myinterceptor" type="InterfaceInterceptor" isDefaultForType="true" /> <policyInjection /> </register> <register type="BusinessLogic.IBLLUserMenu, BusinessLogic" mapTo="BusinessLogic.BLLUserMenu, BusinessLogic"> <interceptor name="myinterceptor" type="InterfaceInterceptor" isDefaultForType="true" /> <policyInjection /> </register>
關於數據庫的事務,如果你的數據庫任務相對比較簡單(比如僅CRUD),數據庫事務size比較小,可以用System.Transactions.TransactionScope,那么可以下面的一段代碼實現超簡單aop方式的事務管理:
代碼6, 這是替換代碼1的一個片段:
using (TransactionScope ts = new TransactionScope()) { var retvalue = getNext().Invoke(input, getNext); if (retvalue.Exception != null) { SysLog.GetInstance().LogError(retvalue.Exception); } else { ts.Complete(); } return retvalue }
var retvalue = getNext().Invoke(input, getNext);這句是調用被攔截的方法。即我們的商業方法。我們可以在攔截之前,即此語句之前做一些logging的工作,還可以在此語句之后做一些logging的工作。視具體要求而定。有了Exception處理,和相應的logging,還有數據庫事務的處理。那么商務邏輯的類和相應的數據庫訪問的類就變得相對簡單了。原來需要顯式地創建事務對象,現在不需要了,現在的商務方法默認就是打開了事務管理的。在商務方法里的所有數據庫操作都會被看成是同一個數據庫事務。在商務方法結束以后會自動進行數據庫事務的提交。
三層代碼
代碼7 抽象接口的代碼,其引用了自定義的attribute "MyDBHandler",這個attribute會帶來aop方式的Exception, logging, 和事務處理。
using System; namespace BusinessLogic { [MyDBHandler] public interface IBLLUserMenu { int AddUserMenu(DataEntity.UserMenu usermenu); int DeleteUserMenu(DataEntity.UserMenu usermenu); DataEntity.UserMenu FindAUserMenu(DataEntity.UserMenu usermenu); System.Collections.Generic.List<DataEntity.UserMenu> GetAllUserMenu(); int SelectCountUserMenu(); int SelectCountWhereClauseUserMenu(DataEntity.UserMenu usermenu); System.Collections.Generic.List<DataEntity.UserMenu> SelectGroupByPrimaryKeyUserMenu(); System.Collections.Generic.List<DataEntity.UserMenu> SelectMenusByApplicationID(DataEntity.UserMenu usermenu); System.Collections.Generic.List<DataEntity.UserMenu> SelectOrderByPrimaryKeyUserMenu(); System.Collections.Generic.List<DataEntity.UserMenu> SelectTopUserMenu(); int UpdateUserMenu(DataEntity.UserMenu usermenu); object CONNECTION { get; set; } DataEntity.UserMenu USERMENU { get; set; } System.Collections.Generic.List<DataEntity.UserMenu> USERMENU_LIST { get; set; } } }
代碼8 商務類及其方法, 經過aop處理之后,每一個方法都有Exception的處理, logging, 和事務控制。
using System; using System.Collections.Generic; using System.Data; using DataEntity; using DataAccess; using DataAccessCommon; using CommonUtil; namespace BusinessLogic { internal class BLLUserMenu : BusinessLogic.IBLLUserMenu { private readonly DataAccess.DALUserMenu dal = new DataAccess.DALUserMenu(); private object conn = null; private UserMenu usermenu; private List<UserMenu> usermenus; public object CONNECTION { get { return conn; } set { conn = value; } } public UserMenu USERMENU { get { return usermenu; } set { usermenu = value; } } public List<UserMenu> USERMENU_LIST { get { return usermenus; } set { usermenus = value; } } #region business logic method public int DeleteUserMenu(UserMenu usermenu) { return dal.DeleteUserMenu(conn,usermenu); } public int UpdateUserMenu(UserMenu usermenu) { return dal.UpdateUserMenu(conn,usermenu); } public int AddUserMenu(UserMenu usermenu) { return dal.AddUserMenu(conn,usermenu); } public List<UserMenu> GetAllUserMenu() { return dal.GetAllUserMenu(conn); } public UserMenu FindAUserMenu(UserMenu usermenu) { return dal.FindAUserMenu(conn,usermenu); } public System.Int32 SelectCountUserMenu() { return dal.SelectCountUserMenu(conn); } public System.Int32 SelectCountWhereClauseUserMenu(UserMenu usermenu) { return dal.SelectCountWhereClauseUserMenu(conn,usermenu); } public List<UserMenu> SelectTopUserMenu() { return dal.SelectTopUserMenu(conn); } public List<UserMenu> SelectOrderByPrimaryKeyUserMenu() { return dal.SelectOrderByPrimaryKeyUserMenu(conn); } public List<UserMenu> SelectGroupByPrimaryKeyUserMenu() { return dal.SelectGroupByPrimaryKeyUserMenu(conn); } public List<UserMenu> SelectMenusByApplicationID(UserMenu usermenu) { return dal.SelectMenusByApplicationID(conn, usermenu); } #endregion } }
代碼9 有多個數據庫操作的商務方法, 上面的商務類及其方法是比較簡單的,所以一個商務方法里只有一個數據庫操作. 很多情況下是有多個的,甚至是比較復雜的。如下面的例子:
public bool MoveUpItem(UserMenuItem usermenuitem) { usermenuitem = dal.FindAUserMenuItem(conn, usermenuitem); UserMenuItem previousMenuItem = dal.SelectPreviousMenuItem(conn, usermenuitem); int iTempOrdinal = usermenuitem.ORDINAL; usermenuitem.ORDINAL = previousMenuItem.ORDINAL; previousMenuItem.ORDINAL = iTempOrdinal; dal.UpdateUserMenuItem(conn, usermenuitem); dal.UpdateUserMenuItem(conn, previousMenuItem); return true; } public bool MoveDownItem(UserMenuItem usermenuitem) { usermenuitem = dal.FindAUserMenuItem(conn, usermenuitem); UserMenuItem nextMenuItem = dal.SelectNextMenuItem(conn, usermenuitem); int iTempOrdinal = usermenuitem.ORDINAL; usermenuitem.ORDINAL = nextMenuItem.ORDINAL; nextMenuItem.ORDINAL = iTempOrdinal; dal.UpdateUserMenuItem(conn, usermenuitem); dal.UpdateUserMenuItem(conn, nextMenuItem); return true; }
代碼10, 數據訪問的代碼, 基本都是這個模式, 構造參數,CRUD的sql語句,然后調用db helper執行。這些數據訪問的方法返回類型是這幾種:int, 單個商務實體,商務實體的列表,bool. int 通常表示影響到的記錄數,也可以是某個整形字段的值, 單個商務實體代表數據庫里一條記錄。商務實體的列表代表數據庫里多個符合條件的記錄。這些都轉換成了商務實體類。
using System; using System.Collections.Generic; using System.Data; using DataEntity; using DataAccessCommon; namespace DataAccess { public class DALUserForm { #region data access methods public int DeleteUserForm(Object conn, UserForm userform) { List<MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> { new MyStaticDBHelper.MyDBParameter("@FormID", DbType.Int32, userform.FORMID) }; string strSQL = "DELETE FROM [UserForm] WHERE [FormID] = @FormID"; int result = 0; result = MyStaticDBHelper.ExecuteNonQuery(conn, System.Data.CommandType.Text, strSQL, paras); return result; } public int UpdateUserForm(Object conn, UserForm userform) { List<MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> { new MyStaticDBHelper.MyDBParameter("@FormName", DbType.String, userform.FORMNAME), new MyStaticDBHelper.MyDBParameter("@TableID", DbType.Int32, userform.TABLEID), new MyStaticDBHelper.MyDBParameter("@SingleOrList", DbType.Boolean, userform.SINGLEORLIST), new MyStaticDBHelper.MyDBParameter("@WithCRUD", DbType.Boolean, userform.WITHCRUD), new MyStaticDBHelper.MyDBParameter("@ApplicationID", DbType.Int32, userform.APPLICATIONID), new MyStaticDBHelper.MyDBParameter("@FormID", DbType.Int32, userform.FORMID) }; string strSQL = "UPDATE [UserForm] SET [FormName] = @FormName, [TableID] = @TableID, [SingleOrList] = @SingleOrList, [WithCRUD] = @WithCRUD, [ApplicationID] = @ApplicationID WHERE [FormID] = @FormID"; int result = 0; result = MyStaticDBHelper.ExecuteNonQuery(conn, System.Data.CommandType.Text, strSQL, paras); return result; } public int AddUserForm(Object conn, UserForm userform) { List<MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> { new MyStaticDBHelper.MyDBParameter("@FormName", DbType.String, userform.FORMNAME), new MyStaticDBHelper.MyDBParameter("@TableID", DbType.Int32, userform.TABLEID), new MyStaticDBHelper.MyDBParameter("@SingleOrList", DbType.Boolean, userform.SINGLEORLIST), new MyStaticDBHelper.MyDBParameter("@WithCRUD", DbType.Boolean, userform.WITHCRUD), new MyStaticDBHelper.MyDBParameter("@ApplicationID", DbType.Int32, userform.APPLICATIONID), new MyStaticDBHelper.MyDBParameter("@FormID", DbType.Int32, userform.FORMID) }; string strSQL = "INSERT INTO [UserForm] ( [FormName] , [TableID] , [SingleOrList] , [WithCRUD] , [ApplicationID] ) VALUES( @FormName, @TableID, @SingleOrList, @WithCRUD, @ApplicationID ); SELECT SCOPE_IDENTITY() as [FormID]"; int result = 0; DataSet ds = null; ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL, paras); if (ds.Tables.Count > 0 && ds.Tables[0].Rows.Count > 0){ userform.FORMID = Convert.ToInt32(ds.Tables[0].Rows[0][0]); result = 1; } return result; } public List<UserForm> GetAllUserForm(Object conn) { string strSQL = "SELECT * FROM [UserForm] "; DataSet ds = null; ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL); return DataMapper.MapDataTableToObjectList<UserForm>(ds.Tables[0]); } public UserForm FindAUserForm(Object conn, UserForm userform) { List<MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> { new MyStaticDBHelper.MyDBParameter("@FormID", DbType.Int32, userform.FORMID) }; string strSQL = "SELECT * FROM [UserForm] WHERE [FormID] = @FormID"; DataSet ds = null; ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL, paras); return DataMapper.MapDataTableToSingleRow<UserForm>(ds.Tables[0]); } //省略了大部分 #endregion } }
代碼11 商務實體類的代碼
using System; using System.Collections.Generic; namespace DataEntity { public class UserMenu { #region private members private int _iMenuID; private string _strMenuName; private int _iApplicationID; private int _iMenuStyle; private int _iScheme; #endregion #region Properties public int MENUID { get { return _iMenuID; } set { _iMenuID = value; } } public string MENUNAME { get { return _strMenuName; } set { _strMenuName = value; } } public int APPLICATIONID { get { return _iApplicationID; } set { _iApplicationID = value; } } public int MENUSTYLE { get { return _iMenuStyle; } set { _iMenuStyle = value; } } public int SCHEME { get { return _iScheme; } set { _iScheme = value; } } #endregion } }
如果不用TransactionScope
嚴格的說,TransactionScope有些局限性。人們已經發現不少的TransactionScope的局限(baidu, google搜搜就有好多)。不管是否分布式事務,只要是規模比較大,復雜度比較高,最好都不要用TransactionScope。數據庫事務規模小,復雜度低,用TransactionScope還是不錯的。一旦規模變大,復雜度變高,TransactionScope帶來的問題會很棘手。最后就只有棄用TransactionScope。問題也來了,不用TransactionScope那用什么呢。TransactionScope還是帶來編程的一些方便,寫一個using子句就可以了,它可以管理非分布式數據庫事務,也可以管理分布式數據庫事務。大家可以去看園友artech的一篇文章: http://www.cnblogs.com/artech/archive/2012/01/05/custom-transaction-scope.html, 他們提出來一個類似於TransactionScope用法的類。不過他們提出的類暫時不支持分布式事務。如果是非分布式的,那么可以借用。另外需要說的是artech在這篇文章里貼的代碼證明一例TransactionScope的問題。當插入記錄到了100000條,一次性提交事務,TransactionScope居然掛了。這還只是單數據庫,不知道更復雜的,更大的會如何。希望大家集思廣益,早點找出一個完美代替TransactionScope的解決方案。