The Framework Designing (4) – Abstract Data Layout


切出Data Layout,通常是一個資料庫應用程式最初、也是最重要的部分,或許有些初學者對此感到困惑,是的!你可以用SqlDataSource做出客戶資料的編修畫面,

但一旦牽扯到商業邏輯,SqlDataSource絕對不會是選項,硬要使用的話會成為負擔。

想像一下,當設計訂單編修畫面時,你可以使用SqlDataSource來呈現訂單表頭及表身的編輯動作,但儲存前後庫存的控管就一定得回到ADO.NET處理,這時商業

邏輯便會呈現出與UI混雜的窘境,整個應用程式也會變得難以維護。

The Framework Designing (4) – Abstract Data Layout

 

 

/黃忠成

 

 

a simple application

 

  切出Data Layout,通常是一個資料庫應用程式最初、也是最重要的部分,或許有些初學者對此感到困惑,是的!你可以用SqlDataSource做出客戶資料的編修畫面,

但一旦牽扯到商業邏輯,SqlDataSource絕對不會是選項,硬要使用的話會成為負擔。

  想像一下,當設計訂單編修畫面時,你可以使用SqlDataSource來呈現訂單表頭及表身的編輯動作,但儲存前後庫存的控管就一定得回到ADO.NET處理,這時商業

邏輯便會呈現出與UI混雜的窘境,整個應用程式也會變得難以維護。

  切出Data Layout最初的動機,通常也就是分割資料存取、商業邏輯與UI,其它的如可抽換的商業邏輯或是可抽換的Data Layout就只是更深一層的考量而已。

  就架構上而言,不管是ASP.NET或WinForm、WPF,都應該要將應用程式設計成如圖1的基礎架構,也就是切出一個DataModel專門處理資料存取及商業邏輯的部分。

圖1

具現於ASP.NET專案的樣子就像圖2。

圖2

這個範例(Step1目錄)中將DataSet/TableAdapters放在另一個Class Library中,然後於WebApplication1專案中以ObjectDataSource來結合,這種架構在需要處理訂單與

庫存間的關係時,程式碼會寫在DataModel中,而不是包含UI的WebApplication。

 

designing database-independent data layout

 

  誠如先前提到的,切出Data Layout的最初目的是隔離UI及資料處理,但接下來的那一步通常就會出現可切換資料庫的需求,這有很多種解決辦法,最簡單的就是乾脆使用

ADO.NET Entity Framework或NHibernate等OR/Mapping Framework。

  不過有時我們可能會因為不熟悉這些Framework或是其它原因而捨其不用,改採透過架構設計的方式來達到可切換資料庫的需求,此時通常需要由Data Layout中切出一小塊

Data Access Layout,然後令其成為可抽換的部分,如圖3所示。

圖3

這個範例(Step2)跟前例不同的是,其將TableAdapters部分由DataModel中抽離,成為一個名為SqlDataModel的Class Library,而主應用程式透過app.config的設定來載入,

當資料庫由Sql Server換成Oracle時,只需要撰寫一個OracleDataModel並修改app.config,即可讓整個應用程式由使用Sql Server資料庫變成Oracle。

 

Default.aspx.cs(in WebApplication1 project)


using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Configuration;

using SimpleFramework;


namespace WebApplication1

{

    public partial class Default : System.Web.UI.Page

    {

        protected void Page_Init(object sender, EventArgs e)

        {

            string[] typeInfos = ConfigurationManager.AppSettings["Customers"].Split(',');

            Type type = TypeLoader.LoadType(typeInfos[1], typeInfos[0]);

            ObjectDataSource1.TypeName = type.FullName;

        }


        protected void Page_Load(object sender, EventArgs e)

        {

        }

    }

}

web.config

<?xmlversion="1.0"?>

<configuration>

    <system.web>

        <compilationdebug="true"targetFramework="4.0"/>

    </system.web>

  <connectionStrings>

    ………………….

  </connectionStrings>

  <appSettings>

    <addkey="Customers"value="SqlDataModel.SqlCustomersModel, SqlDataModel"/>

  </appSettings>

