小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

設(shè)計(jì),由你掌握

 ljjzlm 2006-08-04

設(shè)計(jì),由你掌握

Filed under:  bruce zhang @ 11:12 am

前言:XP中有個(gè)準(zhǔn)則,就是只做目前你需要做的。例如,我需要加法運(yùn)算,你就沒有必要實(shí)現(xiàn)乘法運(yùn)算,因?yàn)檫@不是客戶需要的。因此,在開發(fā)中,我們可以不去考慮程序?qū)τ谖磥淼臄U(kuò)展性。“簡單最好!”那么,是否就不需要設(shè)計(jì)了呢?至于設(shè)計(jì)模式,是否也可以不去了解了呢?答案至少是否定的。因?yàn)榭蛻舻男枨笫?#8220;與時(shí)俱進(jìn)”的,現(xiàn)在不實(shí)現(xiàn),并不等于今后不實(shí)現(xiàn)。在實(shí)現(xiàn)中,不管是重構(gòu),還是重新設(shè)計(jì),通過應(yīng)用設(shè)計(jì)模式,能令你如虎添翼。關(guān)鍵不在于設(shè)計(jì)模式是否重要,而在于你怎么應(yīng)用它,以及選擇什么樣的時(shí)機(jī)。總而言之,設(shè)計(jì),由你掌握!

一、從需求開始

在我們的項(xiàng)目中,作費(fèi)用結(jié)算的時(shí)候,客戶要求將該過程與結(jié)果寫入到日志文件中。不過他們的要求很善良,只需要知道日志記錄結(jié)算開始與結(jié)束的時(shí)間而已。是的,按照XP的理念,我們只需要做好客戶需要的事情就OK了。既然是這樣,事情就好辦,代碼輕易而舉就實(shí)現(xiàn)了:
public class Fee
{
 //結(jié)算程序?qū)⒄{(diào)用數(shù)據(jù)層的相關(guān)方法,訪問數(shù)據(jù)庫;
 //為簡單起見,我用累加數(shù)取代;
 public double SettleFee(double money,int records)
 {
  double result = 0.0;
  //我用控制臺輸出來表示寫日志;
  Console.WriteLine(”Start settling fee at {0}”,DateTime.Now);
  for (int i=0;i   {
   result+=money;
  }
  Console.WriteLine(”Settling fee finished at {0}”,DateTime.Now);
  return result;
 }
}
寫好這個(gè),還差點(diǎn)什么?不錯(cuò),我們還需要為Fee類撰寫相應(yīng)的測試代碼,做好單元測試。可能對于傳統(tǒng)的程序員來說,更喜歡的是在編碼完成后,再根據(jù)測試計(jì)劃編寫測試樣例,最后測試。但敏捷開發(fā)的要求卻是測試先行,單元測試是必不可少的環(huán)節(jié)。不過,我認(rèn)為單元測試畢竟只是一種手段。我們在實(shí)際的項(xiàng)目開發(fā)中,對于單元測試不可拘泥教科書的要求,按部就班地一步一步進(jìn)行;而應(yīng)該根據(jù)實(shí)際情況,比如開發(fā)者對語言的掌握程度,對設(shè)計(jì)的理解等等,來決定你單元測試的步驟,乃至于重構(gòu)的步伐。
[TestFixture]
public class TestFee
{
 [Test]
 pubic void Settle()
 {
  Fee fee = new Fee();
  Assert.IsNotNull(fee);
  Assert.AreEqual(6,fee.SettleFee(2.0,3));
 }
}
在NUnit中打開這個(gè)測試類,并運(yùn)行。毫無疑義,你會(huì)看到測試的綠燈全部都亮了。你可以在NUnit中看看控制臺輸出的結(jié)果。自然你也可以在AreEqual()方法中,故意將預(yù)期的值設(shè)置錯(cuò)誤,來看看運(yùn)行NUnit是什么情況,以及出現(xiàn)的錯(cuò)誤提示信息。不過,這些都不是本文關(guān)注的重點(diǎn)。

二、當(dāng)需求改變了
在XP中,客戶的重要是舉足輕重的。在客戶提出需求的時(shí)候,你需要和他盡可能地溝通,并保證意見最后要達(dá)成一致。然而客戶對產(chǎn)品的理解可能有時(shí)候會(huì)出現(xiàn)偏差,也許有時(shí)候?qū)Ψ降囊笠矔?huì)隨著產(chǎn)品的應(yīng)用而逐漸發(fā)生改變。所幸的是,這一次需求的改變,發(fā)生在項(xiàng)目開發(fā)過程中,且是在你和他結(jié)對交流的時(shí)候,最終發(fā)現(xiàn)的缺陷。因?yàn)榭蛻粽J(rèn)為,這個(gè)日志過于簡單了,并不利于今后對產(chǎn)品的維護(hù)。我得承認(rèn)這是一個(gè)好的要求。
事實(shí)上,日志記錄得越詳細(xì),對于開發(fā)人員自己也是有好處的。最后,我們決定,日志不僅僅要記載結(jié)算的起止時(shí)間,還應(yīng)該記載可能會(huì)出現(xiàn)的錯(cuò)誤信息,最好還能記載這個(gè)結(jié)算的過程代碼,比如我們執(zhí)行的是哪一個(gè)存儲(chǔ)過程,讀取了哪些表的數(shù)據(jù),包括這些表的字段。

