小菜編程成長記

 面試受挫——代碼無錯就是好?

小菜今年計算機專業大四了,學了不少軟件開發方面的東西,也學着編了些小程序,躊躇滿志,一心要找一個好單位。當投遞了無數份簡歷後,終於收到了一個單位的面試通知,小菜欣喜若狂。

到了人家單位,前臺小姐給了他一份題目,上面寫着,“請用C++、Java、C#或VB.NET任意一種面嚮對象語言實現一個計算器控制檯程序,要求輸入兩個數和運算符號,得到結果。”

小菜一看,這個還不簡單,三下五除二,10分鐘不到,小菜寫完了,感覺也沒錯誤。交卷後,單位說一週內等通知吧。於是小菜只得耐心等待。可是半個月過去了,什麼消息也沒有,小菜很納悶,我的代碼實現了呀,爲什麼不給我機會呢。

小菜找到工作三年的師哥大鳥,請教原因,大鳥問了題目和了解了小菜代碼的細節以後,哈哈大笑,說道:“小菜呀小菜,你上當了,人家單位出題的意思,你完全都沒明白,當然不會再聯繫你了”。

小菜說:“我的代碼有錯嗎?單位題目不就是要我實現一個計算器的代碼嗎,我這樣寫有什麼問題。”


class Program
{
static void Main(string[] args)
{
Console.Write("請輸入數字A:");
string A = Console.ReadLine();
Console.Write("請選擇運算符號(+、-、*、/):");
string B = Console.ReadLine();
Console.Write("請輸入數字B:");
string C = Console.ReadLine();
string D = "";

if (B == "+")
D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
if (B == "-")
D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
if (B == "*")
D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
if (O == "/")
D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));

Console.WriteLine("結果是:" + D);
}
}


小菜的代碼有什麼問題呢?

二 代碼規範、重構

大鳥說:“且先不說出題人的意思,單就你現在的代碼,就有很多不足的地方需要改進。比如變量命名,你的命名就是ABCD,變量不帶有任何具體含義,這是非常不規範的;判斷分支,你這樣的寫法,意味着每個條件都要做判斷,等於計算機做了三次無用功;數據輸入有效性判斷等,如果用戶輸入的是字符符號而不是數字怎麼辦?如果除數時,客戶輸入了0怎麼辦?這些都是可以改進的地方。”

“哦,說得沒錯,這個我以前聽老師說過,可是從來沒有在意過,我馬上改,改完再給你看看。”



class Program
{
static void Main(string[] args)
{
try
{
Console.Write("請輸入數字A:");
string strNumberA = Console.ReadLine();
Console.Write("請選擇運算符號(+、-、*、/):");
string strOperate = Console.ReadLine();
Console.Write("請輸入數字B:");
string strNumberB = Console.ReadLine();
string strResult = "";

switch (strOperate)
{
case "+":
strResult = Convert.ToString(Convert.ToDouble(strNumberA) + Convert.ToDouble(strNumberB));
break;
case "-":
strResult = Convert.ToString(Convert.ToDouble(strNumberA) - Convert.ToDouble(strNumberB));
break;
case "*":
strResult = Convert.ToString(Convert.ToDouble(strNumberA) * Convert.ToDouble(strNumberB));
break;
case "/":
if (strNumberB != "0")
strResult = Convert.ToString(Convert.ToDouble(strNumberA) / Convert.ToDouble(strNumberB));
else
strResult = "除數不能爲0";
break;
}

Console.WriteLine("結果是:" + strResult);

Console.ReadLine();


}
catch (Exception ex)
{
Console.WriteLine("您的輸入有錯:" + ex.Message);
}
}
}


大鳥:“吼吼,不錯,不錯,改得很快嗎?至在目前代碼來說,實現計算器是沒有問題了,但這樣寫出的代碼是否合出題人的意思呢?”

小菜:“你的意思是面向對象?”

大鳥:“哈,小菜非小菜也!”

三 複製VS複用

小菜:“我明白了,他說用任意一種面嚮對象語言實現,那意思就是要用面向對象的編程方法去實現,對嗎?OK,這個我學過,只不過當時我沒想到而已。”

大鳥:“所有編程初學者都會有這樣的問題,就是碰到問題就直覺的用計算機能夠理解的邏輯來描述和表達待解決的問題及具體的求解過程。這其實是用計算機的方式去思考,比如計算器這個程序,先要求輸入兩個數和運算符號,然後根據運算符號判斷選擇如何運算,得到結果,這本身沒有錯,但這樣的思維卻使得我們的程序只爲滿足實現當前的需求,程序不容易維護,不容易擴展,更不容易複用。從而達不到高質量代碼的要求。”

小菜:“鳥哥呀,我有點糊塗了,如何才能容易維護,容易擴展,又容易複用呢,能不能具體點?”

大鳥:“比如說,我現在要求你再寫一個windows的計算器,你現在的代碼能不能複用呢?”

小菜:“那還不簡單,把代碼複製過去不就行了嗎?改動又不大,不算麻煩。”

大鳥:“小菜看來還是小菜呀,有人說初級程序員的工作就是Ctrl+C和Ctrl+V,這其實是非常不好的編碼習慣,因爲當你的代碼中重複的代碼多到一定程度,維護的時候,可能就是一場災難。越大的系統,這種方式帶來的問題越嚴重,編程有一原則,就是用盡可能的辦法去避免重複。想想看,你寫的這段代碼,有哪些是和控制檯無關的,而只是和計算器有關的?”

四 業務的封裝

小菜:“你的意思是分一個類出來? 哦,對的,讓計算和顯示分開。”

大鳥:“準確的說,就是讓業務邏輯與界面邏輯分開,讓它們之間的耦合度下降。只有分離開,才容易達到容易維護或擴展。”

小菜:“讓我來試試看。”


class Program
{
static void Main(string[] args)
{
try
{
Console.Write("請輸入數字A:");
string strNumberA = Console.ReadLine();
Console.Write("請選擇運算符號(+、-、*、/):");
string strOperate = Console.ReadLine();
Console.Write("請輸入數字B:");
string strNumberB = Console.ReadLine();
string strResult = "";

strResult = Convert.ToString(Operation.GetResult(Convert.ToDouble(strNumberA),Convert.ToDouble(strNumberB),strOperate));

Console.WriteLine("結果是:" + strResult);

Console.ReadLine();

}
catch (Exception ex)
{
Console.WriteLine("您的輸入有錯:" + ex.Message);
}
}
}

public class Operation
{
public static double GetResult(double numberA,double numberB,string operate)
{
double result = 0d;
switch (operate)
{
case "+":
result = numberA + numberB;
break;
case "-":
result = numberA - numberB;
break;
case "*":
result = numberA * numberB;
break;
case "/":
result = numberA / numberB;
break;
}
return result;
}
}


小菜:“鳥哥,我寫好了,你看看!”

大鳥:“哈,孺鳥可教也,:),寫得不錯,這樣就完全把業務和界面分離了。”

小菜心中暗罵:“你纔是鳥呢。” 口中說道:“如果你現在要我寫一個Windows應用程序的計算器,我就可以複用這個運算類(Operation)了。”

大鳥:“不單是Windows程序,Web版程序需要運算可以用它,PDA,手機等需要移動系統的軟件需要運算也可以用它呀。”

小菜:“哈,面向對象不過如此。下會寫類似代碼不怕了。”

大鳥:“別急,僅此而已,實在談不上完全面向對象,你只用了面向對象三大特性的一個,還兩個沒用呢?”

小菜:“面向對象三大特性不就是封裝、繼承和多態嗎,這裏我用到的應該是封裝。這還不夠嗎?…………我實在看不出,這麼小的程序如何用到繼承。至於多態,其它我一直也不太瞭解它到底有什麼好處,如何使用它。”

大鳥:“慢慢來,有的東西好學了,你好好想想吧,我要去“魔獸”了,改時聊。”

五 體會簡單工廠模式的美妙

次日,小菜再來找大鳥,問道:“你昨天說計算器這樣的小程序還可以用到面向對象三大特性?繼承和多態怎麼可能用得上,我實在不可理解。”

大鳥:“小菜很有鑽研精神嗎?好,今天我讓你功力加深一級。你先要考慮一下,你昨天寫的這個代碼,能否做到很靈活的可修改和擴展呢?”

小菜:“我已經把業務和界面分離了呀,這不是很靈活了嗎?”

大鳥:“那我問你,現在如果我希望增加一個開根(sqrt)運算,你如何改?”

小菜:“那隻需要改Operation類就行了,在switch中加一個分支就行了。”

大鳥:“問題是你要加一個平方根運算,卻需要把加減乘除的運算都得來參與編譯,如果你一不小心,把加法運算改成了減法,這不是大大的糟糕。打個比方,如果現在公司要求你爲公司的薪資管理系統做維護,原來只有技術人員(月薪),市場銷售人員(底薪+提成),經理(年薪+股份)三種運算算法,現在要增加兼職工作人員的(時薪)算法,但按照你昨天的程序寫法,公司就必須要把包含有的原三種算法的運算類給你,讓你修改,你如果心中小算盤一打,‘TMD,公司給我的工資這麼低,我真是鬱悶,這會有機會了’,於是你除了增加了兼職算法以外,在技術人員(月薪)算法中寫了一句


if (員工是小菜)
{
salary = salary * 1.1;
}


那就意味着,你的月薪每月都會增加10%(小心被抓去坐牢),本來是讓你加一個功能,卻使得原有的運行良好的功能代碼產生了變化,這個風險太大了。你明白了嗎?”

小菜:“哦,你的意思是,我應該把加減乘除等運算分離,修改其中一個不影響另外的幾個,增加運算算法也不影響其它代碼,是這樣嗎?”