</configuration>

當然,看起來是很簡單,但如果仔細去查看DataModel及SqlDataModel的內容,會發現其必須要定出很多的函式來符合UI存取資料庫的需求,當使用OR/Mapping Framework時,

就可以跳過這些繁瑣的步驟,直接開始撰寫實際的應用程式。另外有一點需特別提出來,那就是在設計Data Access Layout時,不要朝可應用於其他不同系統的角度出發,因為這樣

一來就會設計出類似於ADO.NET Entity Framework的架構,而這個本來就不是我們所預期的結果,而且也沒那麼多時間可以實作這種東西。

 

 

make robust contract

 

  前例的缺點是偷懶應用了ObjectDataSource使用Reflection的特點,跳過了定義應用程式與SqlDataModel間的協定,實務上,你絕對會需要自己建立SqlDataModel並呼叫其特定的函式,

例如當搜尋客戶編號時需呼叫SqlDataModel的GetCustomerByID函式,如果採用前例的作法,就只能透過Reflection呼叫了,這是很沒有效率及很危險的手法,定義兩者互通的介面才是

真正的解決之道。

圖4

此範例的原始碼於Step3目錄中,在此也首次看到了此架構的另一個特色,那就是可以讓Web Application/Win Form Application共用一個Data Layout,也就是共用資料層。

 

from client/server to multi-tier with designed data layout

 

   現今的中大型資料庫應用程式,多半會採用Multi-Tier的系統架構,也就是說將資料存取層放置於Web Service之中,Step4目錄中就是這種架構的範例程式。

圖5

圖5有兩個亮點,一是其將原本直接參考DataModel的部分,改為呼叫WCF Service,也就是說Web Application、WIn Form Application的資料層就是WCF Service,

第二點是Data Service也就是擔任資料層的WCF Service還是沿用動態載入Data Access Layout的部分,其仍然擁有可抽換式Data Access Layout的特色。

 

dataset or not?

 

  當將應用程式由Client/Server架構移轉為Multi-Tier架構時,最常遭遇的選擇就是是否還要使用DataSet/DataTable,如果UI是ASP.NET/WinForm/WPF的話,那麼

這個問題就不是那麼重要了,因為三者皆能妥善的處理WCF Service間DataSet/DataTable的傳遞,但如果需要提供Silverlight或是與Java Client Application互動時,

DataSet/DataTable先天上複雜的XML結構就會成為阻礙。

  這時只能夠選擇解析DataSet/DataTable的XML或是另外定義Data Objects(又稱為Data Transfer Object Pattern)來傳遞,如圖6的架構。

圖6

首先要把Data Objects給定義出來,這應該定義於DataModel Project中。

 

NorthwindModel.cs


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;


namespace DataModel

{

    public class Customers

    {

        public string CustomerID { get; set; }

        public string CompanyName { get; set; }

        public string ContactName { get; set; }

        public string ContactTitle { get; set; }

        public string Address { get; set; }

        public string City { get; set; }

        public string Region { get; set; }

        public string PostalCode { get; set; }

        public string Country { get; set; }

        public string Phone { get; set; }

        public string Fax { get; set; }

        public string NOTES { get; set; }

    }

}

接著要修改ICustomersModel的介面定義。

 

DataModelIntf.cs


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;


namespace DataModel

{

    public interface ICustomersModel

    {

        int Delete(Customers dataItem);

        int Insert(Customers dataItem);

        int Update(Customers dataItem);

        List GetData(

                          int startRowIndex, int maximumRows);

        int GetCount(int startRowIndex, int maximumRows);

    }

}

最後實作放置於SqlCustomersModel專案中。

 

SqlCustomersModelImpl.cs


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Reflection;

using DataModel;


namespaceSqlDataModel

