[修練營ASP.NET]使用Spring.Net輔助切層的專案架構

  • 39003
  • 0
  • 2010-01-21

[修練營ASP.NET]使用Spring.Net實現切層的專案架構

前言

之前Danny叔叔已經發過一篇快速使用Spring.Net的Adotemplate了,詳情請見:Spring.DataQuickStart
不過這篇只把Data Access Layer跟網頁分開,使用了OR-mapping讓查詢到的資料,以object的方式回傳。

最近點部落上有不少篇很好的切層概念的文章,
例如:

  1. 邁向架構師的暖身運動(5):系統開發的分層概念

  2. [修練營ASP.NET]淺談多層式架構 (Multi Tiers)

  3. 還有小的一篇寫的很亂的:[工作心得]傳統程式架構到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叔叔那一篇。

 

二、設定專案參考

安裝完上述的資料庫與Spring.Net後,接著我們要建立一個新的方案,
裡面有著網站(Presentation layer)與我們的類別庫(Business Layer+Data Access Layer),在我的範例裡,我的類別庫專案命名為Core。

接著將需要參考的Spring.Net的dll加入到Core的專案參考裡。

加入Spring參考


我這邊是還有加入log4net來方便記錄相關的log,下載網址:http://logging.apache.org/log4net/download.html


接著在網站上,把Core專案加入參考。

加入參考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。

3layerArchitecture_thumb

詳細的說明可以參考: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提供了哪些方法的介面

folder 

我們接下來的例子,以單表維護為例,維護的為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的部分,用滑鼠右鍵選實作介面即可。

實作regionInterface

 

接著請跟上我的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

http://geekswithblogs.net/AmusinglyMOSS/archive/2009/07/05/service-architecture-using-spring.net.aspx

 

最後,就是要怎麼在網頁上操作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/