大鳥:“自己想去吧,如何用繼承和多態,你應該有感覺了。”

小菜:“OK,我馬上去寫。”



/// <summary>
/// 運算類
/// </summary>
class Operation
{
private double _numberA = 0;
private double _numberB = 0;

/// <summary>
/// 數字A
/// </summary>
public double NumberA
{
get{ return _numberA; }
set{ _numberA = value;}
}

/// <summary>
/// 數字B
/// </summary>
public double NumberB
{
get{ return _numberB; }
set{ _numberB = value; }
}

/// <summary>
/// 得到運算結果
/// </summary>
/// <returns></returns>
public virtual double GetResult()
{
double result = 0;
return result;
}

}





/// <summary>
/// 加法類
/// </summary>
class OperationAdd : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA + NumberB;
return result;
}
}

/// <summary>
/// 減法類
/// </summary>
class OperationSub : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA - NumberB;
return result;
}
}

/// <summary>
/// 乘法類
/// </summary>
class OperationMul : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA * NumberB;
return result;
}
}

/// <summary>
/// 除法類
/// </summary>
class OperationDiv : Operation
{
public override double GetResult()
{
double result = 0;
if (NumberB==0)
throw new Exception("除數不能爲0。");
result = NumberA / NumberB;
return result;
}
}



小菜:“大鳥哥,我按照你說的方法寫出來了一部分,首先是一個運算類,它有兩個Number屬性,主要用於計算器的前後數,然後有一個虛方法GetResult(),用於得到結果,然後我把加減乘除都寫成了運算類的子類,繼承它後,重寫了GetResult()方法,這樣如果要修改任何一個算法,都不需要提供其它算法的代碼了。但問題來了,我如何讓計算器知道我是希望用哪一個算法呢?”

大鳥:“寫得很不錯嗎,大大超出我的想象了,你現在的問題其實就是如何去實例化對象的問題,哈,今天心情不錯,再教你一招‘簡單工廠模式’,也就是說,到底要實例化誰,將來會不會增加實例化的對象(比如增加開根運算),這是很容易變化的地方,應該考慮用一個單獨的類來做這個創造實例的過程,這就是工廠,來,我們看看這個類如何寫。”


/// <summary>
/// 運算類工廠
/// </summary>
class OperationFactory
{
public static Operation createOperate(string operate)
{
Operation oper = null;
switch (operate)
{
case "+":
{
oper = new OperationAdd();
break;
}
case "-":
{
oper = new OperationSub();
break;
}
case "*":
{
oper = new OperationMul();
break;
}
case "/":
{
oper = new OperationDiv();
break;
}
}

return oper;
}
}



大鳥:“哈,看到吧,這樣子,你只需要輸入運算符號,工廠就實例化出合適的對象,通過多態,返回父類的方式實現了計算器的結果。”


Operation oper;
oper = OperationFactory.createOperate("+");
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();



大鳥: “哈,界面的實現就是這樣的代碼,不管你是控制檯程序,Windows程序,Web程序,PDA或手機程序,都可以用這段代碼來實現計算器的功能,當有一天我們需要更改加法運算,我們只需要改哪裏?”

小菜:“改OperationAdd 就可以了。”

大鳥: “那麼我們需要增加各種複雜運算,比如平方根,立方根,自然對數,正弦餘弦等,如何做?”

小菜:“只要增加相應的運算子類就可以了呀。”

大鳥: “嗯?夠了嗎?”

小菜:“對了,還需要去修改運算類工廠,在switch中增加分支。”

大鳥: “哈,那纔對,那如果要修改界面呢?”

小菜:“那就去改界面呀,關運算什麼事呀。”

小菜:“ 回想那天我面試題寫的代碼,我終於明白我爲什麼寫得不成功了,原來一個小小的計算器也可以寫出這麼精彩的代碼,謝謝大鳥。”

大鳥: “吼吼,記住哦,編程是一門技術,更加是一門藝術,不能只滿足於寫完代碼運行結果正確就完事,時常考慮如何讓代碼更加簡煉,更加容易維護,容易擴展和複用,只有這樣纔可以是真的提高。寫出優雅的代碼真的是一種很爽的事情。不過學無止境,其實這纔是理解面向對象的開始呢。給你出個作業,做一個商場收銀軟件,營業員根據客戶購買商品單價和數量,向客戶收費。”

小菜:“就這個?沒問題呀。”
小菜心裏想:“大鳥要我做的是一個商場收銀軟件,營業員根據客戶購買商品單價和數量,向客戶收費。這個很簡單,兩個文本框,輸入單價和數量,再用個列表框來記錄商品的合計,最終用一個按鈕來算出總額就可,對,還需要一個重置按鈕來重新開始,不就行了?!”

代碼樣例(可使用):

 


商場收銀系統v1.0關鍵代碼如下:

//聲明一個double變量total來計算總計
        double total = 0.0d;
        
private void btnOk_Click(object sender, EventArgs e)
        
{
            
//聲明一個double變量totalPrices來計算每個商品的單價(txtPrice)*數量(txtNum)後的合計
            double totalPrices=Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text);
            
//將每個商品合計計入總計
            total = total + totalPrices;
            
//在列表框中顯示信息
            lbxList.Items.Add("單價:"+txtPrice.Text+" 數量:"+txtNum.Text+" 合計:"+totalPrices.ToString());
            
//在lblResult標籤上顯示總計數
            lblResult.Text = total.ToString();
        }

       “大鳥,”小菜叫道,“來看看,這不就是你要的收銀軟件嗎?我不到半小時就搞定了。”
       “哈哈,很快嗎,”大鳥說着,看了看小菜的代碼。接着說:“現在我要求商場對商品搞活動,所有的商品打8折。”
       “那不就是在totalPrices後面乘以一個0.8嗎?”
       “小子,難道商場活動結束,不打折了,你還要再把程序改寫代碼再去把所有機器全部安裝一次嗎?再說,我現在還有可能因爲週年慶,打五折的情況,你怎麼辦?”
        小菜不好意思道:“啊,我想得是簡單了點。其實只要加一個下拉選擇框就可以解決你說的問題。”
        大鳥微笑不語。

商場收銀系統v1.1關鍵代碼如下:

double total = 0.0d;
        
private void btnOk_Click(object sender, EventArgs e)
        
{
            
double totalPrices=0d;
            
//cbxType是一個下拉選擇框,分別有“正常收費”、“打8折”、“打7折”和“打5折”
            switch(cbxType.SelectedIndex)
            
{
                
case 0:
                    totalPrices 
= Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text);
                    
break;
                
case 1:
                    totalPrices 
= Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.8;
                    
break;
                
case 2:
                    totalPrices 
= Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.7;
                    
break;
                
case 3:
                    totalPrices 
= Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.5;
                    
break;

            }

            total 
= total + totalPrices;
            lbxList.Items.Add(
"單價:" + txtPrice.Text + " 數量:" + txtNum.Text + " "+cbxType.SelectedItem+ " 合計:" + totalPrices.ToString());
            lblResult.Text 
= total.ToString();
        }

       “這下可以了吧,只要我事先把商場可能的打折都做成下拉選擇框的項,要變化的可能性就小多了。”小菜說道。
       “這比剛纔靈活性上是好多了,不過重複代碼很多,像Convert.ToDouble(),你這裏就寫了8遍,而且4個分支要執行的語句除了打折多少以外幾乎沒什麼不同,應該考慮重構一下。不過還不是最主要的,現在我的需求又來了,商場的活動加大,需要有滿300返100的促銷算法,你說怎麼辦?”
        “滿300返100,那要是700就要返200了?這個必須要寫函數了吧?”
         “小菜呀,看來之前教你的白教了,這裏面看不出什麼名堂嗎?”   
         “哦!我想起來了,你的意思是簡單工廠模式是吧,對的對的,我可以先寫一個父類,再繼承它實現多個打折和返利的子類,利用多態,完成這個代碼。”
         “你打算寫幾個子類?”
         “根據需求呀,比如8折、7折、5折、滿300送100、滿200送50……要幾個寫幾個。”
        “小菜又不動腦子了,有必要這樣嗎?如果我現在要3折,我要滿300送80,你難道再去加子類?你不想想看,這當中哪些是相同的,哪些是不同的?”
         “對的,這裏打折基本都是一樣的,只要有個初始化參數就可以了。滿幾送幾的,需要兩個參數纔行,明白,現在看來不麻煩了。”
        “面向對象的編程,並不是類越多越好,類的劃分是爲了封裝,但分類的基礎是抽象,具有相同屬性和功能的對象的抽象集合纔是類 。打一折和打九折只是形式的不同,抽象分析出來,所有的打折算法都是一樣的,所以打折算法應該是一個類。好了,空話已說了太多,寫出來再是真的懂。”

         大約1個小時後,小菜交出了第三份的作業


商場收銀系統v1.3關鍵代碼如下

 

    //現金收取父類
    abstract class CashSuper
    
{
        
//抽象方法:收取現金,參數爲原價,返回爲當前價
        public abstract double acceptCash(double money);
    }

    
//正常收費,繼承CashSuper
    class CashNormal : CashSuper
    
{
        
public override double acceptCash(double money)
        
{
            
return money;
        }

    }

    
//打折收費,繼承CashSuper
    class CashRebate : CashSuper
    
{
        
private double moneyRebate = 1d;
        
//初始化時,必需要輸入折扣率,如八折,就是0.8
        public CashRebate(string moneyRebate)
        
{
            
this.moneyRebate = double.Parse(moneyRebate);
        }


        
public override double acceptCash(double money)
        
{
            
return money * moneyRebate;
        }

    }

    
//返利收費,繼承CashSuper
    class CashReturn : CashSuper
    
