2010年12月1日

LinQ對分層架構(UIL, BLL ,DAL)的影響

網路上看到LinQ的優點大都從O/R Mapping(ORM)的角度,或整合不同型態的來源,
對於我而言,其對分層架構的影響更大的多...

隨著開發的系統日漸龐大,
約在2003年,我們公司參考微軟的範例(duwamish)在幾個同事推動下導入了分層設計,
將系統大致切為User Interface Layer、Business Logical Layer、Data Access Layer三層,
這種做法的優點就不再贅述,這些年來也頗常見(或稱N-tier architecture),
但是,很多問題也因此發生。

舉例來說,
如果使用者要求在進入人事系統中,
單位主管除了看到人員列表外,還希望看到目前每個人的負責/完成專案數,以及簽/遲到狀況,
總務部門希望看到每個人的是否還有信件未領取,福利金是否繳交。

像這樣的需求實務上應該不少見,也不算不合理,
但在一般提到分層架構的範例或書籍卻相當少被提及。

就OO設計的單一責任(SRP)原則 ,人員列表、專案數與簽到、遲到明顯屬於不同邏輯,
但,難道開發人員能用『商務邏輯應該單一化』為由,
要求使用者一定要在不同的介面進行操作與查詢嗎?

簡單一點的做法(我指.Net架構中),
有人就在DAL中寫個很複雜的Query,把人員查詢、負責專案數、完成專案數、簽到...
全部 Join 起來,然後用 DataSet 或 DataTable 傳回去,
但這樣就違反了幾個原則:
  1. 將查專案數、簽到、遲到等不同的邏輯全混在一起,全無『高內聚,低耦合』可言。
  2. 查專案數、簽到、遲到等可能並不是單純的select table,其中有許多條件,這麼一來,等於在DAL放了許多的Bussiness Role。
  3. 只要介面又被要求修改,例如再加個請假天數,連DAL也要動,喪失分層架構的原意。
這些問題即使改用Typed DataSet也一樣,
更多了一個問題:難道要為人員+ 負責專案數+完成專案數... 多一個宣告Typed DataTable嗎?

為了能藉決這些問題,
我試過自己寫DataTable merge的工具程式(類似SQL Helper),
好讓 DAL能分別獨立的取得人員清單、專案數、簽到、遲到等資料,再於 BLL做合併。
看起來雖然好一些,但也常常有效能或功能仍然不夠的問題。

這個問題終於隨著LinQ面世,帶來了一絲曙光。

這不只是可以快速產生帶有Bussiness Rule的資料物件(指LinQ  to SQL),
並且有了一種能在BLL作資料運算的語言(不用自己來制訂)。
你可以維持DAL的單純化,分別提供人員清單、專案數、簽到、遲到等資料存取方法,
然後在BLL中取得後再進行條件過濾、合併(join)甚至 group等動作,
真正的讓『業務邏輯規則』集中在BLL。

我是說理論上是這樣吧!
實際上的效能等問題,可能過一陣子再來探討。

2010年11月16日

以TDD開發『數字轉中文』程式

嘗試以TDD(測試導向開發, Test-Driven development)方式進行開發
程式片段與測試可以看這裡
過程大致如下:

  1. 不用多說就先寫出測試函數ConvertNum2CStringTest1,先測ConvertNum2CString(1)吧!(如果為了方便先寫空程式再產生Testing也可)
  2. 再讓ConvertNum2CString()傳入1可以得到"一"
  3. 接下來讓個位數正確
  4. 讓千位數以下有『?千?百?十?』
  5. 考慮中間位數為0補上『零』
  6. 考慮中間位數連續多個0只補一個『零』
  7. 當尾數為0不可補『零』喲
  8. 接下來處理每四位數的『萬、億』
  9. 『萬、億』中間補零問題
  10. 差不多了,試試『一億』『二十億零一』之類的


簡略的心得:
  1. 心態調整:採用TDD會增加工作量?我覺得可以換個方向想,本來程式開發的過程就會不停的進行測試,現在能把每階段的測試過程保留下來,並且自動的重複執行,反而節省了測試成本。
  2. 撰寫過程:TDD的開發過程中,先從最簡單的空函式,再一步一步處理下一個問題...,會迫使你用漸進的思考方式,而不用一開始就考慮整個運算架構(如:怎麼補零,『萬、億』的進位),可以說非常合乎敏捷式開發,沒遇到問題前先用最簡單方式作的精神。
  3. 反覆測試:每當為了處理了更複雜的問題,而對原來的程式進行修改或重構,只要再執行一次測試,就不怕原來對的地方被改錯了。可以說修改時更有信心。
  4. 邊創作邊毀滅:之前看過對測試導向的一個說法,你要在兩個角色之間一直切換:先扮演找碴者,極盡所能想出能讓現有程式出錯的測試;再扮演創造者,解決這個出錯問題。這樣一來,能強迫自己單方面作為程式設計者,對成果可能有的姑息心態。
