讓 ASP.NET WebForm 頁面好還要更好 (一) - 職責分離 (中)

根據上次的內容,繼續完成更新產品,也繼續將資料存取層完成。讓範例更接近真實應用。

參考資料

[ADO.NET] 為何 / 如何 使用 SQLParameter 物件

物件導向設計原則:SOLID + DI

泛型字典

 

上次提到怎麼把資料庫讀取程式重構到單獨類別,範例程式中都有個小數一直變化,不曉得有沒有人注意到。

那指的是距離主題完成目標還有多遠,上次才停留在 0.4 ,這次就讓我們慢慢補足剩下的部分。

首先,上次範例都只有讀取產品清單,難道這樣可以完成上頭派下來的任務嗎?當然不可能,光是產品就應該有增刪修查,再加上與其他表也會有互動。這樣才是一個正常的任務。

我們試著把這樣的功能完成,並且依照上次的做法重構,當然我們還是從爛程式開始,一步步重構。為簡化範例程式,只做到能編輯就好了。

 

先提一下,我的爛程式中,有十分嚴重的 SQL Injection 的問題,這會是在任何重構之前的優先修正事項。

 

第一步,先假設我們受命完成產品編輯,其中包括剛才提到的增刪修查。根據老寫法,如果把清單及明細寫在同一頁,大概頁面及程式會長這樣。

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Demo5.aspx.cs" Inherits="WebApplication1.Demo5" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Data" %>
<script runat="server">
   protected void Page_Init(object sender, EventArgs e)
   {
       // 這裡是上次改的
       WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();
       
       List<WebApplication1.DataObject.ProductValueObject> list = da.getProducts();
       this.GridView1.DataSource = list;
       this.GridView1.DataBind();

       this.FormView1.Visible = false;
   }

   protected void Page_Load(object sender, EventArgs e)
   {
 
   }
   

   protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
   {
       string ProductNo = e.CommandArgument.ToString();
       this.HF_ProductNo.Value = ProductNo;

       try
       {
           string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

           using (SqlConnection conn = new SqlConnection(connStr))
           {
               // 拜託拜託,這個寫法絕對有 SQL Injection 的資安問題
               // 此範例為待改進的不良範例,請勿照抄,至少也請看完下方的改進一
               string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = '" + ProductNo + "'";

               SqlCommand cmd = new SqlCommand(SELECTCmd, conn);
               conn.Open();
               var dt = new DataTable();

               SqlDataReader reader = cmd.ExecuteReader();
               dt.Load(reader);

               this.FormView1.DataSource = dt;
               this.FormView1.DataBind();

               this.FormView1.Visible = true;
           }
       }
       catch (Exception ex)
       {
           // Do Something 
       }
   }

   protected void FormView1_ItemCommand(object sender, FormViewCommandEventArgs e)
   {
       if (e.CommandName == "CancelCmd")
       {
           this.FormView1.Visible = false;
           this.HF_ProductNo.Value = "-1";
       }
     
       
       if (e.CommandName == "UpdateCmd")
       {
           this.getProductAndUpdate();
       }
   }
   

   private void getProductAndUpdate()
   {
       var ctlProductNoLabel = this.FormView1.FindControl("ProductNoLabel");
       var ctlEan13TextBox = this.FormView1.FindControl("Ean13TextBox");
       var ctlProductTypeTextBox = this.FormView1.FindControl("ProductTypeTextBox");
       var ctlTempTextBox = this.FormView1.FindControl("TempTextBox");
       var ctlExpireTextBox = this.FormView1.FindControl("ExpireTextBox");
       var ctlLengthTextBox = this.FormView1.FindControl("LengthTextBox");
       var ctlWidthTextBox = this.FormView1.FindControl("WidthTextBox");
       var ctlHeightTextBox = this.FormView1.FindControl("HeightTextBox");
       var ctlWeightTextBox = this.FormView1.FindControl("WeightTextBox");

       string ProductNoLabel = ((ITextControl)ctlProductNoLabel).Text;
       string Ean13TextBox = ((ITextControl)ctlEan13TextBox).Text;
       string ProductTypeTextBox = ((ITextControl)ctlProductTypeTextBox).Text;
       string TempTextBox = ((ITextControl)ctlTempTextBox).Text;
       string ExpireTextBox = ((ITextControl)ctlExpireTextBox).Text;
       string LengthTextBox = ((ITextControl)ctlLengthTextBox).Text;
       string WidthTextBox = ((ITextControl)ctlWidthTextBox).Text;
       string HeightTextBox = ((ITextControl)ctlHeightTextBox).Text;
       string WeightTextBox = ((ITextControl)ctlWeightTextBox).Text;

       try
       {
           string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

           using (SqlConnection conn = new SqlConnection(connStr))
           {
               // 拜託拜託,這個寫法絕對有 SQL Injection 的資安問題
               // 此範例為待改進的不良範例,請勿照抄,至少也請看完下方的改進一
               string UpdateCmd =
                   " UPDATE Product " +
                   " SET    Ean13 = '" + Ean13TextBox + "', ProductType = '" + ProductTypeTextBox + "', Temp = '" + TempTextBox + "', Expire = '" + ExpireTextBox + "', " +
                   "       Length = '" + LengthTextBox + "', Width = '" + WidthTextBox + "', Height = '" + HeightTextBox + "', Weight = '" + WeightTextBox + "' " +
                   " WHERE  ProductNo = '" + ProductNoLabel + "' ";

               SqlCommand cmd = new SqlCommand(UpdateCmd, conn);
               conn.Open();
               cmd.ExecuteNonQuery();
               conn.Close();
           }
       }
       catch (Exception ex)
       {
           // Do Something 
       }
   }