{
        
private double moneyCondition = 0.0d;
        
private double moneyReturn = 0.0d;
        
//初始化時必須要輸入返利條件和返利值,比如滿300返100,則moneyCondition爲300,moneyReturn爲100
        public CashReturn(string moneyCondition, string moneyReturn)
        
{
            
this.moneyCondition = double.Parse(moneyCondition);
            
this.moneyReturn = double.Parse(moneyReturn);
        }


        
public override double acceptCash(double money)
        
{
            
double result = money;
            
//若大於返利條件,則需要減去返利值
            if (money >= moneyCondition)
                result 
= money - Math.Floor(money / moneyCondition) * moneyReturn;

            
return result;
        }

    }

    
//現金收取工廠
    class CashFactory
    
{
        
//根據條件返回相應的對象
        public static CashSuper createCashAccept(string type)
        
{
            CashSuper cs 
= null;
            
switch (type)
            
{
                
case "正常收費":
                    cs 
= new CashNormal();
                    
break;
                
case "滿300返100":
                    CashReturn cr1 
= new CashReturn("300""100");
                    cs 
= cr1;
                    
break;
                
case "打8折":
                    CashRebate cr2 
= new CashRebate("0.8");
                    cs 
= cr2;
                    
break;
            }

            
return cs;
        }

    }


    
//客戶端窗體程序(主要部分)
    CashSuper csuper;//聲明一個父類對象
    double total = 0.0d;
    
private void btnOk_Click(object sender, EventArgs e)
    