然而有個(gè)不利的因素是,這個(gè)費(fèi)用結(jié)算的過程可能會(huì)很頻繁的使用。如果寫入的日志太復(fù)雜了,會(huì)否影響產(chǎn)品的性能?而且頻繁寫日志的話,日志文件會(huì)不會(huì)越來越大?如果我們這個(gè)產(chǎn)品已經(jīng)非常健壯,還有必要去記載這些信息嗎?畢竟有很多信息,對于普通用戶而言,并沒有實(shí)際用處,反而干擾了他有效獲取日志的有用信息。

所以后來我們想到一個(gè)方法,就是將日志進(jìn)行分級,從最簡單到最詳盡。用戶在進(jìn)行費(fèi)用結(jié)算的時(shí)候,可以根據(jù)自身需要,選擇日志的級別。無疑,這是一個(gè)令人滿意的策略。

三、如果不熟悉設(shè)計(jì)模式

假設(shè)我們的開發(fā)人員對于設(shè)計(jì)模式一概不知,經(jīng)過分析客戶的需求,他會(huì)直接了當(dāng)?shù)淖龀鋈绱说慕鉀Q方案。首先定義三種級別的日志:SimplestLog,NormalLog,DetailedLog。SimplestLog只記錄結(jié)算的起止時(shí)間和耗費(fèi)的時(shí)間,同時(shí)還要記錄結(jié)算后的結(jié)果。NormalLog則除此之外,還要記錄可能會(huì)出現(xiàn)的錯(cuò)誤信息。而DetailedLog最詳盡,它不僅包含了NormalLog記錄的信息,還包括記錄結(jié)算的實(shí)現(xiàn)方法,如用到的存儲(chǔ)過程,數(shù)據(jù)表和相應(yīng)的字段。

我們最初設(shè)想為這三種級別建立三個(gè)不同的私有方法,然后在SettleFee()方法中,引入一個(gè)日志級別參數(shù),然后根據(jù)日志級別的值,決定調(diào)用哪一個(gè)私有方法,例如:
private void WriteSimplestLog();
private void WriteNormalLog();
private void writeDetailedLog();

public enum LogLevelEnum{Simple=0,Normal,Detail};

public double SettleFee(double money,int records,LogLevelEnum logLevel)
{
 switch (logLevel)
 {
  case LogLevel.Simple:
   WriteSimplestLog();
   break;
  case LogLevel.Normal:
   WriteNormalLog();
   break;
  case LogLevel.Detail:
   writeDetailedLog();
   break;    
 }
 for (int i=0;i  {
  result+=money;
 }
 
}

不用說,我們的程序員遇到麻煩了。因?yàn)樵谟涗浫罩拘畔⒌臅r(shí)候,可能會(huì)在結(jié)算的前后來進(jìn)行。也就是說,結(jié)算的那一段代碼必須放到記錄日志的方法中,才可以實(shí)現(xiàn)。幸運(yùn)的是,我們的程序員應(yīng)該還具備重構(gòu)的知識,他決定把結(jié)算的那一段代碼專門抽取出來,形成一個(gè)單獨(dú)的方法,再放到日志方法中調(diào)用。“Extract Method”,不是嗎?很聰明的做法。

