程式寫的好與寫的不好,其實一直很難去定義出來。雖然可以透過一些Quality attribute以及相關的KPI來決定所謂的品質,但卻很難去解釋怎麼樣的寫法,會比較好維護,是比較好的架構。
而提升系統品質一個很重要的過程:重構。
基本上說到重構,一開始就該帶入測試,所以,在這邊我還是得強調,重構得先透過測試來保護,才能證明自己重構完的程式,仍如同原本預期的結果運作。至於測試的部分,會在後續的文章中,慢慢地帶出來觀念、步驟、使用的工具,以及導入測試可能會碰到的困難。
[如何提升系統品質]系列文章連結
範例
我們切入點,先用個很單純的例子,輸入帳號、密碼,然後驗證。
先舉出原型版的寫法,也就是最常見的糾結版:
上面是常在forum或一般業界沒有規定系統架構下,很常見到的程式寫法。(其實已經算很OK了,至少命名OK,且還使用parameter來防止SQL injection)。
大家可以看到,按了Verify的按鈕之後,把所有要處理的程式寫在一起,包括了Business logic, Data access, 以及UI的呈現控制。
概念上就像是下圖:
壞處:不管是DB table欄位的調整,connection string的調整,business logic的調整,UI的調整,這個方法都會被影響到,這一支.aspx.cs都要修改,而且同樣的Data access以及同樣的Business logic,在其他地方要用,就得再重寫一次。
重構
步驟一:
我們先將Data access以及Business logic,跟根據資料回傳結果以及商業邏輯判斷後UI呈現的部分選起來,按滑鼠右鍵,選『重構』,『擷取方法』,然後給它個有意義的名字,代表要處理的事情。
這樣重構完的結果,其實只是把一堆事情封裝成一個方法而已,這樣並沒有代表太大的意義。
步驟二:
我們接著要把UI呈現控制的部分,從VerifyPasswordById的方法中抽離出來,並微調一下我們的方法,改為回傳一個我們透過Enum自訂的狀態。讓UI的呈現控制,根據這個狀態來決定怎麼處理呈現的控制。如此一來,Business logic與Data access的部分,可以與UI處理隔離開來。這代表著,UI要怎麼處理,UI未來要怎麼變化,都跟Business logic與Data access無關了。
如此一來,我們的程式架構就會變成:
步驟三:
既然我們的UI已經可以獨立開來,未來修改UI都不會影響到Business logic跟Data Access的部分,那我們接著就用同樣的方式把Business logic與Data access隔開吧。
我們一樣微調一下重構後的新方法,改回傳DataTable,如此就不會影響到我們原本的Business logic的程式。
重構的第一版,我們將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;
}
結論
如果您以前撰寫的程式,要維護前人撰寫的程式,還在原型的糾結版時,請至少依據這樣簡單的方式,先把程式的思緒釐清出來,光可維護性就可以提升很多。當然,這還只是第一版,很多讀者的能力早就不知道在第幾版了,敬請期待後面我們一路的重構下去,會變成什麼模樣吧。
[註]密碼的驗證,基本上不會長的像這樣,而是將input的密碼加上hash處理,去跟DB已經hash過的密碼值比對。不過這邊我就只是簡單的示意,到更後面的例子,怎麼驗證這件事,就更不重要了。
事實上現在只有在專案是這樣做, 但若一套用在 Custom Based 的網站, 很多登入面的邏輯又會差很多...
就得用到後面的interface介紹。