{
        
//利用簡單工廠模式根據下拉選擇框,生成相應的對象
        csuper = CashFactory.createCashAccept(cbxType.SelectedItem.ToString());
        
double totalPrices=0d;
        
//通過多態,可以得到收取費用的結果
        totalPrices = csuper.acceptCash(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
        total 
= total + totalPrices;
        lbxList.Items.Add(
"單價:" + txtPrice.Text + " 數量:" + txtNum.Text + " "+cbxType.SelectedItem+ " 合計:" + totalPrices.ToString());
        lblResult.Text 
= total.ToString();
    }

代碼樣例(可使用)

 


      “大鳥,搞定,這次無論你要怎麼改,我都可以簡單處理就行了。”小菜自信滿滿的說。
      “是嗎,我要是需要打5折和滿500送200的促銷活動,如何辦?”
      “只要在現金工廠當中加兩個條件,在界面的下拉選項框里加兩項,就OK了。”
      “現金工廠?!你當是生產鈔票呀。是收費對象生成工廠才準確。說得不錯,如果我現在需要增加一種商場促銷手段,滿100積分10點,以後積分到一定時候可以領取獎品如何做?”
      “有了工廠,何難?加一個積分算法,構造方法有兩個參數:條件和返點,讓它繼承CashSuper,再到現金工廠,哦,不對,,是收—費—對—象—生—成—工—廠里加滿100積分10點的分支條件,再到界面稍加改動,就行了。”
      “嗯,不錯,那我問你,如果商場現在需要拆遷,沒辦法,只能跳樓價銷售,商場的所有商品都需要打8折,打折後的價錢再每種商品滿300送50,最後計總價的時候,商場還滿1000送200,你說如何辦?”
      “搞沒搞錯哦,這商場不如白送得了,哪有這樣促銷的?老闆跳樓時估計都得赤條條的了。”
       “商場大促銷你還不高興呀!當然,你是軟件開發者,客戶老是變動需求的確不爽,但你不能不讓客戶提需求呀,我不是說過嗎,需求的變更是必然!所以開發者應該的是考慮如何讓自己的程序更能適應變化,而不是抱怨客戶的無理,客戶不會管程序員加班時的汗水,也不相信程序員失業時的眼淚,因爲客戶自己正在爲自己的放血甩賣而流淚呀。”
        大鳥接着說:“簡單工廠模式雖然也能解決這個問題,但的確不是最好的辦法,另外由於商場是可能經常性的更改打折額度和返利額度,每次更改都需要改寫代碼重新編譯部署真的是很糟糕的處理方式,面對算法的時常變動,應該有更好的辦法。好好去研究一下設計模式吧,推薦你看一本書,《深入淺出設計模式》,或許你看完第一章,就會有解決辦法了。
        小菜進入了沉思中……
 

(待續)


另:建議大家去閱讀《深入淺出設計模式》,第一章下載,本人非常喜歡這本書的風格,這是真正的做到了深入淺出呀。我也希望自己可以用類似的方式講述問題。
本文還有一個用意是對一些初學者,可以考慮一下大鳥提出的問題,在我的下一篇《小菜編程成長記 八》出來之前,改寫我的源代碼,實現更靈活更方便的商場收銀程序共享給大家討論,或許您寫的東東比我寫的還要好,那樣就大家都有提高了。程序不是看出來的,是寫出來的。好好加油

 

小菜次日來找大鳥,說:“《深入淺出設計模式》的第一章我看完了,它講的是策略模式(Strategy)。『策略模式』定義了算法家族,分別封裝起來,讓它們之間可以互相替換, 此模式讓算法的變化, 不會影響到使用算法的客戶。看來商場收銀系統應該考慮用策略模式?”
      “你問我?你說呢?”大鳥笑道,“商場收銀時如何促銷,用打折還是返利,其實都是一些算法,用工廠來生成算法對象,感覺是不是很怪?而最重要的是這些算法是隨時都可能互相替換的,這就是變化點,而封裝變化點是我們面向對象的一種很重要的思維方式。”

       策略模式的結構 (源自呂震宇 博客)
       


      這個模式涉及到三個角色:

 

  • 環境(Context)角色:持有一個Strategy類的引用。
  • 抽象策略(Strategy)角色:這是一個抽象角色,通常由一個接口或抽象類實現。此角色給出所有的具體策略類所需的接口。
  • 具體策略(ConcreteStrategy)角色:包裝了相關的算法或行爲。

      “我明白了,”小菜說,“我昨天寫的CashSuper就是抽象策略,而正常收費CashNormal、打折收費CashRebate和返利收費CashReturn就是三個具體策略,也就是策略模式中說的具體算法,對吧?”


      “是的,那麼關鍵就在於Context以及客戶端程序如何寫了?去查查資料,研究後把代碼寫出來給我看。”大鳥鼓勵道。
      “好的,我一定很快寫出來給你看!”小菜很興奮。

        過一小時後,小菜給出商場收銀程序的第四份作業。
CashContext類代碼如下:

 

    
    
//收費策略Context
    class CashContext
    
{
        
//聲明一個現金收費父類對象
        private CashSuper cs;

        
//設置策略行爲,參數爲具體的現金收費子類(正常,打折或返利)
        public void setBehavior(CashSuper csuper)
        
{
            
this.cs = csuper;
        }


        
//得到現金促銷計算結果(利用了多態機制,不同的策略行爲導致不同的結果)
        public double GetResult(double money)
        
{
            
return cs.acceptCash(money);
        }

    }

客戶端主要代碼如下:


       
double total = 0.0d;//用於總計
        private void btnOk_Click(object sender, EventArgs e)
        
{
            CashContext cc 
= new CashContext();
            
switch (cbxType.SelectedItem.ToString())
            
{
                
case "正常收費":
                    cc.setBehavior(
new CashNormal());
                    
break;
                
case "滿300返100":
                    cc.setBehavior(
new CashReturn("300","100"));
                    
break;
                
case "打8折":
                    cc.setBehavior(
new CashRebate("0.8"));
                    
break;
            }

            
            
double totalPrices = 0d;
            totalPrices 
= cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
            total 
= total + totalPrices;
            lbxList.Items.Add(
"單價:" + txtPrice.Text + " 數量:" + txtNum.Text + " "+cbxType.SelectedItem+ " 合計:" + totalPrices.ToString());
            lblResult.Text 
= total.ToString();
        }

實現的界面同之前一樣(可點擊使用)

 


       “大鳥,我用策略模式是實現了,但有些疑問,用了策略模式,則把分支判斷又放回到客戶端來了,這等於要改變需求算法時,還是要去更改客戶端的程序呀?”
       “問得好,如果不是因爲前面有工廠的例子,再來通過你的思考寫出的這個策略模式的程序,你就問不出這樣的問題的。”大鳥很開心,繼續講道,“最初的策略模式是有缺點的,客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味着客戶端必須理解這些算法的區別,以便適時選擇恰當的算法類。換言之,策略模式只適用於客戶端知道所有的算法或行爲的情況。”
        “那還不如工廠模式好用,至少要增加促銷或改進打折額度時,不用去大改界面,而現在,界面程序要承擔的責任還是太大。沒有體現你說的封裝變化點的作用呀。”小菜疑問多多。
       “就目前而言,的確是這樣,這樣的程序確實還是不夠完善,要改的地方還很多。”大鳥說道,“不過正所謂病毒時間長了會有變種,殺毒軟件本身也會隨着病毒的變化而升級改良,如果我們對策略模式做一些改進,引入一些新的技術處理方式,就可以避免現在的這種耦合了。小菜,又有新的東西要學了,好好加油呀!”
      “大鳥,謝謝你,,你總是讓我帶着問題去思考,而不是直接說答案,我覺得這樣學習進步很快,也不覺得設計模式很難了。” 
      “,用不着這麼客氣,我只是覺得,沒有人是天生就牛X的,有一些所謂的技術牛人總會在人面前說什麼,‘你連這都不懂’,‘這還不簡單了,你夠笨的’等等說詞。給人感覺他非常聰明,天生就會的樣子,其實他在之前也不知走過多少彎路,犯過多少錯,或許他之前也被更早的牛人羞辱過,所以再繼續把羞辱傳給後人。”大鳥有些激動。
       小菜小心的說道:“大鳥,你是不是也曾經被人羞……”
       “哈哈,馬雲曾說過,男人的胸懷是被冤枉撐大的!天天在這行當裏混,閱人無數,被羞辱也是正常的事了。問題在於是不是頭腦清醒,自己不能放棄呀。所以我希望能真正的幫助初學者成長,而不是去顯示牛氣充當狂人。小菜,記住,學習一定是一個自己感悟的過程,而程序員的感悟就是自己寫程序做項目,通過實踐再學習,最終昇華爲牛人。
      “嗯,我記住了,不過到底如何改良策略模式呢?”
       大鳥微笑不語

“到底如何去改良策略模式呢?”小菜懇切地問道。
         “你仔細觀察過沒有,你的代碼,不管是用工廠模式寫的,還是用策略模式寫的,那個分支的switch依然去不掉。原因在哪裏?”大鳥反問道。
          “因爲程序裏有下拉選擇,用戶是有選擇的,那麼程序就必須要根據用戶的選擇來決定實例化哪一個子類對象。無論是在客戶端窗體類編程還是到工廠類裏編程,這個switch總是少不掉的。問題主要出在這裏。”小菜十分肯定的說。
         “是呀,”大鳥道,“所以我們要考慮的就是可不可以不在程序裏寫明‘如果是打折就去實例化CashRebate類,如果是返利就去實例化CashReturn類’這樣的語句,而是在當用戶做了下拉選擇後,再根據用戶的選擇去某個地方找應該要實例化的類是哪一個。這樣,我們的switch就可以對它說再見了。”
        “聽不太懂哦,什麼叫‘去某個地方找應該要實例化的類是哪一個’?’小菜糊塗地說
        “,我要說的就是一種編程方式:依賴注入(Dependency Injection),從字面上不太好理解,我們也不去管它。關鍵在於如何去用這種方法來解決我們的switch問題。本來依賴注入是需要專門的IoC容器提供,比如spring.net,顯然當前這個程序不需要這麼麻煩,你只需要再瞭解一個簡單的.net技術‘反射’就可以了。”
        “大鳥,你一下子說出又是‘依賴注入’又是‘反射’這些莫名其妙的名詞,我有點暈哦!”小菜有些犯困,“我就想知道,如何向switch說bye-bye!至於那些什麼概念我不想了解。”
        “心急討不了好媳婦!你急什麼?”大鳥嘲笑道,“反射技術看起來很玄乎,其實實際用起來不算難。”

       “請看下面的兩個樣例:

1//實例化方法一   
2//原來我們把一個類實例化是這樣的
3Animal animal=new Cat();  //聲明一個動物對象,名稱叫animal,然後將animal實例化成貓類的對象
4
5//實例化方法二
6//我們還可以用反射的辦法得到這個實例
7using System.Reflection;//先引用System.Reflection
8//假設當前程序集是AnimalSystem,名稱空間也是AnimalSystem
9Animal animal = (Animal)Assembly.Load("AnimalSystem").CreateInstance("AnimalSystem.Cat");

 其中關鍵是

Assembly.Load("程序集名稱").CreateInstance("名稱空間.類名稱")

那也就是說,我們可以在實例化的時候,再給計算機一個類的名稱字符串,來讓計算機知道應該實例化哪一個類。”大鳥講解道。
       “你的意思是,我之前寫的‘cc.setBehavior(new CashNormal());’可以改寫爲‘cc.setBehavior((CashSuper)Assembly.Load("商場管理軟件").CreateInstance("商場管理軟件.CashNormal")’,不過,這只不過是換了種寫法而已,又有什麼神奇之處呢?”小菜依然迷茫。
        “分析一下,原來new CashNormal()是什麼?是否是寫死在程序裏的代碼,你可以靈活更換嗎?”大鳥問。
        “不可以,那還換什麼,寫什麼就是什麼了唄。”
        “那你說,在反射中的CreateInstance("商場管理軟件.CashNormal"),可以靈活更換‘CashNormal’嗎?”大鳥接着問。
        “還不是一樣,寫死在代碼…………等等,哦!!!我明白了。”小菜一下子頓悟過來,,興奮起來。“因爲這裏是字符串,可以用變量來處理,也就可以根據需要更換。哦,My God!太妙了!”
       “哈哈,博客園中的有篇博文《四大發明之活字印刷——面向對象思想的勝利》中曾經寫過,‘體會到面向對象帶來的好處,那種感覺應該就如同是一中國酒鬼第一次喝到了茅臺,西洋酒鬼第一次喝到了XO一樣,怎個爽字可形容呀。’,你有沒有這種感覺了?”
        “嗯,我一下子知道這裏的差別主要在原來的實例化是寫死在程序裏的,而現在用了反射就可以利用字符串來實例化對象,而變量是可以
更換的。”小菜說道。
        “由於字符串是可以寫成變量,而變量的值到底是CashReturn(返利),還是CashRebate(打折),完全可以由誰決定?”大鳥再問。
         “當然是由用戶在下拉中選擇的選項決定,也就是說,我只要把下拉選項的值改成這些算法子類的名稱就好了,是吧?”
        “你說得對,不過還不是最好。因爲把comboBox的每個選項value都改爲算法子類的名稱。以後我們要加子類,你不是還要去改comboBox嗎?繼續往下想,現在我們的代碼對有誰依賴?”
        “對下拉控件comboBox的選項有依賴。”
        “那麼怎麼辦,這個控件的選項可不可以通過別的方式生成。比如利用它的綁定?”
        “你的意思是讀數據庫?”
        “讀數據庫當然最好了,其實用不着這麼麻煩,我們不是有XML這個東東嗎,寫個配置文件不就解決了?”
        “哦,我知道你的意思了,讓它去讀XML的配置文件,來生成這個下拉列表框,然後再根據用戶的選擇,通過反射實時的實例化出相應的算法對象,最終利用策略模式計算最終的結果。好的好的,我馬上去寫出來。我現在真有一種不把程序寫出來就難受的感覺了。”小菜急切的說。
       “OK,還有一個小細節,你的CashRebate和CashReturn在構造函數中都是有參數的,這需要用到CreateInstance()方法的重載函數,不會用去查幫助吧!”
       “好嘞!你別走哦,等我,不見不散!”小菜向外跑着還叫道。
        大鳥搖頭苦笑,嘴裏嘟囔着:“這小子,忒急了吧!還不見不散呢,難道真沒完沒了啦!”

一個小時後,小菜交出了商場收銀程序的第五份作業。

客戶端主要代碼:

       using System.Reflection;

       DataSet ds;
//用於存放配置文件信息
        double total = 0.0d;//用於總計

        
private void Form1_Load(object sender, EventArgs e)
        
{
            
//讀配置文件
            ds = new DataSet();
            ds.ReadXml(Application.StartupPath 
+ "//CashAcceptType.xml");
            
//將讀取到的記錄綁定到下拉列表框中
            foreach (DataRowView dr in ds.Tables[0].DefaultView)
            
{
                cbxType.Items.Add(dr[
"name"].ToString());
            }

            cbxType.SelectedIndex 
= 0;
        }


        
private void btnOk_Click(object sender, EventArgs e)
        
{
            CashContext cc 
= new CashContext();
            
//根據用戶的選項,查詢用戶選擇項的相關行
            DataRow dr = ((DataRow[])ds.Tables[0].Select("name='" + cbxType.SelectedItem.ToString()+"'"))[0];
            
//聲明一個參數的對象數組
            object[] args =null;
            
//若有參數,則將其分割成字符串數組,用於實例化時所用的參數
            if (dr["para"].ToString() != "")
                args 
= dr["para"].ToString().Split(',');
            
//通過反射實例化出相應的算法對象
            cc.setBehavior((CashSuper)Assembly.Load("商場管理軟件").CreateInstance("商場管理軟件." + dr["class"].ToString(), false, BindingFlags.Default, null, args, nullnull));
            
            
double totalPrices = 0d;
            totalPrices 
= cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
            total 
= total + totalPrices;
            lbxList.Items.Add(
"單價:" + txtPrice.Text + " 數量:" + txtNum.Text + " "+cbxType.SelectedItem+ " 合計:" + totalPrices.ToString());
            lblResult.Text 
= total.ToString();
        }

配置文件 CashAcceptType.xml 的代碼

 1<?xml version="1.0" encoding="utf-8" ?>
 2<CashAcceptType>
 3    <type>
 4        <name>正常收費</name>
 5        <class>CashNormal</class>
 6        <para></para>
 7    </type>
 8    <type>
 9        <name>滿300返100</name>
10        <class>CashReturn</class>
11        <para>300,100</para>
12    </type>
13    <type>
14        <name>滿200返50</name>
15        <class>CashReturn</class>
16        <para>200,50</para>
17    </type>
18    <type>
19        <name>打8折</name>
20        <class>CashRebate</class>
21        <para>0.8</para>
22    </type>
23    <type>
24        <name>打7折</name>
25        <class>CashRebate</class>
26        <para>0.7</para>
27    </type>
28</CashAcceptType>

實現的界面同之前一樣(可點擊使用)


          “大鳥,我再次搞定了,這會是真的明白了。”小菜說。
         “說說看,你現在的理解!”大鳥問。
         “無論你的需求是什麼,我現在連程序都不動,只需要去改改XML文件就全部擺平。比如你如果覺得現在滿300送100太多,要改成送80,我只需要去XML文件裏改就行,再比如你希望增加新的算法,比如積分返點,那我先寫一個返點的算法類繼承CashSuper,再去改一下XML文件,對過去的代碼依然不動。總之,現在是真的做到了程序易維護,可擴展。”小菜得意地壞笑道,“吼吼!此時商場老闆以爲要改一天的程序,我幾分鐘就搞定,一天都可以休息。反射——真是程序員的快樂呀!”
       “在做夢了吧,你當老闆是傻瓜,會用反射纔是正常水平,不會用的早應該走人了。”大鳥打擊了小菜的情緒,“不過呢小菜的確是有長進,不再是小菜鳥了。那你說說看,現在代碼還有沒有問題。”
        “還有不足?不會吧,我都改5次了,重構到了這個地步,還會有什麼問題?”小菜不以爲然。
       “知足是可以常樂,但知足如何能進步!你的代碼真的沒有問題了,比如說,你現在把列表是打印在了listBox列表框中,我現在還需要輸出到打印機打印成交易單據,我還希望這些清單能存入數據庫中,你需要改客戶端的代碼嗎?”
       “這個,你這是加需求了,更改當然是必須的。”
       “更改是必須的沒有錯,但爲什麼我只是要對交易清單加打印和存數據,就需要去改客戶端的代碼呢?這兩者沒什麼關係吧?”大鳥說。
       “啊,你的意思是…………”
        “別急着下結論,先去好好思考一下再說。”大鳥打斷了小菜。

小菜學會了反射後,正在興奮,想着大鳥的問題。此時,突然聲音響起。
      “死了都要愛,不淋漓盡致不痛快,感情多深只有這樣,才足夠表白。死了都要愛……”
       原來是小菜的手機鈴聲,大鳥嚇了一跳,說道:”你小子,用這歌做鈴聲,嚇唬人啊!這要是在公司開大會時響起,你要被領導淋漓盡致愛死!MD,還在唱,快接!”
       小菜很是鬱悶,拿起手機一看,一個美女來的電話,由,馬上接通了手機,“喂!”
      “小菜呀,我是嬌嬌我電腦壞了你快點幫幫我呀!”手機裏傳來急促的女孩聲音。
      “哈,是你呀,你現在好嗎?最近怎麼不和我聊天了?”小菜慢條斯理的說道。
      “快點幫幫我呀,,電腦不能用了啊!”嬌嬌略帶哭腔的說。
      “別急別急,怎麼個壞法?”
      “每次打開QQ,一玩遊戲,機器就死了。出來藍底白字的一堆亂七八糟的英文,過一會就重啓了,再用QQ還是一樣。怎麼辦呀?”
      “哦,明白了,藍屏死機吧,估計內存有問題,你的內存是多少兆的?”
      “什麼內存多少兆,我聽不懂呀,你能過來幫我修一下嗎?”
      “啊,你在金山,我在寶山,雖說在上海兩地名都錢味兒十足,可兩山相隔萬重路呀!現在都晚上了,又是星期一,週六我去你那裏幫你修吧!”小菜無耐的說。
     “要等五天那不行,你說什麼藍屏?怎麼修法?”嬌嬌依然急不可待。
     “藍屏多半內存壞了,你要不打開機箱看看,或許有兩個內存,可以拔一根試試,如果只有一根內存,那就沒戲了。”
     “機箱怎麼打開呢?”嬌嬌開始認真起來。
     “這個,你找機箱後面,四個角應該都有螺絲,靠左側邊上兩個應該就可以打開左邊蓋了。”小菜感覺有些費力,遠程手機遙控修電腦,這是頭一次。
     “我好象看到了,要不先掛電話,我試試看,打開後再打給你。”
     “哦,好的。”小菜正說着,只聽嬌嬌邊嘟囔着“老孃就不信收拾不了你這破電腦”邊掛掉了電話。

     “呵!”小菜長出一口氣,“不懂內存爲何物的美眉修電腦,強!”
     “你小子,人家在困難時刻想得到你,說明心中有你,懂嗎?這是機會!”大鳥說道。
     “這倒也是,這小美眉長得蠻漂亮的,我看過照片。就是脾氣大些,不知道有沒有男朋友了。”
     “切,你幹嗎不對她說,‘你可以找男友修呀’,真是沒腦子,要是有男友,就算男友不會修也要男友找人搞定,用得着找你求助呀,笨笨!”大鳥嘲笑道,“你快把你那該死的手機鈴聲換掉——死了都要愛,死了還愛個屁!”
     “噢!知道了。”

      十分鐘後。

     “我在這兒等着你回來,等着你回來,看那桃花開。我在這兒等着你回來,等着你回來,把那花兒採……”小菜的手機鈴聲再次響起。
      “菜花癡,你就不能找個好聽的歌呀。”大鳥氣着說道。
     “好好好,我一會改,一會改。”小菜拿起手機,一副很聽話的樣子,嘴裏卻跟着哼“我在這兒等着你回來哎”,把手機放到耳邊。
      “小菜,我打開機箱了,快說下一步怎麼走!”嬌嬌仍然着急着說。
     “你試着找找內存條,內存大約是10公分長,2公分寬,上有多個小長方形集成電路塊的長條,應該是豎插着的。”小菜努力把內存樣子描述得容易理解。
       “我看到一個風扇,沒有呀,在哪裏?”嬌嬌說道,“哦,我找到了,是不是很薄,很短的小長條?咦,怎麼有兩根?”
       “啊,太好了,有兩根估計就能解決問題了,你先試着拔一根,然後開機試試看,如果還是死機,再插上,撥另一根試,應該總有一根可以保證不藍屏。”
      “我怎麼撥不下來呢?”
      “旁邊有卡子,你扳開再試。”
      “嗯,這下好了,你別掛,我這就重啓看看。”

         十分鐘後。

       “哈,沒有死機了啊,小菜,你太厲害了,我竟然可以修電腦了,要我怎麼感謝你呢!”嬌嬌興奮地說
      “最好以身相許吧,”小菜心裏這麼遐想着,口中卻謙虛地說:“不客氣,都是你聰明,敢自己獨自打開機箱修電腦的女孩很少的。你把換下的內存去電腦城換掉,就可以了。”
      “我不懂的,要不周六你幫我換?週六我請你喫飯吧!”
      “這怎麼好意思——你說在什麼時間在哪碰面?”小菜假客氣着,卻不願意放棄機會。
      “週六下午5點在徐家彙太平洋數碼門口吧。”
      “好的,沒問題。”
      “今天真的謝謝你,那就先Bye-Bye了!”
      “嗯,拜拜!”

      “小菜走桃花運了哦,”大鳥有些羨慕道,“那鈴聲看來有些效果,不過還是換掉吧,俗!”
     “嘿嘿,你說也怪,修電腦,這在以前根本不可能的事,怎麼就可以通過電話就教會了,而且是真的修到可以用了呢。”
     “你有沒有想過這裏的最大原因?”大鳥開始上課了。
     “藍屏通常是內存本身有問題或內存與主板不兼容,主板不容易換,但內存卻只需要更換就可以了,而且換起來很容易。”
     “如果是別的部件壞了,比如硬盤,顯卡,光驅等,是否也只需要更換就可以了?”
     “是呀,確實很方便,只需要懂一點點計算機知識,就可以試着修電腦了。”
     “想想和我們編程有什麼聯繫?”
     “你的意思是——面向對象?”
     “說說看,面向對象的四個好處?”
     “這個我記得最牢了,就是活字印刷那個例子啊,是可維護、可擴展、可複用和靈活性好。我知道了,可以把PC電腦理解成是大的軟件系統,任何部件如CPU、內存、硬盤,顯卡等都可以理解爲程序中封裝的類或程序集,由於PC易插撥的方式,那麼不管哪一個出問題,都可以在不影響別的部件的前題下進行修改或替換。”
     “PC電腦裏叫易插撥,面向對象裏把這種關係叫什麼?”
     “應該是叫強內聚、松耦合吧。”
     “對的,非常好,我們電腦裏的CPU全世界也就是那麼幾家生產的,大家都在用,可是就是不知道Intel/AMD等是如何做出這個小東西。去年國內不是還出現了漢芯造假的新聞嗎!這就說明CPU的強內聚的確是強。但它又獨自成爲了產品可以在千千萬萬的電腦主板上插上就可以使用,這是什麼原因?”大鳥又問。
       “因爲CPU的對外都是針腳式或觸點式等標準的接口。啊,我明白了,這就是接口的最大好處。CPU只需要把接口定義好,內部再複雜我也不讓外界知道,而主板只需要預留與CPU針腳的插槽就可以了。”
       “很好,你已經在無意的談話間提到了設計模式其中的幾大設計原則,單一職責原則,開放—封閉原則,依賴倒轉原則(參考《敏捷軟件開發——原則、模式與實踐》)”大鳥接着講道,“所謂單一職責原則,就是指就一個類而言,應該僅有一個引起它變化的原因,就剛纔修電腦的事,顯然內存壞了,不應該成爲更換CPU的理由。開放—封閉原則是說對擴展開發,對修改關閉,通俗的講,就是我們在設計一個模塊的時候,應當使這個模塊可以在不被修改的前提下被擴展,換句話說就是,應當可以在不必修改源代碼的情況下改變這個模塊的行爲。比如內存不夠只要插槽多就可以加,比如硬盤不夠了,可以用移動硬盤等,PC的接口是有限的,所以擴展有限,軟件系統設計得好,卻可以無限的擴展。依賴倒轉原則,原話解釋是抽象不應該依賴細節,細節應該依賴於抽象,這話繞口,說白了,就是要針對接口編程,不要對實現編程,無論主板、CPU、內存、硬盤都是在針對接口編程,如果針對實現編程,那就會出現換內存需要把主板也換了的尷尬。你想在小MM面前表現也就不那麼容易了。所以說,PC電腦硬件的發展,和麪向對象思想發展是完全類似的。這也說明世間萬物都是遵循某種類似的規律,誰先把握了這些規律,誰就最早成爲了強者。” 
        “還好,她沒有問我如何修收音機,收音機裏都是些電阻、三極管,電路板等等東東,我可不會修的。”小菜慶幸道。
       “哈,小菜你這個比方打得好,”大鳥開心的說,“收音機就是典型的耦合過度,只要收音機出故障,不管是聲音沒有、不能調頻、有雜音,反正都很難修理,不懂的人根本沒法修,因爲任何問題都可能涉及其它部件。非常複雜的PC電腦可以修,反而相對簡單的收音機不能修,這其實就說明了很大的問題。當然,電腦的所謂修也就是更換配件,CPU或內存要是壞了,老百姓是沒法修的。其實現在在軟件世界裏,收音機式強耦合開發還是太多了,比如前段時間某銀行出問題,需要服務器停機大半天的排查修整,這要損失多少錢。如果完全面向對象的設計,或許問題的查找和修改就容易得多。”
       “是的是的,我聽說很多銀行目前還是純C語言的面向過程開發,非常不靈活,維護成本是很高昂的。”
      “那也是沒辦法的,銀行系統哪是說換就換的,所以現在是大力鼓勵年輕人學設計模式,直接面向對象的設計和編程,從大的方向上講,這是國家大力發展生產力的很大保障呀。”
     “大鳥真是高瞻遠矚呀,我對你的敬仰猶如滔滔江水,連綿不絕!”小菜怪笑道,“我去趟WC”。
      
     “浪奔,浪流,萬里江海點點星光耀,人間事,多紛擾,化作滾滾東逝波濤,有淚,有笑…………”
     “小菜,電話。小子,怎麼又換成上海灘的歌了,這歌好聽。”大鳥笑道,“剛纔是死了都要愛,現在是爲愛復仇而死。你怎麼找的歌都跟愛過不去呀。快點,電話,又是剛纔那個叫嬌嬌的小MM的。”
     “來了來了,尿都只尿了一半!”小菜心急地接起電話,“喂!”
     “小菜呀,我家收音機壞了,你能不能教我修修呢!”

“大鳥,我們繼續討論吧!”小菜很沮喪的說。
         “小夥子,不會修收音機也是很正常的,沒什麼大不了的,用不着喪着一個臉。好象失戀一樣,男人再強也要學會說‘不’。”大鳥安慰着說,“如果你的目標是要成爲修理電器專家,那麼你連收音機都不會修,那是很鬱悶的事。但你現在的目標是什麼?”
       “我想成爲軟件架構師,編程專家。”小菜毫不含糊的說。
      “就是,你的人生目標很明確,別的方面弱一些有什麼關係呢。”大鳥繼續說道,“現在電視節目《波士堂》裏請來的嘉賓,全是中國的大企業家,許多人身家上億,節目中都要求他們要有一個Boss秀,難道真的要把他們的才藝去和人家藝術家比嗎,我看老闆們唱歌雖很業餘,但卻也感覺得到他們那份認真和情趣——原來億萬富翁也是會唱歌,會跳舞,會食人間煙火的。至於他們歌唱得是不是跑調沒有人在意的,明白嗎?”
      “我明白!,我一定要好好努力,成爲編程專家。”,小菜說,“我們言歸正傳,你說我那程序用了反射後,還有什麼需要修改的呢?”
      “嗯,好!”大鳥清了清嗓子,開始上課,“如果你的程序再也不修改了,或者就是改改打折的額度和返利額度,那麼你的代碼是足夠可以了。不過需求卻是會不斷產生的。比如說,現在這個程序是單機版的程序,如果需要商場多層樓的所有收銀機都要使用,那該怎麼辦?”
      “那用XML的配置文件就不合適了,應該用數據庫會比較好!”
      “那麼老闆聽說了C/S架構的壞處,更新麻煩,不夠安全等等,他也不是傻瓜,每次更新都需要針對每臺機器部署,一次就半天,那些工作時間他是需要給程序員付薪水的。所以他提出要改爲B/S架構,客戶端用瀏覽器支持,你怎麼辦?”
      “那需要改界面了,把應用程序改成Web程序。”
      “就你現在的代碼,改起來容易嗎?”
      “好象不容易,需要重新寫,儘管可以複製一些代碼過去,不過要重新寫的東西還是很多的。”
      “好,那你有沒有發現,我說了這麼多的需求變動,但系統中有一些東西一直沒有變,是哪些?”
      “我知道,是策略模式用到的那幾個類,也就是正常收費、打折消費、返利消費等算法是沒有變化的。”
      “是呀,其實不是算法不會變,而是之前我們已經考慮它很多了,用了策略模式,用了反射技術使得它的變化相對穩定。你剛纔也說,要把應用程序改爲Web是需要複製粘貼的,可實際上,改改界面和這些算法有什麼關係?”
      “沒有關係。”     
      “還有,把配置文件改爲數據庫訪問,這其實是讀取寫入數據的操作,和算法又有什麼關係呢?”
      “也沒有關係,我知道了,你是說,他們之間完全可以分離開,互不影響,改動其一,不要影響其它兩者?哦,這是不是就是所謂的三層架構?”
      “對,說得好,就是三層架構。三層架構或者分層開發說起來容易,在程序開發時的初學者還是有很多的誤解。比如有些初學者以爲,DBServer-WebServer-Client是三層架構,其實這是物理意思上的三層架構,和程序的三層架構沒有什麼關係。還有人以爲,WinForm界面的窗體或者WebForm的aspx是最上一層,它們對應的代碼後置(codebehind)文件Form.cs或aspx.cs是第二層,然後再有一個訪問數據庫的代碼,比如ado.cs或SqlHelper.cs是最下一層,這其實也是非常錯誤的理解。再有,很多人認爲MVC模式(Model-View-Controler)就是三層架構,這是比較經典的錯誤理解了。總之,儘管三層架構不算難,不過由於現在很多數書籍材料的講解不透,所以讓我們初學者都概念模糊,理解有誤,非常的可惜的。”
      “啊,我一直以爲MVC就是三層架構呀,看來真的弄錯了。那麼三層具體是什麼呢?”
      “我不是已經告訴你了嗎?你說說看,不管是應用程序WinForm,還是網頁程序Aspx,它們主要用來幹嗎的?”
      “用來界面顯示和處理的,對的,它們可以看作是一層。叫界面層?”
      “界面層這種叫法可以,或者叫UI層、表現層都可以。”
      “訪問配置文件或處理數據庫是不是就是數據層了?”
      “哈,三層架構是不是不難理解呀!說得很對,不過名稱應該叫做數據訪問層(Data Access Layer)或簡稱DAL層。”
     “那麼第三個層就是那些算法類了,這叫什麼層呢?”
     “這些算法是誰制定的?由誰來決定其變化?”
     “當然是需求提出者,即軟件系統所有者制定的,他們要改算法,我們開發就得改。這都是他們的業務算法呀!”
     “哈,好,你說到了一個詞,業務(Business)或叫商務,這其實是軟件的核心,我們就是根據業務規則來開發軟件提供服務的,所以這個層叫做業務邏輯層(Business Logic Layer)。不過它應該是中間的一層,介於另兩者之間。”
     “哦,所謂的三層開發,就是關於表現層、業務邏輯層和數據訪問層的開發。那麼他們之間的關係呢?”
     “你需要知道,這其實只是大方向的分層,每個層中都有可能再細分爲多個層次和結構。比如PetShop4,這是微軟用它來展示.Net企業系統開發的能力的範例,PetShop儘管作爲對大型軟件系統開發的樣例還是不夠,但可以理解爲兒童的智力玩具。不過對於初學編程的小菜你來說,玩具卻是最好的學習道具。”

下面圖源自Bruce Zhang博客



 
      “如果是要細化,可能結構就會變得很複雜。比如給你看看PetShop4的結構圖。”大鳥繼續說道。

       “啊,上面那圖我是明白了,下面這圖看得暈暈乎乎的,哪有這樣複雜的玩具,大鳥又在故弄玄虛,快點解釋一下?”小菜疑惑的說。
       “第一次看到就完全看明白,那不就成天才了。學習它還需要慢慢來,以後再說。你現在應該對改寫商場收銀系統有點數了吧,應該怎麼做呢?”
       “應該原來的解決方案分爲三個項目,一個UI項目,目前是WinForm的程序,一個BLL項目,用來把算法類都封裝,還有一個DAL項目,用來訪問配置文件。對嗎?”
      “嗯,差不多了,快去改吧,口說容易,實踐中會有很多細節問題等着你去解決的。”
      “好的,不過今天不行了,我前幾天面試的一家公司給我Offer了,我明天就要去第一天上班,明晚我再去改寫這個程序。”小菜說道。
      “恭喜恭喜,就是你之前提到了那家做物流軟件的公司嗎?找到工作你得請客啦。”
      “No problem,不過等我發工資吧。就是那一家。感覺公司還是很大的。”
      “那你快去休息吧,第一天要好好表現哦!”
(待續)
應一些回覆朋友的要求,專門寫了關於Web架構方面的文章,本篇還只是簡單介紹。其實這些都不是新鮮的東西,如果你認爲自己的確是小菜,我建議你去下載上一篇的代碼來根據本篇的介紹去改寫,編程是實踐性很強的技術,理解不等於會應用的。
 次日傍晚,小菜敲開了大鳥家的門。
        “回來啦!怎麼樣?第一天上班感受多吧。”大鳥關心的問道。
        “感受真是多哦!!!”小菜一臉的不屑一顧
        “怎麼了?受委屈了嗎。說說看怎麼回事?”
        “委屈談不上,就感覺公司氛圍不是很好。我一大早就到他們公司,正好我的主管出去了不在公司。人事處的小楊讓我填了表後,就帶我到IT部領取電腦,她向我介紹了一個叫‘小張’的同事認識,說我跟他辦領取電腦的手續就可以了。小張還蠻客氣,正打算要裝電腦的時候,來了個電話,叫他馬上去一個客戶那裏處理PC故障,他說要我等等,回來幫我弄。我坐了一上午,都沒有見他回來,但我發現IT部其實人還有兩個人,他們都在電腦前,一個忙於QQ,一個好象在看新聞。我去問人事的小楊,可不可以請其他人幫我辦理領取手續,她說她現在也在忙,讓我自己去找一下IT部的小李,他或許有空。我又返回IT部辦公室,問小李幫忙,小李先是忙着回了兩個QQ後才接過我領取電腦的單子,看到上面寫着‘張XX’負責電腦領取安裝工作,於是說這個事是小張負責的,他不管,叫我還是等小張回來再做吧。我就這樣又像皮球一樣被踢到桌邊繼續等待,還好我帶着一本《重構》在看,不然真要鬱悶死。小張快到下班的時候纔回來,開始幫我裝系統,加域,設置密碼等,其實也就Gohost恢復再設置一下,差不多半小時就弄好了。”小菜感嘆的說道,“就這樣,我這人生一個最重要的第一次就這麼渡過了。”
        “哈哈,就業、結婚、生子,人生三大事,你這第一大事的第一次是夠鬱悶的。”大鳥同情道,“不過現實社會就是這樣的,他們又不認識你,不給你面子,也是很正常的。就象現在曹啓泰主持的電視《上班這點事》節目,當中可聊可學之事還真不少,上班可不是上學,複雜着呢。罷了罷了,誰叫你運氣不好,你的主管在公司,事情就會好辦多了。”
       “不過,這家公司讓你感覺不好原因在於管理上存在一些問題。”大鳥接着說,“這倒是讓我想起來我們設計模式的一個原則,你的這個經歷完全可以體現這個原則觀點。”
       “哦,是什麼原則?”小菜情緒被調動了起來,“你怎麼什麼事都可以和軟件設計模式搭界呢?”
       “,大鳥我顯然不是吹出來的……”大鳥洋洋得意道。
       “嘖嘖,行了行了,大鳥你強!!!不是吹的,是天生的!,快點說說,什麼原則?”小菜對大鳥的吹鳥腔調頗爲不滿,希望快些進入正題。
       “你到了公司,通過人事部小楊,認識了IT部小張,這時,你已認識了兩個人。但因沒人介紹你並不認識IT部小李。而既然小張小李都屬於IT部,本應該都可以給你裝系統配帳號的,但卻因小張有事,而你又不認識小李,而造成你的人生第一次大大損失,你說我分析得對吧?”
       “你這都是廢話,都是我告訴你的事情,哪有什麼分析。”小菜失望道。
       “如果你同時認識小張和小李,那麼任何一人有空都可以幫你搞定了,你說對吧?”
       “還是廢話。”
       “這就說明,你如果把人際關係搞好,所謂‘無熟人難辦事’,你在IT部‘有人’,不就萬事不愁了嗎?”大鳥一臉壞笑
       “大鳥,你到底想說什麼?我要是有關係,對公司所有人都熟悉,還用得着你說呀。”
       “小菜,瞧你急的,其實我想說的是,如果IT部有一個主管,負責分配任務,不管任何需要IT部配合的工作都讓主管安排,不就沒有問題了嗎?”大鳥開始正經起來。
       “你的意思是說,如果小楊找到的是IT的主管,那麼就算小張沒空,還可以通過主管安排小李去做,是嗎?”
       “對頭(四川方言發音)。”大鳥笑着鼓勵道。
       “我明白了,關鍵在於公司裏可能沒有IT主管,他們都是找到誰,就請誰去工作,如果都熟悉,有事可以協調着辦,如果不熟悉,那麼就會出現我碰到的情況了,有人忙死,有人空着,而我在等待。”
       “沒有管理,單人情協調也很難辦成事的。如果公司IT部就一個小張,那什麼問題也沒有,只不過效率低些。後來再來個小李,那工作是叫誰去做呢?外人又不知道他們兩人誰忙誰閒的,於是抱怨、推諉、批評就隨風而至。要是三個人在IT部還沒有管理人員,則更加麻煩了。正所謂一個和尚挑水喫,兩個和尚擡水喫,三個和尚沒水喫。”
       “看來哪怕兩個人,也應該有管理纔好。我知道你的意思了,不過這是管理問題,和設計模式有關係嗎?”
       “急什麼,還沒講完呢?就算有IT主管,如果主管正好不在辦公室怎麼辦呢?公司幾十號人用電腦,時時刻刻都有可能出故障,電話過來找主管,人不在,難道就不解決問題了?”
       “這個,看來需要規章制度,不管主管在不在,誰有空先去處理,過後彙報給主管,再來進行工作協調。”小菜也學着分析起來。
       “是呀,就像有人在路上被車撞了,送到醫院,難道還要問清楚有沒有錢纔給治療嗎,‘人命大於天’,同樣的,在軟件公司,‘電腦命大於天’,開發人員工資平均算下來每天按數百記的,耽誤一天半天,實在是公司的大損失呀——所以你想過應該怎麼辦?”
       “我覺得,不管認不認識IT部的人,我只要電話或親自找到IT部,他們都應該想辦法幫我解決問題。”
       “好,說得沒錯,那你打電話時,怎麼說呢?是說‘經理在嗎?……小張在嗎?……’,還是‘IT部是吧,我是小菜,電腦已壞,再不修理,軟件歇菜。’”
      “,當然是軟件歇菜來得更好!你這傢伙,就拿我開心!”
      “這樣子的話,公司不管任何人,找IT部就可以了,不管認識不認識人,反正他們會想辦法找人來解決。”
      “哦,我明白了,我真的明白了。你的意思是說,IT部代表是抽象類或接口,小張小李代表是具體類,之前你在分析會修電腦不會修收音機裏講的依賴倒置原則,即面向接口編程,不要面向實現編程就是這個意思?”小菜,興奮異常。
      “當然,這個原則也是滿足的,不過我今天想講的是另一個原則:‘迪米特法則(LoD)’ 也叫最少知識原則,簡單的說,就是如果兩個類不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用。如果其中一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。其實道理就是你今天碰到的這個例子,你第一天去公司,怎麼會認識IT部的人呢,如果公司有很好的管理,那麼應該是人事的小楊打個電話到IT部,告訴主管安排人給小菜你裝電腦,就算開始是小張負責,他臨時有急事,主管也可以再安排小李來處理,如果小李當時不忙的話。其實迪米特法則還是在講如何減少耦合的問題,類之間的耦合越弱,越有利於複用,一個處在弱耦合的類被修改,不會對有關係的類造成波及。也就是說,信息的隱藏促進了軟件的複用。”
        “明白,由於IT部是抽象的,哪怕裏面的人都離職換了新人,我的電腦出問題也還是可以找IT部解決,而不需要認識其中的同事,純靠關係幫忙了。就算需要認識,我也只要認識IT部的主管就可以了,由他來安排工作。”
       “小菜動機不純嗎!你不會是希望小李快些被炒魷魚吧?哈!”大鳥瞧着小菜笑道
       “去!!!我是那樣的人嗎?好了,你昨天說過,要我改商場收銀代碼爲三層架構,有些麻煩的。我得想想。”

(待續)
注:有回覆說到《小菜編程成長記》系列講問題不透,其實這是正常的,畢竟這不是上課,而是在寫對話,聊天而已,建議看文章後若有學習的想法再去搜索相關主題研究,千萬不能認爲看了小菜系列就可以學懂設計模式。伍迷更希望是在你工作學習辛苦這餘,看看《小菜》系列,調劑一下笑笑而已。另:本文迪米特法則知識來自《Java與模式》,一本國人寫的難得的好書,給出“購買”評級。

大鳥說道:“實際上沒有學過設計模式去理解三層架構會有失偏頗的,畢竟分層是更高一級別的模式,所謂的架構模式。不過在程序中,有意識的遵循設計原則,卻也可以有效的做出好的設計。”
      “不要告訴我,剛纔講的‘迪米特法則’就會在分層中用得上?”小菜說。
     “當然用得上,否則講它幹嗎,你當我是在安慰你而臨時編個法則來騙騙你呀?來,再來看看你上次寫的代碼。”

 1        private void Form1_Load(object sender, EventArgs e)
 2        {
 3            //讀配置文件
 4            ds = new DataSet();
 5            ds.ReadXml(Application.StartupPath + "//CashAcceptType.xml");
 6            //將讀取到的記錄綁定到下拉列表框中
 7            foreach (DataRowView dr in ds.Tables[0].DefaultView)
 8            {
 9                cbxType.Items.Add(dr["name"].ToString());
10            }

11            cbxType.SelectedIndex = 0;
12        }

13

       “這是Form_Load的代碼,裏面有沒有什麼與界面無關的東西?”大鳥問道。
       “第4、5行是讀配置文件的代碼,它應該屬於DAL層。對吧?”
      “很好,再看下面的這段,裏面又有哪些呢?”

 1        private void btnOk_Click(object sender, EventArgs e)
 2        {
 3            CashContext cc = new CashContext();
 4            //根據用戶的選項,查詢用戶選擇項的相關行
 5            DataRow dr = ((DataRow[])ds.Tables[0].Select("name='" + cbxType.SelectedItem.ToString()+"'"))[0];
 6            //聲明一個參數的對象數組
 7            object[] args =null;
 8            //若有參數,則將其分割成字符串數組,用於實例化時所用的參數
 9            if (dr["para"].ToString() != "")
10                args = dr["para"].ToString().Split(',');
11            //通過反射實例化出相應的算法對象
12            cc.setBehavior((CashSuper)Assembly.Load("商場管理軟件").CreateInstance("商場管理軟件." + dr["class"].ToString(), 
13                                false, BindingFlags.Default, null, args, nullnull));
14            
15            double totalPrices = 0d;
16            totalPrices = cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
17            total = total + totalPrices;
18            lbxList.Items.Add("單價:" + txtPrice.Text + " 數量:" + txtNum.Text + " "+cbxType.SelectedItem+ " 合計:" + totalPrices.ToString());
19            lblResult.Text = total.ToString();
20        }

     “這裏3-13行,是爲確定哪種算法而創建CashContext對象,其中用到了反射技術,爲計算做準備。第16行是真正的計算打折價或返利,17-19是界面顯示的部分。所以應該把3-16行都搬到BLL層去。不過,我還有些疑問,這樣做會讓配置文件的數據要先從DAL轉到BLL,再轉到表示層,多麻煩呀,什麼不直接表示層讀DAL,它想要數據就去讀DAL,它想算結果就去請求BLL處理?”
     “那是說明你沒有真的瞭解什麼叫迪米特法則,象你那樣說,不就等於,你小菜又要認識小張,又要認識小李了,這不就耦合過度嗎?本來你只需要認識一個人就可以了,這樣依賴纔會小呀!”
     “可是我就得在BLL裏寫一個專門返回從DAL裏得到數據的方法,這個方法不屬於現在的任何類,我就還得再寫一個類來做這種傳聲筒的角色。而且由於界面還要涉及到其它的類,如CashContext,感覺UI和BLL耦合還是很高。”
     “說得沒錯,你的確是講到點子上了,由於表示層UI需要與BLL有兩個類進行交互,這是很麻煩,不過前輩們就想了了一個較好的辦法,另一個設計模式,‘門面模式’(Facade)或叫外觀模式”

(以下源自呂震宇 博客)
門面模式要求一個子系統的外部與其內部的通信必須通過一個統一的門面(Facade)對象進行。門面模式提供一個高層次的接口,使得子系統更易於使用。 

門面模式的結構

門面模式是對象的結構模式。門面模式沒有一個一般化的類圖描述,下圖演示了一個門面模式的示意性對象圖:

 

在這個對象圖中,出現了兩個角色:

門面(Facade)角色:客戶端可以調用這個角色的方法。此角色知曉相關的(一個或者多個)子系統的功能和責任。在正常情況下,本角色會將所有從客戶端發來的請求委派到相應的子系統去。

子系統(subsystem)角色:可以同時有一個或者多個子系統。每一個子系統都不是一個單獨的類,而是一個類的集合。每一個子系統都可以被客戶端直接調用,或者被門面角色調用。子系統並不知道門面的存在,對於子系統而言,門面僅僅是另外一個客戶端而已。

       “哦,你這樣一講,我就明白了。”小菜說,“上篇所講的IT部,其實可以由部門主管就是門面,我們只需要找到部門主管,就可以通過他安排相關的人來提供服務,我們不需要了解IT部的具體情況了。”
       “其實現實中這樣的例子很多。比如以前上海市沒有新聞發言人,當要到春運時,所有的記者都跑到交通部去了解信息,當有非典或禽流感時,所有的記者又跑到衛生部去打聽情況,突然這時候樓市大跌,記者們又得馬不停蹄前往建設部收集新聞。辛苦呀,有什麼辦法呢,喫這口飯的。但其實辛苦地又何止只是記者。各個政府部門都需要專人來應付這些記者,不能多說話,不能說錯話,但也不能不說話。也辛苦呀,誰叫他們是政府呢。”大鳥彷彿自己感同身受似的描述着,“於是,新聞發言人橫空出世,一位知識女性焦揚,代表上海市政府發言,從此,老記們不需要頭頂驕陽奔跑於各大政府部門之間,只需要天天等在新聞發言廳門口守着就可以寫出準確及時的新聞。而政府部門也不用專人來應付老記們的圍追堵截,有更多的時間爲人民做實事辦好事。這裏就只辛苦一個人。”
      “那一定是新聞發言人自己了,因爲她需要先與政府部門溝通好,要說些什麼、如何說、如何回答刁鑽問題。然後要站在鎂光燈下承受壓力接受記者的訪問。不過,幹這一行就是需要辛苦的,這是政府的門面呀。”小菜感慨到。

 

       “好了,去改寫吧,你一定會感受到分層後代碼的漂亮。”大鳥鼓勵道。

         過一小時後,小菜給出商場收銀程序的第六份作業。。

DAL層代碼(目前是讀配置文件,以後可以很容易的修改爲訪問數據庫)

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

namespace 商場管理軟件.DAL
{
    
public class CashAcceptType
    
{
        
public DataSet GetCashAcceptType()
        
{
            
//讀配置文件到DataSet
            DataSet ds = new DataSet();
            ds.ReadXml(
"CashAcceptType.xml");
            
return ds;
        }

    }

}

BLL層主要代碼(Facade類代碼)

namespace 商場管理軟件.BLL
{
    
public class CashFacade
    
{
        
const string ASSEMBLY_NAME = "商場管理軟件.BLL";
        
        
//得到現金收取類型列表,返回字符串數組
        public string[] GetCashAcceptTypeList()
        
{
            CashAcceptType cat 
= new CashAcceptType();
            DataSet ds 
= cat.GetCashAcceptType();
            
int rowCount = ds.Tables[0].DefaultView.Count;
            
string[] arrarResult = new string[rowCount];

            
for (int i = 0; i < rowCount; i++)
            
{
                arrarResult[i] 
= (string)ds.Tables[0].DefaultView[i]["name"];
            }

            
return arrarResult;
        }


        
/// <summary>
        
/// 用於根據商品活動的不同和原價格,計算此商品的實際收費
        
/// </summary>
        
/// <param name="selectValue">下拉列表選擇的折價類型</param>
        
/// <param name="startTotal">原價</param>
        
/// <returns>實際價格</returns>

        public double GetFactTotal(string selectValue, double startTotal)
        
{
            CashAcceptType cat 
= new CashAcceptType();
            DataSet ds 
= cat.GetCashAcceptType();

            CashContext cc 
= new CashContext();
            DataRow dr 
= ((DataRow[])ds.Tables[0].Select("name='" + selectValue + "'"))[0];
            
object[] args = null;
            
if (dr["para"].ToString() != "")
                args 
= dr["para"].ToString().Split(',');

            cc.setBehavior((CashSuper)Assembly.Load(ASSEMBLY_NAME).CreateInstance(ASSEMBLY_NAME 
+ "." 
                                                           + dr["class"].ToString(), false, BindingFlags.Default, null, args, nullnull));
            
return cc.GetResult(startTotal);

        }

    }

}

UI層代碼(可以很容易的轉換爲Web頁面)


        
double total = 0.0d;//用於總計
        CashFacade cf = new CashFacade();
            
        
private void Form1_Load(object sender, EventArgs e)
        
{
            
//讀數據綁定下拉列表
            cbxType.DataSource=cf.GetCashAcceptTypeList();
            
            cbxType.SelectedIndex 
= 0;
        }


        
private void btnOk_Click(object sender, EventArgs e)
        
{
            
double totalPrices = 0d;
            
//傳進下拉選擇值和原價,計算實際收費結果
            totalPrices = cf.GetFactTotal(cbxType.SelectedItem.ToString(), Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
            total 
= total + totalPrices;
            lbxList.Items.Add(
"單價:" + txtPrice.Text + " 數量:" + txtNum.Text + " "+cbxType.SelectedItem+ " 合計:" + totalPrices.ToString());
            lblResult.Text 
= total.ToString();
        }


 

項目文件結構圖

 

       “大鳥,來看看這下怎麼樣,還有沒有可修改的地方?”小菜問道。
       “小菜開始謙虛了嗎!以前不是一直信誓旦旦,現在怎麼,沒信心了?”
       “越學越覺得自己知道的少,感覺代碼重構沒有最好,只有更好呀。”小菜誠心的答道
       “寫得很不錯。BLL層的CashFacade類其實就是新聞發言人,程序的門面;而應用程序或Web其實就類似CCTV和SMG,都是新聞單位,他們不應該也不需要關心門面後面的實現是如何的。現在用了門面模式以後,耦合比以前要少很多了,更改會更加方便,擴展也很容易了。你要是再回過頭來看看最初的代碼和現在的代碼,你會體會更深刻,更加明白重構的魅力。”

 

         大鳥接着說:“之前的代碼,下拉控件的綁定是硬編碼,所以只要改動需求就得改代碼,現在是讀配置文件,大大增加靈活性;之前的代碼是根據用戶選擇,分支判斷執行相應的算法,現在整個算法類全部搬走,做到了業務與界面的分離;之前的代碼由於全寫在form裏,所以要更換成Web方式,即C/S改爲B/S非常困難,要全部重新寫(注意真實的軟件系統不會這麼簡單,所以簡單複製不能解決問題),現在的代碼由於把業務運算分離,所以界面的更改不會影響業務的編寫。還有就是現在的代碼由於DAL與BLL分離,配置文件可以很容易的更換爲數據庫讀取,且不需要影響表示層與業務邏輯層的代碼。總的來講,若是程序不會變化,原有的設計就沒什麼問題,運行結果正確足夠了,但若是程序可能會時常隨業務而變化,新的設計就大大提高了應變性,這其實就是應用設計模式的目的所在。”
       “我現在越來越有信心學好它,設計模式真的很有意思,學它不學它,寫出來的代碼大不一樣。老大,跟你混,看來沒有錯。”
       “嗨,小菜,我不做老大已經很久了!”大鳥仰身長嘆,揚長而去。

(待續)

本文源代碼。其中分四個項目,DAL、BLL、WebUI和WinUI,可設置WebUI和WinUI爲啓動項目,注意由於只是學習源代碼,配置路徑沒有做處理(實際應用需要config文件),WebUI配置文件CashAcceptType.xml在“/商場管理軟件06分層/”根目錄下,而WinUI的配置文件CashAcceptType.xml在“/商場管理軟件06分層/商場管理軟件/bin/Debug/”目錄下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章