好吧,我們來實(shí)現(xiàn)它吧,看看會(huì)是怎樣?
首先,實(shí)現(xiàn)專門的結(jié)算方法:
private double Settle(double money,int records)
{
 double result = 0.0;
 for (int i=0;i  {
  result+=money;
 }
 return result;
}
再來實(shí)現(xiàn)日志方法:
private void WriteSimplestLog()
{
 DateTime startTime,endTime;
 startTime = DateTime.Now;
 Console.WriteLine(”Start settling fee at {0}”,startTime);
 result = Settle(money,records);
 endTime = DateTime.Now;
 Console.WriteLine(”Settling fee finished at {0}”,endTime);
 TimeSpan wasted = endTime - startTime;
 Console.WriteLine(”It wasted time {0}”,wasted);
 Console.WriteLine(”The result is {0}”,result);
}
在這個(gè)方法中,result是Fee類中的一個(gè)私有變量,用來保存結(jié)算后的結(jié)果。假設(shè)不使用這個(gè)變量,而是在方法中引入局部變量,那么WritesimplestLog()方法就必須返回double類型了,這個(gè)設(shè)計(jì)可夠糟糕的!同樣的,money和records也應(yīng)該是通過私有變量傳遞值,否則這個(gè)日志方法就必須帶上這兩個(gè)參數(shù)了。

接著實(shí)現(xiàn)下面兩個(gè)方法。我們已經(jīng)注意到根據(jù)日志級別的不同,最詳盡的日志內(nèi)容總是包含了其低一級日志的內(nèi)容。并且,后兩級日志沒有包括性能的記錄,因此記錄的日志并未要求必須出現(xiàn)在結(jié)算方法的前后。
private void WriteNormalLog()
{
 try
 {
  WriteSimplestLog();
  Console.WriteLine(”Settling operation succeed!”);
 } 
 catch (Exception ex)
 {
  Console.WriteLine(”The error occured while settling the fee.”);
  Console.WriteLine(”The error is ” + ex.Message);
 }
}

private void WriteDetailedLog()
{
 WriteNormalLog();
 Console.WriteLine(”The StoreProcedure whick was invoked is SpSettleFee.”);  
 Console.WriteLine(”Data table is: UserFee, OnLineRecord.”);
}
剩下的代碼就簡單了:
private double result = 0.0;
private doulbe money = 0.0;
private int record = 0;

public double SettleFee(double money,int records,LogLevelEnum logLevel)
{
 this.money = money;
 this.record = record;
 switch (logLevel)
 {
  case LogLevel.Simple:
   WriteSimplestLog();
   break;
  case LogLevel.Normal:
   WriteNormalLog();
   break;
  case LogLevel.Detail:
   writeDetailedLog();
   break;    
 }
 return result; 
}

嘿嘿,看起來還不錯(cuò)!
當(dāng)然,與之相應(yīng)的測試代碼也要發(fā)生改變:
[Test]
pubic void Settle()
{
 Fee fee = new Fee();
 Assert.IsNotNull(fee);
 Assert.AreEqual(6,fee.SettleFee(2.0,3,LogLevelEnum.Simple));
 Assert.AreEqual(6,fee.SettleFee(2.0,3,LogLevelEnum.Normal));
 Assert.AreEqual(6,fee.SettleFee(2.0,3,LogLevelEnum.Detail));
}

四、問題又出現(xiàn)了

我們程序員都應(yīng)該有這個(gè)信仰,就是:簡單最好。我不喜歡那些常常賣弄自己水平的人,僅僅為了一個(gè)簡單的要求,卻故意把代碼弄得非常復(fù)雜,以為賣弄高深就是學(xué)問。其實(shí)不然。我傾向于簡單的算法,即使它的性能稍差。因?yàn)樾阅懿?,我們還可以通過提升硬件等多種方式來解決;而如果整個(gè)項(xiàng)目都充斥著難于理解的算法,想一想,如果寫代碼的“牛人”走了,而目前又需要對程序做改進(jìn)。那么,項(xiàng)目的后任者,在deadline的壓力下,面對這一大堆“高深”的算法,會(huì)是怎樣的抓狂???