當然,目前只是抱著玩玩看的心態,寫個小功能。如果是整個系統都要這樣開發,一定會有更多的挑戰。

2010年11月12日

數字轉中文的程式段

為了嘗試TDD(測試導向開發, Test-Driven development)
寫了一個數字轉中文的函式

在網路上意外發現,要用這個題目找個簡短一點的C#程式段好像沒想像中多
雖不敢說自己寫的是最好
但可讀性應該還可以
扣掉註解後程式約30行
提供需要的人參考(不考慮小數部分)

有空再寫以測試導向的思維,進行以下開發的過程
也歡迎先進不吝指點一二


public string ConvertNum2CString(int intNum)
{
    if (intNum == 0) return ("零");

    string[] strPosition = {"", "萬", "億"};
    string strTheValue = "";
    string strReturn = ""; //傳回值
    string strMod = "";

    //從尾巴開始,每次處理四個位數
    for (int i = intNum, j = 0; i >= 1; i = i / 10000, j++)
    {
        strTheValue = ConvertNum2CString1000(i % 10000);
        if (!string.IsNullOrEmpty(strTheValue))
        {
            strReturn = strTheValue + strPosition[j] + strMod + strReturn;
        }
        else if (strReturn.Length > 0 && strReturn.Substring(0, 1) != "零") //本位數雖空白 但後面有數字
        {
            strReturn = "零" + strReturn;
        }

        //如果本四個位數不滿四位且非0 則 上一級尾數需加 "零" 如: 五萬零三
        strMod = (i % 10000 > 0 && i % 10000 < 1000) ? "零" : "";
    }
    return (strReturn);
}

/// <summary>
/// 處理千位以下 數字轉中文
/// </summary>
/// <param name="intNum"></param>
/// <returns></returns>
private string ConvertNum2CString1000(int intNum)
{
    string strCString = "零一二三四五六七八九";
    string[] strPosition = {"", "十", "百", "千" };
    string strReturn = ""; //傳回值
    int intTheValue;
    int intPosition = 0; //目前處理到幾位數

    //從尾巴開始,每次處理一個位數
    for (int i = intNum; i >= 1; i = i / 10)
    {
        intTheValue = i % 10; //目前處理數值

        if (intTheValue > 0)    //目前數字 > 0才需加位數
        {
            strReturn = strCString.Substring(intTheValue, 1) + strPosition[intPosition] + strReturn;
        }
        else if (strReturn.Length > 0 && strReturn.Substring(0, 1) != "零")  //目前數字雖為0但後面有數字,且下一位不是零 則加"零"
        {
            strReturn = "零" + strReturn;
        }

        intPosition++;
    }
    return (strReturn);
}





================================================================


附上測試程式

public void ConvertNum2CStringTest1()
{
    BaseUtility objUtility = new BaseUtility();
    Assert.AreEqual("一", objUtility.ConvertNum2CString(1), "未傳回預期的值。");
    Assert.AreEqual("五", objUtility.ConvertNum2CString(5), "未傳回預期的值。");
    Assert.AreEqual("一十", objUtility.ConvertNum2CString(10), "未傳回預期的值。");
    Assert.AreEqual("八十六", objUtility.ConvertNum2CString(86), "未傳回預期的值。");
    Assert.AreEqual("二百五十", objUtility.ConvertNum2CString(250), "未傳回預期的值。");
    Assert.AreEqual("四千二百七十九", objUtility.ConvertNum2CString(4279), "未傳回預期的值。");
    Assert.AreEqual("零", objUtility.ConvertNum2CString(0), "未傳回預期的值。");
    Assert.AreEqual("三百零五", objUtility.ConvertNum2CString(305), "未傳回預期的值。");
    Assert.AreEqual("三千零五", objUtility.ConvertNum2CString(3005), "未傳回預期的值。");
    Assert.AreEqual("一萬", objUtility.ConvertNum2CString(10000), "未傳回預期的值。");
    Assert.AreEqual("五萬三千零五", objUtility.ConvertNum2CString(53005), "未傳回預期的值。");
    Assert.AreEqual("五千萬三千零五", objUtility.ConvertNum2CString(50003005), "未傳回預期的值。");
    Assert.AreEqual("五千二百萬三千零五", objUtility.ConvertNum2CString(52003005), "未傳回預期的值。");
    Assert.AreEqual("五千二百零九萬三千零七十五", objUtility.ConvertNum2CString(52093075), "未傳回預期的值。");
    Assert.AreEqual("五千二百零九萬", objUtility.ConvertNum2CString(52090000), "未傳回預期的值。");
    Assert.AreEqual("一億", objUtility.ConvertNum2CString(100000000), "未傳回預期的值。");
    Assert.AreEqual("五十萬零三", objUtility.ConvertNum2CString(500003), "未傳回預期的值。");
    Assert.AreEqual("二十億零一", objUtility.ConvertNum2CString(2000000001), "未傳回預期的值。");
    Assert.AreEqual("二十億零三百萬零一", objUtility.ConvertNum2CString(2003000001), "未傳回預期的值。");
}