{

    public class SqlCustomersModelImpl:ICustomersModel

    {

        private SqlCustomersModel _model = null;


        public SqlCustomersModelImpl()

        {

            _model = new SqlCustomersModel();

        }


        public int Delete(Customers dataItem)

        {

            return _model.Delete(dataItem.CustomerID, dataItem.CompanyName,

                                 dataItem.ContactName, dataItem.ContactTitle,

                                 dataItem.Address, dataItem.City,

                                 dataItem.Region, dataItem.PostalCode,

                                 dataItem.Country, dataItem.Phone,

                                 dataItem.Fax, dataItem.Fax);

        }


        public int Insert(Customers dataItem)

        {

            return _model.Insert(dataItem.CustomerID, dataItem.CompanyName,

                                 dataItem.ContactName, dataItem.ContactTitle,

                                 dataItem.Address, dataItem.City,

                                 dataItem.Region, dataItem.PostalCode,

                                 dataItem.Country, dataItem.Phone,

                                 dataItem.Fax, dataItem.Fax);

        }


        public int Update(Customers dataItem)

        {

            return _model.Update(dataItem.CustomerID, dataItem.CompanyName, dataItem.ContactName, dataItem.ContactTitle, dataItem.Address,

                                 dataItem.City, dataItem.Region, dataItem.PostalCode, dataItem.Country, dataItem.Phone, dataItem.Fax, dataItem.NOTES);

        }


        public List GetData(int startRowIndex, int maximumRows)

        {

            Northwind.CustomersDataTable table = _model.GetData(startRowIndex, maximumRows);

            List results = new List();

            foreach (Northwind.CustomersRow row in table.Rows)

            {

                results.Add(new Customers()

                {

                    CustomerID = row.CustomerID,

                    CompanyName = row.CompanyName,

                    ContactName = row.IsContactNameNull() ? null : row.ContactName,

                    ContactTitle = row.IsContactTitleNull() ? null : row.ContactTitle,

                    Address = row.IsAddressNull() ? null : row.Address,

                    City = row.IsCityNull() ? null : row.City,

                    Region = row.IsRegionNull() ? null : row.Region,

                    PostalCode = row.IsPostalCodeNull() ? null : row.PostalCode,

                    Country = row.IsCountryNull() ? null : row.Country,

                    Phone = row.IsPhoneNull() ? null : row.Phone,

                    Fax = row.IsFaxNull() ? null : row.Fax,

                    NOTES = row.IsNOTESNull() ? null : row.NOTES                

                });

            }

            return results;

    }


        public int GetCount(int startRowIndex, int maximumRows)

        {

            return _model.GetCount(startRowIndex, maximumRows);

        }

    }

}

這樣一來,ASP.NET/Windows Form/Silverlight就能共用此DataService,達到多UI共用單一Service層UI的架構,完整範例於Step6目錄中。

 

 

what’s we not say?

 

 

   一切似乎都很美好不是,不同UI可以共用一個Service層,而Service層的Data Access Layout也是可抽換的,可達到不變UI/Service的情況下將SQL Server改為Oracle,那還有什麼不滿的呢?

有的,那就是當客戶不需要這種Multi-Tier架構, 覺得使用一般的Client/Server架構就可,還可省略掉Service層讓系統達到更高的效率。

  其實,我們一開始為Client/Server所設計的架構就能適應這些需求,只要將Data Objects的概念加入,該架構不但可以不換UI的情況下由Client/Server轉變為Multi-Tier,關鍵就在於可抽換的

DataModel層,如圖7所示。

圖7

圖7中,當應用程式是Client/Server架構時,就直接使用SqlDataModel做為DataModel,形成不需要Service層的架構,日後若客戶需要,可以直接把SqlDataModel換成DataServiceModel,

由它來代理與Data Service溝通,這樣就直接變成Multi-Tier了,Step7目錄中是此架構的完整範例。

 

範例下載: http://code6421.myweb.hinet.net/Framework/Arch_3.zip