所以,從目前來看,以上實(shí)現(xiàn)的代碼并沒有什么不合適的地方。簡單易懂,也完成了客戶的需求。不過,很多時(shí)候事情并非盡如人意??蛻舻男枨髸?huì)隨著對項(xiàng)目的跟進(jìn),而逐漸發(fā)生改變。

一周后,我們的客戶提出了新的要求。首先,他希望這個(gè)日志功能,能夠展現(xiàn)它更靈活的一面。不是死板的從最簡單到最詳盡,而是根據(jù)日志記載的內(nèi)容,任意靈活的組合。原來的日志層次如圖一:


圖一:最簡單的日志組合

而根據(jù)現(xiàn)在的需求,可能會(huì)有多種組合:


圖二:靈活的組合,日志變得多種多樣

我們算一算,如果按照前面的方式來實(shí)現(xiàn)新的需求,可能會(huì)寫出多少個(gè)方法?此時(shí)的你應(yīng)該怎樣辦呢?

或許應(yīng)該將邏輯抽象出來,為日志建立一個(gè)基類;然后根據(jù)實(shí)際的需求派生不同的子類。在調(diào)用的時(shí)候,可以通過多態(tài)的方式,決定調(diào)用何種具體的日志對象方法。

但是問題接踵而至。首先是寫日志方法和費(fèi)用結(jié)算方法如何結(jié)合起來?尤其是A類日志,寫日志的時(shí)候必須是在費(fèi)用結(jié)算的前后,以確定結(jié)算的起止時(shí)間。也許,我們可以考慮將該方法分離為兩個(gè)方法BeforeWriteLog()和AfterWriteLog()。那么與之對應(yīng)的,凡是和A類日志有關(guān)的其他日志,也必須實(shí)現(xiàn)這種分離策略。再想想繼承的子類,根據(jù)日志這種靈活的組合,我們需要?jiǎng)?chuàng)建多少個(gè)子類對象。一旦需求再增加呢?這個(gè)無底洞我是不愿意去跳的。

而客戶的要求并沒有結(jié)束。他還需要在與費(fèi)用有關(guān)的其他方法中,也實(shí)現(xiàn)日志的功能。例如費(fèi)用查詢。由于之前的策略是在日志方法中包含費(fèi)用結(jié)算的功能。如果需要記錄費(fèi)用查詢的日志,豈不是要為它再建立不同的日志方法?

總之,如果你不了解設(shè)計(jì)模式的話,你可能會(huì)非常棘手。你會(huì)用盡所有已掌握的知識,通過你對OOP的理解,使用接口,繼承和聚合,最后你可能會(huì)發(fā)現(xiàn)你碰壁了。樂觀的是,你最終解決了問題??墒强纯唇鉀Q方案呢?要么拙劣不忍目睹,要么幸運(yùn)的是你采用了正確的策略。你已經(jīng)達(dá)到了GOF的水平,自己創(chuàng)造出我們應(yīng)該正確使用的模式了。不過,可惜的是,你會(huì)沮喪地聽到,有人會(huì)告訴你,你采用的其實(shí)就是設(shè)計(jì)模式中的Decorator模式。與其這樣,為什么不好好地學(xué)習(xí)一下設(shè)計(jì)模式呢?

五、結(jié)果完全不一樣了

如果你已經(jīng)熟悉了設(shè)計(jì)模式的話,面對客戶提出的新的需求,你就會(huì)很快獲得完美的解決方案了。

分析一下。雖然日志的級別會(huì)非常之多,但其根本的功能卻是一致的,那就是所有的日志信息都是費(fèi)用結(jié)算(自然也包括費(fèi)用查詢)的包裝而已。形象地說,日志就好比油漆工人,而費(fèi)用結(jié)算就是一間房子,需要油漆工人來給墻壁粉刷上美麗的色彩。如此而已。

修改后的結(jié)構(gòu)類圖如下:

此時(shí),我將原來的日志方法修改為單獨(dú)的類對象。同時(shí)將日志由原來分級(Simplest, Normal, Detailed)的方式,改為按各自的功能劃分,然后建立日志對象(BasicLogDecorator,ErrorLogDecorator,ImplLogDecorator)。具體的修改思路和步驟,如第六部分描述。

在使用Decorator模式來實(shí)現(xiàn)如上需求之前,我想表明自己的態(tài)度:
1、 設(shè)計(jì)模式的重要性已經(jīng)不言而喻了;
2、 不要為了模式而去學(xué)習(xí)模式,設(shè)計(jì)模式必須和項(xiàng)目實(shí)際開發(fā)結(jié)合;
3、 如果目前的需求很簡單,不用設(shè)計(jì)模式并不是一個(gè)壞的選擇;
4、 因?yàn)槲覀冇兄貥?gòu);
5、 但必須記住,重構(gòu)的每一步,需要以單元測試來保證;
6、 你必須深入理解設(shè)計(jì)模式,否則當(dāng)需求復(fù)雜之后,你會(huì)束手無策;
7、 設(shè)計(jì)模式是人創(chuàng)造出來的,但既然已經(jīng)有了前人的成果,為什么不用?

寫到這里,諸位已經(jīng)可以結(jié)束本文的閱讀了。不過我還得繼續(xù)下去,作業(yè)沒有做完,不能交卷。

六、大結(jié)局

因?yàn)楝F(xiàn)在的需求比較復(fù)雜了,所以你在重構(gòu)每一步時(shí),必須小心翼翼。別忘了單元測試,有了它,才可以保證你的正確無誤。

首先,利用“Extract Interface”原則,為裝飾的對象Fee類Extract一個(gè)接口,并讓Fee類實(shí)現(xiàn)它:
public interface IFee
{
double SettleFee(double money, int records);
}
public class Fee:IFee {}
當(dāng)然,我們需要把SettleFee()方法恢復(fù)成原來的模樣。記住這個(gè)過程仍然需要小心翼翼。因?yàn)椋趯?shí)現(xiàn)這一步時(shí),可能已經(jīng)離最初的簡單實(shí)現(xiàn)已經(jīng)有一周的時(shí)間了。所以,再恢復(fù)原樣的過程中,我希望仍然不要放棄使用單元測試。當(dāng)然在這里,我為了行文簡潔,省略了這些過程。

修改測試代碼,然后在NUnit中運(yùn)行它:
[Test]
pubic void Settle()
{
 IFee fee = new Fee();
 Assert.IsNotNull(fee);
 Assert.AreEqual(6,fee.SettleFee(2.0,3));
}

現(xiàn)在來分析日志。根據(jù)對前面A類、B類、C類日志的分析。我們不再從是否詳盡的角度來分類日志,而是從日志的內(nèi)容或者說日志實(shí)現(xiàn)的功能來分類。我們可以將日志分為基本日志類、錯(cuò)誤日志類、實(shí)現(xiàn)日志類三種。

基本日志類:實(shí)現(xiàn)日志的基本功能,包括費(fèi)用結(jié)算的耗時(shí)和結(jié)算后的結(jié)果。
錯(cuò)誤日志類:記錄可能會(huì)出現(xiàn)的錯(cuò)誤消息。
實(shí)現(xiàn)日志類:將費(fèi)用結(jié)算的具體實(shí)現(xiàn)記錄下來,便于以后對于產(chǎn)品的維護(hù)。

因?yàn)槿罩揪褪荄ecorator模式的油漆工,它們都需要具備包裝費(fèi)用結(jié)算的功能,我為它們定義了一個(gè)共同的抽象類:
public abstract class LogDecorator
{
 privated IFee decoratee;
 public LogDecorator(IFee decoratee)
 {
  this.decoratee = decoratee;
 }
 public IFee Decoratee
{
  get {return this.decoratee;}
}
}
基本日志類、錯(cuò)誤日志類、實(shí)現(xiàn)日志類都繼承該抽象類。注意抽象類的自定義構(gòu)造函數(shù),它是實(shí)現(xiàn)裝飾功能的關(guān)鍵。該構(gòu)造函數(shù)負(fù)責(zé)傳遞一個(gè)被裝飾的對象進(jìn)來,并賦給屬性Decoratee。這個(gè)初始化的過程,就好比刷油漆的刷子,對于所有油漆工人來說,都應(yīng)該是一樣的,只是他們刷的油漆不同而已。

要裝飾Fee類,僅僅依靠構(gòu)造函數(shù)來傳遞被裝飾對象還不夠。我們還需要把原來Fee類所做的工作,轉(zhuǎn)移到裝飾類中,如此才能完成裝飾的功用。所以,這三個(gè)日志類,不僅要繼承LogDecorator類,還需要實(shí)現(xiàn)IFee接口,即與Fee類實(shí)現(xiàn)同一個(gè)接口。
首先是基本日志類:
public class BasicLogDecorator:LogDecorator,IFee
{
 public BasicLogDecorator(IFee decoratee):base(decoratee) {}

 public double SettleFee(double money,int records)
 {
  DateTime startTime,endTime;
  startTime = DateTime.Now;
  Console.WriteLine(”Start settling fee at {0}”,startTime);
  double result = Decoratee.SettleFee(money,records);
  endTime = DateTime.Now;
  Console.WriteLine(”Settling fee finished at {0}”,endTime);
  TimeSpan wasted = endTime - startTime;
  Console.WriteLine(”It wasted time {0}”,wasted);
  Console.WriteLine(”The result is {0}”,result);
  return result;
 }
}

做到這一步時(shí),先別急著去實(shí)現(xiàn)另外兩個(gè)類。我們應(yīng)該先做單元測試。修改單元測試代碼:
[Test]
pubic void SettleBasicLog()
{
 IFee fee = new Fee();
 IFee basicLogFee = new BasicLogDecorator(fee);
 Assert.IsNotNull(fee);
 Assert.IsNotNull(basicLogFee);
 Assert.AreEqual(6,fee.SettleFee(2.0,3));
 Assert.AreEqual(6, basicLogFee.SettleFee(2.0,3));
}

不過這個(gè)單元測試代碼似乎有點(diǎn)亂,我們應(yīng)該根據(jù)具體的實(shí)現(xiàn),對測試方法分類,同時(shí)將類對象的初始化放到[SetUp]中。因此,新的測試代碼如下:
[TestFixture]
public class TestFee
{
 [TestFixture]
 public class TestFee
 {
  private IFee fee; 
  [SetUp]
  public void Init()
  {
   fee = new Fee();   
  }

  [Test]
  [Category(”SettleWithoutLog”)]
  public void Settle()
  {  
   Assert.IsNotNull(fee);
   Assert.AreEqual(6,fee.SettleFee(2.0,3));
  }
 
  [Test]
  [Category(”SettleWithBasicLog”)]
  public void SettleBasicLog()
  {  
   IFee basicLogFee = new BasicLogDecorator(fee);
   Assert.IsNotNull(basicLogFee);
   Assert.AreEqual(6,basicLogFee.SettleFee(2.0,3));
  }
[TearDown]
  public void Dispose()
  {
   /*—*/
  }
 }
}

通過Category對測試方法進(jìn)行分類(也可以對測試類進(jìn)行分類)。這樣,我們就可以在Nunit中,根據(jù)測試的情況,選擇要測試的分類,或Exclude(排除)不測試的分類。我們運(yùn)行一下,看NUnit的綠燈是否都亮了?測試通過后,就可以接著實(shí)現(xiàn)另外的日志類了。
public class ErrorLogDecorator:LogDecorator,IFee
{
 public ErrorLogDecorator(IFee decoratee):base(decoratee){}
 public double SettleFee(double money, int records)
 {   
  try
  {
   double result = Decoratee.SettleFee(money,records);
   Console.WriteLine(”Settling operation succeed!”);
   return result;
  } 
  catch (Exception ex)
  {
   Console.WriteLine(”The error occured while settling the fee.”);
   Console.WriteLine(”The error is ” + ex.Message);
   return 0;
  }
 }
}

public class ImplLogDecorator:LogDecorator,IFee
{
 public ImplLogDecorator(IFee decoratee):base(decoratee)
 {}
 public double SettleFee(double money, int records)
 {
  double result = Decoratee.SettleFee(money,records);
  Console.WriteLine(”The StoreProcedure whick was invoked is SpSettleFee.”); 
  Console.WriteLine(”Data table is: UserFee, OnLineRecord.”);
  return result;
 }
}
當(dāng)然每做一步改進(jìn)后,都需要修改測試代碼進(jìn)行單元測試。最后的單元測試代碼:
using System;
using NUnit.Framework;
using FeeManagement;

namespace UnitTest

 [TestFixture]
 public class TestFee
 {
  private IFee fee;
 
  [SetUp]
  public void Init()
  {
   fee = new Fee();   
  }

  [Test]
  [Category(”SettleWithoutLog”)]
  public void Settle()
  {  
   Assert.IsNotNull(fee);
   Assert.AreEqual(6,fee.SettleFee(2.0,3));
  }
 
  [Test]
  [Category(”SettleWithBasicLog”)]
  public void SettleBasicLog()
  {  
   IFee basicLogFee = new BasicLogDecorator(fee);
   Assert.IsNotNull(basicLogFee);
   Assert.AreEqual(6,basicLogFee.SettleFee(2.0,3));
  }

  [Test]
  [Category(”SettleWithErrorLog”)]
  public void SettleErrorLog()
  { 
   IFee errorLogFee = new ErrorLogDecorator(fee);
   Assert.IsNotNull(errorLogFee);
   Assert.AreEqual(6,errorLogFee.SettleFee(2.0,3));
  }

  [Test]
  [Category(”SettleWithImplLog”)]
  public void SettleImplLog()
  { 
   IFee implLogFee = new ImplLogDecorator(fee);
   Assert.IsNotNull(implLogFee);
   Assert.AreEqual(6,implLogFee.SettleFee(2.0,3));
  }

  [Test]
  [Category(”SettleWithBasic&ErrorLog”)]
  public void SettleBasicErrorLog()
  { 
   IFee basicLogFee = new BasicLogDecorator(fee);
   IFee errorLogFee = new ErrorLogDecorator(basicLogFee);
   Assert.IsNotNull(basicLogFee);
   Assert.IsNotNull(errorLogFee);
   Assert.AreEqual(6,errorLogFee.SettleFee(2.0,3));
  }

  [Test]
  [Category(”SettleWithBasic&ImplLog”)]
  public void SettleBasicImplLog()
  { 
   IFee basicLogFee = new BasicLogDecorator(fee);
   IFee implLogFee = new ImplLogDecorator(basicLogFee);
   Assert.IsNotNull(basicLogFee);
   Assert.IsNotNull(implLogFee);
   Assert.AreEqual(6,implLogFee.SettleFee(2.0,3));
  }

  [Test]
  [Category(”SettleWithAllLog”)]
  public void SettleAllLog()
  { 
   IFee basicLogFee = new BasicLogDecorator(fee);   
   IFee implLogFee = new ImplLogDecorator(basicLogFee);
   IFee errorLogFee = new ErrorLogDecorator(implLogFee);
   Assert.IsNotNull(basicLogFee);   
   Assert.IsNotNull(implLogFee);
   Assert.IsNotNull(errorLogFee);
   Assert.AreEqual(6, errorLogFee.SettleFee(2.0,3));
  }

  [TearDown]
  public void Dispose()
  {
   /*—*/
  }
 }
}

由于每一步都嚴(yán)格進(jìn)行了單元測試,所以,我們對代碼的正確性充滿了信心。這也是單元測試的重要性及必要性所在。

七、真的結(jié)束了嗎

從測試代碼中看出,目前的解決方案還存在一個(gè)問題,就是日志對象的創(chuàng)建。由于日志對象可能會(huì)根據(jù)不同的情況,組合成不同的對象。如果不采取相應(yīng)的方法來解決對象創(chuàng)建的問題,可能會(huì)造成對象管理的混亂。因此,我們還有必要引入工廠模式,專門負(fù)責(zé)日志對象的創(chuàng)建。

我最初考慮在工廠方法中,將這些日志類型放到一個(gè)Type[]數(shù)組中,然后再通過反射的方式創(chuàng)建對象。然而,由于創(chuàng)建日志對象的組合會(huì)很麻煩,采用這樣的設(shè)計(jì),反而會(huì)有過度設(shè)計(jì)的嫌疑。(這也是我為什么在Decorator類中使用構(gòu)造函數(shù)而非采用專門的方法來設(shè)置Decoratee對象的原因。)

所以,只需要直接根據(jù)日志的情況為其分別創(chuàng)建相關(guān)的工廠方法就可以了。
public class DecoratorFactory
{
 private static IFee fee = new Fee();
 public static IFee CreateFee()
 {
  return fee;
 }
 public static IFee CreateBasicLogFee()
 {   
  IFee basicLog = new BasicLogDecorator(fee);
  return basicLog;
 }
 public static IFee CreateErrorLogFee()
 {
  IFee errorLog = new ErrorLogDecorator(fee);
  return errorLog;
 }
 public static IFee CreateImplLogFee()
 {
  IFee implLog = new ImplLogDecorator(fee);
  return implLog;
 }
 public static IFee CreateBasicErrorLogFee()
 {
  IFee basicLog = new BasicLogDecorator(fee);
  IFee errorLog = new ErrorLogDecorator(basicLog);
  return errorLog;
 }
 public static IFee CreateBasicImplLogFee()
 {
  IFee basicLog = new BasicLogDecorator(fee);
  IFee implLog = new ImplLogDecorator(basicLog);
  return implLog;
 }
 public static IFee CreateAllLogFee()
 {
  IFee basicLog = new BasicLogDecorator(fee);
  IFee implLog = new ImplLogDecorator(basicLog);
  IFee errorLog = new ErrorLogDecorator(implLog);
  return errorLog;
 }
}

然后再修改NUnit的測試代碼:
[TestFixture]
public class TestFee
{
 private IFee fee;
 [SetUp]
 public void Init()
 {
  fee = DecoratorFactory.CreateFee();   
 }
 [Test]
 [Category(”SettleWithoutLog”)]
 public void Settle()
 {  
  Assert.IsNotNull(fee);
  Assert.AreEqual(6,fee.SettleFee(2.0,3));
 }
 [Test]
 [Category(”SettleWithBasicLog”)]
 public void SettleBasicLog()
 {  
  IFee basicLogFee = DecoratorFactory.CreateBasicLogFee();
  Assert.IsNotNull(basicLogFee);
  Assert.AreEqual(6,basicLogFee.SettleFee(2.0,3));
 }
 [Test]
 [Category(”SettleWithErrorLog”)]
 public void SettleErrorLog()
 { 
  IFee errorLogFee = DecoratorFactory.CreateErrorLogFee();
  Assert.IsNotNull(errorLogFee);
  Assert.AreEqual(6,errorLogFee.SettleFee(2.0,3));
 }
 [Test]
 [Category(”SettleWithImplLog”)]
 public void SettleImplLog()
 { 
  IFee implLogFee = DecoratorFactory.CreateImplLogFee();
  Assert.IsNotNull(implLogFee);
  Assert.AreEqual(6,implLogFee.SettleFee(2.0,3));
 }
 [Test]
 [Category(”SettleWithBasic&ErrorLog”)]
 public void SettleBasicErrorLog()
 { 
  IFee log = DecoratorFactory.CreateBasicErrorLogFee();      
  Assert.IsNotNull(log);
  Assert.AreEqual(6,log.SettleFee(2.0,3));
 }
 [Test]
 [Category(”SettleWithBasic&ImplLog”)]
 public void SettleBasicImplLog()
 { 
  IFee log = DecoratorFactory.CreateBasicImplLogFee();   
  Assert.IsNotNull(log);
  Assert.AreEqual(6,log.SettleFee(2.0,3));
 }
 [Test]
 [Category(”SettleWithAllLog”)]
 public void SettleAllLog()
 { 
  IFee log = DecoratorFactory.CreateAllLogFee();
  Assert.IsNotNull(log);
  Assert.AreEqual(6,log.SettleFee(2.0,3));
 } 
 [TearDown]
 public void Dispose()
 {
  /*—*/
 }
}

經(jīng)過這么多階段的修改和完善,目前看來解決方案已經(jīng)比較完善了。如果在Fee類中還有其他的方法,需要日志功能,方法仍然大同小異。因?yàn)樵贑#中可以同時(shí)實(shí)現(xiàn)多個(gè)接口,如果實(shí)現(xiàn)其他接口的類也要增加該日志功能,則日志的Decorator類同時(shí)還要去實(shí)現(xiàn)這個(gè)新的接口。好處是,你只需要修改這些實(shí)現(xiàn),而調(diào)用的代碼,卻不用作大的修改了。因?yàn)橐筇峁┤罩竟δ艿男枨罂赡軙?huì)不斷增加,但只要日志的種類不變,用作裝飾功能的日志對象個(gè)數(shù)就不會(huì)改變。

自然,本文討論日志功能是完全站在OOP的角度來考慮的。如果引入AOP的思想,將日志看作是一個(gè)方面(Aspect),那么對于客戶而言,可能會(huì)更簡單。但這已不是本文要討論的問題了。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多