[ASP.NET]重構之路系列v1 – UI, Business logic, Data access概念分開

[ASP.NET]重構之路系列v1 – UI, Business logic, Data access概念分開

前言
程式寫的好與寫的不好,其實一直很難去定義出來。雖然可以透過一些Quality attribute以及相關的KPI來決定所謂的品質,但卻很難去解釋怎麼樣的寫法,會比較好維護,是比較好的架構。

這一系列的文章,希望用同一個簡單的範例,慢慢的演進成一版一版越來越好的版本。(這邊的好,也可能只是一般比較有彈性的架構設計方式,有時候沒有這麼迫切的需求,就可能不用把架構切到這麼細,反而導致重構成本、學習成本、理解成本過高)。

另外,題目一直不知道怎麼訂比較好,基本上說到重構,一開始就該帶入測試,一碰到測試,我相信就有一堆人覺得這又是烏托邦的說法,進而直接跳過這文章。所以,在這邊我還是得強調,重構得先透過測試來保護,才能證明自己重構完的程式,仍如同原本預期的結果運作,但這邊測試不會是我的重點,而是希望透過一些範例,讓讀者可以知道,自己寫的code,大概是落在第幾版的情況,進而參考看看,是否可以讓自己的程式碼,調整成面對未來需求變更可以更有彈性。(太進階的版本我也還太菜,只能拋磚引玉再請一些大師來開課或分享了)

範例說明
我們切入點,先用個很單純的例子,輸入帳號、密碼,然後驗證。

image 

先舉出原型版的寫法,也就是最常見的糾結版:

v1

(原型糾結版)

上面是在MSDN forum或一般業界沒有規定系統架構下,很常見到的程式寫法。(其實已經算很OK了,至少命名OK,且還使用parameter來防止SQL injection)
大家可以看到,按了Verify的按鈕之後,把所有要處理的程式寫在一起,包括了Business logic, Data access, 以及UI的呈現控制。

概念上就像是下圖:

image

壞處:不管是DB table欄位的調整,connection string的調整,business logic的調整,UI的調整,這個方法都會被影響到,這一支.aspx.cs都要修改,而且同樣的Data access以及同樣的Business logic,在其他地方要用,就得再重寫一次。

重構

步驟一:
我們先將Data access以及Business logic,跟根據資料回傳結果以及商業邏輯判斷後UI呈現的部分選起來,按滑鼠右鍵,選『重構』,『擷取方法』,然後給它個有意義的名字,代表要處理的事情。

v1-r1
v1-r2

這樣重構完的結果,其實只是把一堆事情封裝成一個方法而已,這樣並沒有代表太大的意義。

步驟二:
我們接著要把UI呈現控制的部分,從VerifyPasswordById的方法中抽離出來,並微調一下我們的方法,改為回傳一個我們透過Enum自訂的狀態。讓UI的呈現控制,根據這個狀態來決定怎麼處理呈現的控制。如此一來,Business logic與Data access的部分,可以與UI處理隔離開來。這代表著,UI要怎麼處理,UI未來要怎麼變化,都跟Business logic與Data access無關了。

v1-r3
v1-r4

如此一來,我們的程式架構就會變成:
image

步驟三:
既然我們的UI已經可以獨立開來,未來修改UI都不會影響到Business logic跟Data Access的部分,那我們接著就用同樣的方式把Business logic與Data access隔開吧。

v1-r5

我們一樣微調一下重構後的新方法,改回傳DataTable,如此就不會影響到我們原本的Business logic的程式。

v2

 

image

重構的第一版,我們將UI, Business logic, Data access的程式獨立開來,這樣子可以達到最基本的關注點分離,只要input參數、output type沒變,各自內容怎麼改變,對彼此都不會有任何影響。


    /// <summary>
    /// 驗證密碼是否OK
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// <history>
    /// 1.使用parameter防止SQL injection,但所有的UI呈現、商業邏輯、資料存取相關的程式,都寫在Button的Click裡面。
    /// 2.將Business logic與Data Access,透過重構->擷取方法,抽成一個method,微調成return 驗證後的狀態,讓UI呈現與Buiness logic分開。
    /// </history>
    protected void Verify_Click(object sender, EventArgs e)
    {
        string id = this.Id.Text;
        string password = this.Password.Text;

        var status = VerifyPasswordById(id, password);

        string result = string.Empty;
        switch (status)
        {

            case VerifyStatus.Passed:
            case VerifyStatus.Failed:
                result = status.ToString();
                break;
            case VerifyStatus.NoExist:
                result = "帳號或密碼輸入錯誤";
                break;
            case VerifyStatus.None:
            default:
                break;
        }
        this.Result.Text = result;
    }

    private enum VerifyStatus
    {
        None = 0,
        Passed,
        Failed,
        NoExist
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="id"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    /// <history>
    /// 1.Business logic獨立完成,只與Data access的QueryPasswordById()有相依性。Business logic並不知道,也不用知道被什麼UI呼叫。
    /// </history>
    private VerifyStatus VerifyPasswordById(string id, string password)
    {
        DataTable dt = QueryPasswordById(id, password);
        if (dt.Rows.Count > 0)
        {
            if (password == dt.Rows[0]["Password"].ToString())
            {                
                return VerifyStatus.Passed;
            }
            else
            {                
                return VerifyStatus.Failed;
            }
        }
        else
        {            
            return VerifyStatus.NoExist;
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="id"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    /// <history>
    /// 1.Data access獨立完成,Data access並不知道,也不用知道被什麼Business logic呼叫
    /// </history>
    private DataTable QueryPasswordById(string id, string password)
    {
        DataTable dt = new DataTable();

        string connectionString = @"myConnectionString";
        using (SqlConnection cn = new SqlConnection(connectionString))
        {
            cn.Open();
            string sqlStatement = @"Select Password From SomeTable Where ID=@id ";
            SqlCommand sqlCommand = new SqlCommand(sqlStatement, cn);
            sqlCommand.Parameters.AddWithValue("@id", id);
            SqlDataAdapter adapter = new SqlDataAdapter(sqlCommand);

            adapter.Fill(dt);
        }
        return dt;
    }

結論
如果您以前撰寫的程式,要維護前人撰寫的程式,還在原型的糾結版時,請至少依據這樣簡單的方式,先把程式的思緒釐清出來,光可維護性就可以提升很多。當然,這還只是第一版,很多讀者的能力早就不知道在第幾版了,敬請期待後面我們一路的重構下去,會變成什麼模樣吧。

[註1]很久很久沒寫長的像糾結版那樣的程式了,所以花了頗長的時間寫那一段,相信後面的重構,會越來越快,越來越快樂的。
[註2]密碼的驗證,基本上不會長的像這樣,而是將input的密碼加上hash處理,去跟DB已經hash過的密碼值比對。不過這邊我就只是簡單的示意,到更後面的例子,怎麼驗證這件事,就更不重要了。因為我們要設計出,可以吃不同驗證方式的架構。


V1 Sample Code: RefactoringToArchitectureSample-v1.zip

 


blog 與課程更新內容,請前往新站位置:http://tdd.best/