</script>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
   <title></title>
</head>
<body>
   <form id="form1" runat="server">
       <asp:HiddenField ID="HF_ProductNo" runat="server" Value="-1" />
       <asp:GridView ID="GridView1" runat="server" EnableViewState="false" AutoGenerateColumns="false" OnRowCommand="GridView1_RowCommand">
           <Columns>
               <asp:BoundField DataField="ProductNo" HeaderText="ProductNo" ReadOnly="True" SortExpression="ProductNo" />
               <asp:BoundField DataField="Ean13" HeaderText="Ean13" SortExpression="Ean13" />
               <asp:BoundField DataField="ProductType" HeaderText="ProductType" SortExpression="ProductType" />
               <asp:BoundField DataField="Temp" HeaderText="Temp" SortExpression="Temp" />
               <asp:BoundField DataField="Expire" HeaderText="Expire" SortExpression="Expire" />
               <asp:BoundField DataField="Length" HeaderText="Length" SortExpression="Length" />
               <asp:BoundField DataField="Width" HeaderText="Width" SortExpression="Width" />
               <asp:BoundField DataField="Height" HeaderText="Height" SortExpression="Height" />
               <asp:BoundField DataField="Weight" HeaderText="Weight" SortExpression="Weight" />
               <asp:TemplateField>
                   <ItemTemplate>
                       <asp:Button runat="server" CommandArgument='<%# Eval("ProductNo") %>' CommandName="GoEdit" Text="this is a Pan." />
                   </ItemTemplate>
               </asp:TemplateField>
           </Columns>
       </asp:GridView>
       <asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductNo" DefaultMode="Edit" OnItemCommand="FormView1_ItemCommand">
           <EditItemTemplate>
               ProductNo:<asp:Label ID="ProductNoLabel" runat="server" Text='<%# Eval("ProductNo") %>' /><br />
               Ean13:<asp:TextBox ID="Ean13TextBox" runat="server" Text='<%# Bind("Ean13") %>' /><br />
               ProductType:<asp:TextBox ID="ProductTypeTextBox" runat="server" Text='<%# Bind("ProductType") %>' /><br />
               Temp:<asp:TextBox ID="TempTextBox" runat="server" Text='<%# Bind("Temp") %>' /><br />
               Expire:<asp:TextBox ID="ExpireTextBox" runat="server" Text='<%# Bind("Expire") %>' /><br />
               Length:<asp:TextBox ID="LengthTextBox" runat="server" Text='<%# Bind("Length") %>' /><br />
               Width:<asp:TextBox ID="WidthTextBox" runat="server" Text='<%# Bind("Width") %>' /><br />
               Height:<asp:TextBox ID="HeightTextBox" runat="server" Text='<%# Bind("Height") %>' /><br />
               Weight:<asp:TextBox ID="WeightTextBox" runat="server" Text='<%# Bind("Weight") %>' /><br />
               <asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="UpdateCmd" Text="更新" />&nbsp;
               <asp:LinkButton ID="UpdateCancelButton" runat="server" CausesValidation="False" CommandName="CancelCmd" Text="取消" />
           </EditItemTemplate>
       </asp:FormView>
   </form>
</body>
</html>

範例看完了,來看看怎麼改進。第一件最重要的事,不是重構,而是摘掉資安問題,套小朱常在論壇上講的「沒有修正 SQL Injection 之前,重構沒有意義。」所以第一件事,先來把所有 SQL Injection 都做基本處理。

 

先用 SQLParameter 好好來處理一下參數的傳遞,本範例目前也只打算做到這等級,關於其它資安問題,之後有機會再來寫。所以讓我們把程式部份改成這樣 (0.4 -> 0.6) :

protected void Page_Init(object sender, EventArgs e)
{
    // 這裡是上次改的
    WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();
    
    List<WebApplication1.DataObject.ProductValueObject> list = da.getProducts();
    this.GridView1.DataSource = list;
    this.GridView1.DataBind();

    this.FormView1.Visible = false;
}

protected void Page_Load(object sender, EventArgs e)
{

}

protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
    string ProductNo = e.CommandArgument.ToString();
    this.HF_ProductNo.Value = ProductNo;

    try
    {
        string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

        using (SqlConnection conn = new SqlConnection(connStr))
        {
            // 摘掉 SQL Injection 就是神清氣爽,心情都好起來了呢
            string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = @ProductNo";
      
            
            SqlCommand cmd = new SqlCommand(SELECTCmd, conn);
            cmd.Parameters.AddWithValue("@ProductNo", ProductNo);   
            
            conn.Open();
            var dt = new DataTable();

            SqlDataReader reader = cmd.ExecuteReader();
            dt.Load(reader);

            this.FormView1.DataSource = dt;
            this.FormView1.DataBind();

            this.FormView1.Visible = true;
        }
    }
    catch (Exception ex)
    {
        // Do Something 
    }
}

protected void FormView1_ItemCommand(object sender, FormViewCommandEventArgs e)
{
    if (e.CommandName == "CancelCmd")
    {
        this.FormView1.Visible = false;
        this.HF_ProductNo.Value = "-1";
    }
 
    
    if (e.CommandName == "UpdateCmd")
    {
        this.getProductAndUpdate();
    }
}

private void getProductAndUpdate()
{
    var ctlProductNoLabel = this.FormView1.FindControl("ProductNoLabel");
    var ctlEan13TextBox = this.FormView1.FindControl("Ean13TextBox");
    var ctlProductTypeTextBox = this.FormView1.FindControl("ProductTypeTextBox");
    var ctlTempTextBox = this.FormView1.FindControl("TempTextBox");
    var ctlExpireTextBox = this.FormView1.FindControl("ExpireTextBox");
    var ctlLengthTextBox = this.FormView1.FindControl("LengthTextBox");
    var ctlWidthTextBox = this.FormView1.FindControl("WidthTextBox");
    var ctlHeightTextBox = this.FormView1.FindControl("HeightTextBox");
    var ctlWeightTextBox = this.FormView1.FindControl("WeightTextBox");

    string ProductNoLabel = ((ITextControl)ctlProductNoLabel).Text;
    string Ean13TextBox = ((ITextControl)ctlEan13TextBox).Text;
    string ProductTypeTextBox = ((ITextControl)ctlProductTypeTextBox).Text;
    string TempTextBox = ((ITextControl)ctlTempTextBox).Text;
    string ExpireTextBox = ((ITextControl)ctlExpireTextBox).Text;
    string LengthTextBox = ((ITextControl)ctlLengthTextBox).Text;
    string WidthTextBox = ((ITextControl)ctlWidthTextBox).Text;
    string HeightTextBox = ((ITextControl)ctlHeightTextBox).Text;
    string WeightTextBox = ((ITextControl)ctlWeightTextBox).Text;

    try
    {
        string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

        using (SqlConnection conn = new SqlConnection(connStr))
        {
            // 摘掉 SQL Injection 就是神清氣爽,心情都好起來了呢
            string UpdateCmd =
                " UPDATE Product " +
                " SET    Ean13 = @Ean13, ProductType = @ProductType, Temp = @Temp, Expire = @Expire, " +
                "       Length = @Length, Width = @Width, Height = @Height, Weight = @Weight " +
                " WHERE  ProductNo = @ProductNo ";

            SqlCommand cmd = new SqlCommand(UpdateCmd, conn);

            cmd.Parameters.AddWithValue("@Ean13", Ean13TextBox);
            cmd.Parameters.AddWithValue("@ProductType", ProductTypeTextBox);
            cmd.Parameters.AddWithValue("@Temp", TempTextBox);
            cmd.Parameters.AddWithValue("@Expire", ExpireTextBox);
            cmd.Parameters.AddWithValue("@Length", LengthTextBox);
            cmd.Parameters.AddWithValue("@Width",WidthTextBox );
            cmd.Parameters.AddWithValue("@Height", HeightTextBox);
            cmd.Parameters.AddWithValue("@Weight", WeightTextBox);
            
            cmd.Parameters.AddWithValue("@ProductNo", ProductNoLabel);   
            
            
            
            conn.Open();
            cmd.ExecuteNonQuery();
            conn.Close();
        }
    }
    catch (Exception ex)
    {
        // Do Something 
    }
}

 

使用了 SQLParameter ,不再透過組字串操作資料庫,底層組件會幫你處理好。如果你的專案中到處都還是組字串操作資料庫,拜託立刻,立刻!改成用 SQLParameter 來傳遞變數。

經過這個修改後,我們繼續進行重構。仿照上次的寫法,將查詢單筆資料以及更新資料的語法都放到 DataAccess 。所以現在程式碼如下 (0.6~0.65) :

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections.Generic;

namespace WebApplication1.DataObject
{
   public class DataAccess
   {
       private ProductValueObject parseDataToObject(IDataReader row)
       {
           var returnObj = new ProductValueObject();

           returnObj.ProductNo = row["ProductNo"].ToString();
           returnObj.Ean13 = row["Ean13"].ToString();
           returnObj.ProductType = row["ProductType"].ToString();
           returnObj.Temp = row["Temp"].ToString();
           returnObj.Expire = row["Expire"].ToString();
           returnObj.Length = (double)row["Length"];
           returnObj.Width = (double)row["Width"];
           returnObj.Height = (double)row["Height"];
           returnObj.Weight = (double)row["Weight"];

           return returnObj;
       }

       public List<ProductValueObject> getProducts()
       {
           var returnList = new List<ProductValueObject>();

           try
           {
               string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

               using (SqlConnection conn = new SqlConnection(connStr))
               {
                   SqlCommand cmd = new SqlCommand("SELECT * FROM Product", conn);
                   conn.Open();
                   var dt = new DataTable();

                   SqlDataReader reader = cmd.ExecuteReader();

                   while (reader.Read())
                   {
                       var productItem = this.parseDataToObject(reader);
                       returnList.Add(productItem);
                   }
               }
           }
           catch (Exception ex)
           { 
               // Do Something
           }

           return returnList;
       }

       protected ProductValueObject getProduct(string ProductNo)
       {
           ProductValueObject obj = null;

           try
           {
               string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

               using (SqlConnection conn = new SqlConnection(connStr))
               {
                   string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = @ProductNo";

                   SqlCommand cmd = new SqlCommand(SELECTCmd, conn);
                   cmd.Parameters.AddWithValue("@ProductNo", ProductNo);

                   conn.Open();
                   var dt = new DataTable();

                   SqlDataReader reader = cmd.ExecuteReader();

                   while (reader.Read())
                   {
                       obj = this.parseDataToObject(reader);
                   }
               }
           }
           catch (Exception ex)
           {
               // Do Something 
           }

           return obj;
       }

       private void updateProduct(string ProductNo, string Ean13, string ProductType, string Temp, string Expire, string Length, string Width, string Height, string Weight)
       {
           try
           {
               string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

               using (SqlConnection conn = new SqlConnection(connStr))
               {
                   string UpdateCmd =
                       " UPDATE Product " +
                       " SET    Ean13 = @Ean13, ProductType = @ProductType, Temp = @Temp, Expire = @Expire, " +
                       "       Length = @Length, Width = @Width, Height = @Height, Weight = @Weight " +
                       " WHERE  ProductNo = @ProductNo ";

                   SqlCommand cmd = new SqlCommand(UpdateCmd, conn);

                   cmd.Parameters.AddWithValue("@Ean13", Ean13);
                   cmd.Parameters.AddWithValue("@ProductType", ProductType);
                   cmd.Parameters.AddWithValue("@Temp", Temp);
                   cmd.Parameters.AddWithValue("@Expire", Expire);
                   cmd.Parameters.AddWithValue("@Length", Length);
                   cmd.Parameters.AddWithValue("@Width", Width);
                   cmd.Parameters.AddWithValue("@Height", Height);
                   cmd.Parameters.AddWithValue("@Weight", Weight);

                   cmd.Parameters.AddWithValue("@ProductNo", ProductNo);

                   conn.Open();
                   cmd.ExecuteNonQuery();
                   conn.Close();
               }
           }
           catch (Exception ex)
           {
               // Do Something 
           }
       }
   }
}

原本頁面中充斥了直接存取資料庫的語法,現在全都改呼叫 DataAccess 的程式,頁面再度瘦身,內容如下:

protected void Page_Init(object sender, EventArgs e)
{
   // 這裡是上次改的
   WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();
   
   List<WebApplication1.DataObject.ProductValueObject> list = da.getProducts();
   this.GridView1.DataSource = list;
   this.GridView1.DataBind();

   this.FormView1.Visible = false;
}

protected void Page_Load(object sender, EventArgs e)
{

}


protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
   string ProductNo = e.CommandArgument.ToString();
   this.HF_ProductNo.Value = ProductNo;

   WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();

   WebApplication1.DataObject.ProductValueObject prodVO = da.getProduct(ProductNo);
   List<WebApplication1.DataObject.ProductValueObject> list = new List<WebApplication1.DataObject.ProductValueObject>();

   list.Add(prodVO);

   this.FormView1.DataSource = list;
   this.FormView1.DataBind();

   this.FormView1.Visible = true;
}

protected void FormView1_ItemCommand(object sender, FormViewCommandEventArgs e)
{
   if (e.CommandName == "CancelCmd")
   {
	   this.FormView1.Visible = false;
	   this.HF_ProductNo.Value = "-1";
   }
 
   
   if (e.CommandName == "UpdateCmd")
   {
	   this.getProductAndUpdate();
   }
}


private void getProductAndUpdate()
{
   var ctlProductNoLabel = this.FormView1.FindControl("ProductNoLabel");
   var ctlEan13TextBox = this.FormView1.FindControl("Ean13TextBox");
   var ctlProductTypeTextBox = this.FormView1.FindControl("ProductTypeTextBox");
   var ctlTempTextBox = this.FormView1.FindControl("TempTextBox");
   var ctlExpireTextBox = this.FormView1.FindControl("ExpireTextBox");
   var ctlLengthTextBox = this.FormView1.FindControl("LengthTextBox");
   var ctlWidthTextBox = this.FormView1.FindControl("WidthTextBox");
   var ctlHeightTextBox = this.FormView1.FindControl("HeightTextBox");
   var ctlWeightTextBox = this.FormView1.FindControl("WeightTextBox");

   string ProductNoLabel = ((ITextControl)ctlProductNoLabel).Text;
   string Ean13TextBox = ((ITextControl)ctlEan13TextBox).Text;
   string ProductTypeTextBox = ((ITextControl)ctlProductTypeTextBox).Text;
   string TempTextBox = ((ITextControl)ctlTempTextBox).Text;
   string ExpireTextBox = ((ITextControl)ctlExpireTextBox).Text;
   string LengthTextBox = ((ITextControl)ctlLengthTextBox).Text;
   string WidthTextBox = ((ITextControl)ctlWidthTextBox).Text;
   string HeightTextBox = ((ITextControl)ctlHeightTextBox).Text;
   string WeightTextBox = ((ITextControl)ctlWeightTextBox).Text;

   WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();

   da.updateProduct(ProductNoLabel, Ean13TextBox, ProductTypeTextBox, TempTextBox, ExpireTextBox, LengthTextBox, WidthTextBox, HeightTextBox, WeightTextBox);
}

到這裡先停一下,或許有人會問,這些東西寫在單獨類別和頁面有什麼差異嗎? 「當然有,不然幹嘛這樣寫呢…」我當然不會這樣回答,也太不技術人了   XD

首先這些切割,都遵尋最基本的 SOLID 原則:物件責任單一化。當一個 Method 塞太多責任時,會讓行數和複雜度不斷成長。到某程度會引發修改者的怒氣也跟著成長,這樣不好。如果能把部份功能抽到另一個 Method ,形同抽出黑箱。黑箱作業不好,但穩定的黑箱程式是讓人開心的,畢竟 Black Box 可是穩定的代名詞呢,哈哈。

抽出 Method 代表可以復用部份程式,不僅減少了手指軟骨的磨損,於測試時也可以分開測試。以上面為例,我們的 getProducts / getProduct / updateProduct 可以分別由各頁面呼叫而不再重寫。真的需要測試「資料程式正確性」時,僅針對此三 Method 撰寫測試碼就好,假設修改前有 N 個頁面要讀取產品清單 / 單一產品, M 個頁面需要修改產品資料,光是測試就是 M ∪ N 頁要跑,而且全都是手~動~操~作。今天改完後,測試只要寫三個,就算是手動測試,數量都從 N + M => 3 ,整個輕鬆愉快,改天同事把程式改爛要亂賴,我們就兩手一攤說:「這是我的測試頁面,馬上來看一下成功或失敗。」,馬上反擊回去了,生活開心~☆

SOLID 原則重點都放在幾項:修改幅度減少、容易擴充有彈性、程式穩定不易壞,建議一定要瞭解這五個原則。如果需要討論,相信各大社群都很歡迎新人進來的。哈哈。

 

講完好處,休息也夠了。來看看下一步要改什麼,讓我們把眼光看到 DataAccess 這個類別,有沒有看到三個 Method 都有共用的程式:「讀取連線字串,組 SQL ,執行 SQL 」,讓這些程式散佈各處也沒什麼問題,但倘若有一天我們想將連線字串的 KEY 值換掉,或是將資料庫換成 MySQL 等免費資料庫,隨著資料庫連線方法越來越多,到時候改不完,根本無法換掉 (先忽視 SQL 語法的差異) 。

來吧,我們試著抽出資料庫連線處理的程式。把現有程式分為為商業邏輯及資料存取,讓商業邏輯層只剩 SQL 語法和處理資料就好,資料庫連線就交由資料存取層。不過此處會卡一個問題,由於透過 SQLParameter 來傳遞參數,其中一個做法是使用字典來當成資料容器。當然目前都透過文字傳遞參數不會有問題,實務上無法這麼做,這點我們之後再解。

第一階段,我們把 updateProduct 中資料庫連線、執行,都抽到另一個方法 executeCmd 中。概念上很簡單, updateProduct 成為溝通介面,當呼叫它時,將 SQL 語法和參數都傳入 executeCmd 並執行之。這個修改很小不易出錯, CODE 會變成如下 (0.65 => 0.7):

private void executeCmd(string CmdSQL, Dictionary<string, string> Parameters)
{
    string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

    using (SqlConnection conn = new SqlConnection(connStr))
    {
        SqlCommand cmd = new SqlCommand(CmdSQL, conn);


        foreach (KeyValuePair<string, string> pair in Parameters)
        {
            cmd.Parameters.AddWithValue("@" + pair.Key, pair.Value);
        }


        conn.Open();
        cmd.ExecuteNonQuery();
        conn.Close();
    }
}


public void updateProduct(string ProductNo, string Ean13, string ProductType, string Temp, string Expire, string Length, string Width, string Height, string Weight)
{
    try
    {
        string UpdateCmd =
            " UPDATE Product " +
            " SET    Ean13 = @Ean13, ProductType = @ProductType, Temp = @Temp, Expire = @Expire, " +
            "       Length = @Length, Width = @Width, Height = @Height, Weight = @Weight " +
            " WHERE  ProductNo = @ProductNo ";


        Dictionary<string, string> paras = new Dictionary<string, string>();

        paras.Add("Ean13", Ean13);
        paras.Add("ProductType", ProductType);
        paras.Add("Temp", Temp);
        paras.Add("Expire", Expire);
        paras.Add("Length", Length);
        paras.Add("Width", Width);
        paras.Add("Height", Height);
        paras.Add("Weight", Weight);

        paras.Add("ProductNo", ProductNo);

        this.executeCmd(UpdateCmd, paras);
    }
    catch (Exception ex)
    {
        // Do Something 
    }
}

這個小小的變更,概念上卻將資料庫的相依性解除了, updateProduct不再相依於 SQLConnection, SQLCommand, SQLParameter ,如果需要更換資料庫時,就將 executeCmd 改寫即可。

當然,我們寫了更新產品,一定也要再補上查詢產品,但查詢因為有回傳值,無法直接套用更新的語法,只好另外寫了。讓我們建立一個 readCmd 方法,並將程式抽進去,回傳值先暫定為 DataTable ,有件事情要小心,原本程式採用 DataReader ,所以 parseDataToObject 也得改寫。程式如下:

private ProductValueObject parseDataToObject(DataRow row)
{
    var returnObj = new ProductValueObject();

    returnObj.ProductNo = row["ProductNo"].ToString();
    returnObj.Ean13 = row["Ean13"].ToString();
    returnObj.ProductType = row["ProductType"].ToString();
    returnObj.Temp = row["Temp"].ToString();
    returnObj.Expire = row["Expire"].ToString();
    returnObj.Length = (double)row["Length"];
    returnObj.Width = (double)row["Width"];
    returnObj.Height = (double)row["Height"];
    returnObj.Weight = (double)row["Weight"];

    return returnObj;
}


private DataTable readCmd(string SqlCmd, Dictionary<string, string> param)
{
    string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
    DataTable dt = new DataTable();


    using (SqlConnection conn = new SqlConnection(connStr))
    {
        SqlCommand cmd = new SqlCommand(SqlCmd, conn);


        foreach (KeyValuePair<string, string> pair in param)
        {
            cmd.Parameters.AddWithValue("@" + pair.Key, pair.Value);
        }

        conn.Open();


        SqlDataReader reader = cmd.ExecuteReader();
        dt.Load(reader);
    }

    return dt;
}


public List<ProductValueObject> getProducts()
{
    var returnList = new List<ProductValueObject>();

    try
    {
        string SELECTCmd = "SELECT * FROM Product";

        DataTable dt = this.readCmd(SELECTCmd, new Dictionary<string, string>());

        foreach (DataRow row in dt.Rows)
        {
            var productItem = this.parseDataToObject(row);
            returnList.Add(productItem);
        }
    }
    catch (Exception ex)
    {
        // Do Something
    }

    return returnList;
}


public ProductValueObject getProduct(string ProductNo)
{
    ProductValueObject obj = null;

    try
    {
        string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = @ProductNo";

        Dictionary<string, string> dicParam = new Dictionary<string, string>();
        dicParam.Add("ProductNo", ProductNo);


        DataTable dt = this.readCmd(SELECTCmd, dicParam);

        foreach(DataRow row in dt.Rows)
        {
            obj = this.parseDataToObject(row);
        }
    }
    catch (Exception ex)
    {
        // Do Something 
    }

    return obj;
}

至此,算是告一個小段落,將資料庫管理程式放到統一的 Method 中,商業邏輯只負責組查詢 SQL 及處理參數。但讓我們再做一個努力,根據「職責分離」概念,資料庫管理程式是一個泛用,且和商業邏輯無關的功能,值得為它抽出類別。首先我們將幾個類別更名,明確定義出這個類別的功能範疇,檔名也要記得改一下哦。

public class DataAccess

改為 public class ProductManager

 

從 public class ProductValueObject

改為 public class ProductModel

 

再來,我們建立一個 SQLDataAccess 這個類別,路徑放在 Root > DataObject > SQLDataAccess.cs ,程式如下 (0.7~0.8) :

using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections.Generic;

namespace WebApplication1.DataObject
{
    internal class SQLDataAccess
    {
        internal DataTable readCmd(string SqlCmd, Dictionary<string, string> param)
        {
            string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
            DataTable dt = new DataTable();


            using (SqlConnection conn = new SqlConnection(connStr))
            {
                SqlCommand cmd = new SqlCommand(SqlCmd, conn);


                foreach (KeyValuePair<string, string> pair in param)
                {
                    cmd.Parameters.AddWithValue("@" + pair.Key, pair.Value);
                }

                conn.Open();


                SqlDataReader reader = cmd.ExecuteReader();
                dt.Load(reader);
            }

            return dt;
        }


        internal void executeCmd(string CmdSQL, Dictionary<string, string> param)
        {
            string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;

            using (SqlConnection conn = new SqlConnection(connStr))
            {
                SqlCommand cmd = new SqlCommand(CmdSQL, conn);


                foreach (KeyValuePair<string, string> pair in param)
                {
                    cmd.Parameters.AddWithValue("@" + pair.Key, pair.Value);
                }


                conn.Open();
                cmd.ExecuteNonQuery();
                conn.Close();
            }
        }
    }
}

同時, ProductManager 也改寫為:

using System;
using System.Data;
using System.Collections.Generic;

namespace WebApplication1.DataObject
{
    public class ProductManager
    {
        private SQLDataAccess sqlDA = new SQLDataAccess();

        private ProductModel parseDataToObject(DataRow row)
        {
            var returnObj = new ProductModel();

            returnObj.ProductNo = row["ProductNo"].ToString();
            returnObj.Ean13 = row["Ean13"].ToString();
            returnObj.ProductType = row["ProductType"].ToString();
            returnObj.Temp = row["Temp"].ToString();
            returnObj.Expire = row["Expire"].ToString();
            returnObj.Length = (double)row["Length"];
            returnObj.Width = (double)row["Width"];
            returnObj.Height = (double)row["Height"];
            returnObj.Weight = (double)row["Weight"];

            return returnObj;
        }


        public List<ProductModel> getProducts()
        {
            var returnList = new List<ProductModel>();

            try
            {
                string SELECTCmd = "SELECT * FROM Product";

                DataTable dt = this.sqlDA.readCmd(SELECTCmd, new Dictionary<string, string>());

                foreach (DataRow row in dt.Rows)
                {
                    var productItem = this.parseDataToObject(row);
                    returnList.Add(productItem);
                }
            }
            catch (Exception ex)
            {
                // Do Something
            }

            return returnList;
        }


        public ProductModel getProduct(string ProductNo)
        {
            ProductModel obj = null;

            try
            {
                string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = @ProductNo";

                Dictionary<string, string> dicParam = new Dictionary<string, string>();
                dicParam.Add("ProductNo", ProductNo);


                DataTable dt = this.sqlDA.readCmd(SELECTCmd, dicParam);

                foreach (DataRow row in dt.Rows)
                {
                    obj = this.parseDataToObject(row);
                }
            }
            catch (Exception ex)
            {
                // Do Something 
            }

            return obj;
        }


        public void updateProduct(string ProductNo, string Ean13, string ProductType, string Temp, string Expire, string Length, string Width, string Height, string Weight)
        {
            try
            {
                string UpdateCmd =
                    " UPDATE Product " +
                    " SET    Ean13 = @Ean13, ProductType = @ProductType, Temp = @Temp, Expire = @Expire, " +
                    "       Length = @Length, Width = @Width, Height = @Height, Weight = @Weight " +
                    " WHERE  ProductNo = @ProductNo ";


                Dictionary<string, string> paras = new Dictionary<string, string>();

                paras.Add("Ean13", Ean13);
                paras.Add("ProductType", ProductType);
                paras.Add("Temp", Temp);
                paras.Add("Expire", Expire);
                paras.Add("Length", Length);
                paras.Add("Width", Width);
                paras.Add("Height", Height);
                paras.Add("Weight", Weight);

                paras.Add("ProductNo", ProductNo);

                this.sqlDA.executeCmd(UpdateCmd, paras);
            }
            catch (Exception ex)
            {
                // Do Something 
            }
        }
    }
}

 

這個改寫完成了,好累啊,喘口氣。回顧一下我們的 ProductManager ,除了 SQL 語法外,終於完全沒有資料庫物件在裡面了 (DataTable 並不算是資料庫物件,它可以由程式塞值) 。還記得主題是「責任分離」分離嗎? 到現在才勉強達成「商業邏輯歸 Business 層,而資料庫連線則是放在 DataAccess 層」。

今天的程式碼量很多,但其實步驟並不複雜,一步步執行這些步驟,我們很確實的將 Webform 往現代化程式推進一點。程式碼量真的變多了嗎? 從長遠的眼光來看,當程式越能重用,以後撰寫相同程式碼就變少了,實際上抽出 Library 的目的也是這樣的,透過重用減少重寫,要記得重寫容易犯錯,但重用會讓我們使用已成熟的程式。

雖然離重構目標只剩下一點點,但放到下次吧。

下次,讓我們從 0.8 ~ 1 ,完成這次重構後,再來討論其它改造主題。來做個預告,下次內容是:「依設定讀取 Table 」、「切換資料庫」、「類別庫」。