asp.net mvc應用架構的思考--Unity的應用及三層代碼


 

最近要做一個項目,和國外的架構師聊過之后。對方提到了他准備采用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的解決方案。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM