设计,由你掌握

来源:互联网 发布:尤克里里价格淘宝 编辑:程序博客网 时间:2024/04/29 09:20

设计,由你掌握
前言:XP中有个准则,就是只做目前你需要做的。例如,我需要加法运算,你就没有必要实现乘法运算,因为这不是客户需要的。所以在开发中,我们可以不去考虑程序对于未来的扩展性。“简单最好!”那么,是否就不需要设计了呢?对于设计模式而言,是否也可以不去了解了呢?答案当然是否定的。因为客户的需求是“与时俱进”的,现在不实现,并不等于今后不实现。在实现中,不管是重构,还是重新设计,通过应用设计模式,能令你如虎添翼。关键不在于设计模式是否重要,而在于你怎么应用它,以及选择什么样的时机。总而言之,设计,由你掌握!


一、从需求开始

在我们的项目中,作费用结算的时候,客户要求将该过程与结果写入到日志文件中。不过他们的要求很善良,只需要知道日志记录结算开始与结束的时间而已。是的,按照XP的理念,我们只需要做好客户需要的事情就OK了。既然是这样,事情就好办,代码轻易而举就实现了:
public class Fee
{
 //结算程序将调用数据层的相关方法,访问数据库;
 //为简单起见,我用累加数取代;
 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<records;i++)
  {
   result+=money;
  }
  Console.WriteLine("Settling fee finished at {0}",DateTime.Now);
  return result;
 }
}


写好这个,还差点什么?不错,我们还需要为Fee类撰写相应的测试代码,做好单元测试。可能对于传统的程序员来说,更喜欢的是在编码完成后,再根据测试计划编写测试样例,最后测试。但敏捷开发的要求却是测试先行,单元测试是必不可少的环节。不过,我认为单元测试毕竟只是一种手段。我们在实际的项目开发中,对于单元测试不可拘泥教科书的要求,按部就班地一步一步进行;而应该根据实际情况,比如开发者对语言的掌握程度,对设计的理解等等,来决定你单元测试的步骤,乃至于重构的步伐。


[TestFixture]
public class TestFee
{
 [Test]
 pubic void Settle()
 {
  Fee fee = new Fee();
  Assert.IsNotNull(fee);
  Assert.AreEqual(6,fee.SettleFee(2.0,3));
 }
}


在NUnit中打开这个测试类,并运行。毫无疑义,你会看到测试的绿灯全部都亮了。你可以在NUnit中看看控制台输出的结果。自然你也可以在AreEqual()方法中,故意将预期的值设置错误,来看看运行NUnit是什么情况,以及出现的错误提示信息。不过,这些都不是本文关注的重点。


二、当需求改变了


在XP中,客户的重要是举足轻重的。在客户提出需求的时候,你需要和他尽可能地沟通,并保证意见最后要达成一致。然而客户对产品的理解可能有时候会出现偏差,也许有时候对方的要求也会随着产品的应用而逐渐发生改变。所幸的是,这一次需求的改变,发生在项目开发过程中,且是在你和他结对交流的时候,最终发现的缺陷。因为客户认为,这个日志过于简单了,并不利于今后对产品的维护。我得承认这是一个好的要求。
 

事实上,日志记录得越详细,对于开发人员自己也是有好处的。最后,我们决定,日志不仅仅要记载结算的起止时间,还应该记载可能会出现的错误信息,最好还能记载这个结算的过程代码,比如我们执行的是哪一个存储过程,读取了哪些表的数据,包括这些表的字段。


然而有个不利的因素是,这个费用结算的过程可能会很频繁的使用。如果写入的日志太复杂了,会否影响产品的性能?而且频繁写日志的话,日志文件会不会越来越大?如果我们这个产品已经非常健壮,还有必要去记载这些信息吗?毕竟有很多信息,对于普通用户而言,并没有实际用处,反而干扰了他有效获取日志的有用信息。


所以后来我们想到一个方法,就是将日志进行分级,从最简单到最详尽。用户在进行费用结算的时候,可以根据自身需要,选择日志的级别。无疑,这是一个令人满意的策略。


三、如果不熟悉设计模式


假设我们的开发人员对于设计模式一概不知,经过分析客户的需求,他会直接了当的做出如此的解决方案。首先定义三种级别的日志:SimplestLog,NormalLog,DetailedLog。SimplestLog只记录结算的起止时间和耗费的时间,同时还要记录结算后的结果。NormalLog则除此之外,还要记录可能会出现的错误信息。而DetailedLog最详尽,它不仅包含了NormalLog记录的信息,还包括记录结算的实现方法,如用到的存储过程,数据表和相应的字段。


我们最初设想为这三种级别建立三个不同的私有方法,然后在SettleFee()方法中,引入一个日志级别参数,然后根据日志级别的值,决定调用哪一个私有方法,例如:
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<records;i++)
 {
  result+=money;
 } 
}


不用说,我们的程序员遇到麻烦了。因为在记录日志信息的时候,可能会在结算的前后来进行。也就是说,结算的那一段代码必须放到记录日志的方法中,才可以实现。幸运的是,我们的程序员应该还具备重构的知识,他决定把结算的那一段代码专门抽取出来,形成一个单独的方法,再放到日志方法中调用。“Extract Method”,不是吗?很聪明的做法。


好吧,我们来实现它吧,看看会是怎样?


首先,实现专门的结算方法:
private double Settle(double money,int records)
{
 double result = 0.0;
 for (int i=0;i<records;i++)
 {
  result+=money;
 }
 return result;
}


再来实现日志方法:
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);
}


在这个方法中,result是Fee类中的一个私有变量,用来保存结算后的结果。假设不使用这个变量,而是在方法中引入局部变量,那么WritesimplestLog()方法就必须返回double类型了,这个设计可够糟糕的!同样的,money和records也应该是通过私有变量传递值,否则这个日志方法就必须带上这两个参数了。


接着实现下面两个方法。我们已经注意到根据日志级别的不同,最详尽的日志内容总是包含了其低一级日志的内容。并且,后两级日志没有包括性能的记录,因此记录的日志并未要求必须出现在结算方法的前后。
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 LogLevelEnum.Simple:
   WriteSimplestLog();
   break;
  case LogLevelEnum.Normal:
   WriteNormalLog();
   break;
  case LogLevelEnum.Detail:
   writeDetailedLog();
   break;    
 }
 return result; 
}

嘿嘿,看起来还不错!
当然,与之相应的测试代码也要发生改变:
[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));
}


四、问题又出现了


我们程序员都应该有这个信仰,就是:简单最好。我不喜欢那些常常卖弄自己水平的人,仅仅为了一个简单的要求,却故意把代码弄得非常复杂,以为卖弄高深就是学问。其实不然。我倾向于简单的算法,即使它的性能稍差。因为性能差,我们还可以通过提升硬件等多种方式来解决;而如果整个项目都充斥着难于理解的算法,想一想,如果写代码的“牛人”走了,而目前又需要对程序做改进。那么,项目的后任者,在deadline的压力下,面对这一大堆“高深”的算法,会是怎样的抓狂!?


所以,从目前来看,以上实现的代码并没有什么不合适的地方。简单易懂,也完成了客户的需求。不过,很多时候事情并非尽如人意。客户的需求会随着对项目的跟进,而逐渐发生改变。


一周后,我们的客户提出了新的要求。首先,他希望这个日志功能,能够展现它更灵活的一面。不是死板的从最简单到最详尽,而是根据日志记载的内容,任意灵活的组合。原来的日志层次如图一:

 
  
图一:最简单的日志组合


而根据现在的需求,可能会有多种组合:

 
 
图二:灵活的组合,日志变得多种多样


我们算一算,如果按照前面的方式来实现新的需求,可能会写出多少个方法?此时的你应该怎样办呢?


或许应该将逻辑抽象出来,为日志建立一个基类;然后根据实际的需求派生不同的子类。在调用的时候,可以通过多态的方式,决定调用何种具体的日志对象方法。


但是问题接踵而至。首先是写日志方法和费用结算方法如何结合起来?尤其是A类日志,写日志的时候必须是在费用结算的前后,以确定结算的起止时间。也许,我们可以考虑将该方法分离为两个方法BeforeWriteLog()和AfterWriteLog()。那么与之对应的,凡是和A类日志有关的其他日志,也必须实现这种分离策略。再想想继承的子类,根据日志这种灵活的组合,我们需要创建多少个子类对象。一旦需求再增加呢?这个无底洞我是不愿意去跳的。


而客户的要求并没有结束。他还需要在与费用有关的其他方法中,也实现日志的功能。例如费用查询。由于之前的策略是在日志方法中包含费用结算的功能。如果需要记录费用查询的日志,岂不是要为它再建立不同的日志方法?


总之,如果你不了解设计模式的话,你可能会非常棘手。你会用尽所有已掌握的知识,通过你对OOP的理解,使用接口,继承和聚合,最后你可能会发现你碰壁了。乐观的是,你最终解决了问题。可是看看解决方案呢?要么拙劣不忍目睹,要么幸运的是你采用了正确的策略。你已经达到了GOF的水平,自己创造出我们应该正确使用的模式了。不过,可惜的是,你会沮丧地听到,有人会告诉你,你采用的其实就是设计模式中的Decorator模式。与其这样,为什么不好好地学习一下设计模式呢?


五、结果完全不一样了


如果你已经熟悉了设计模式的话,面对客户提出的新的需求,你就会很快获得完美的解决方案了。

分析一下。虽然日志的级别会非常之多,但其根本的功能却是一致的,那就是所有的日志信息都是费用结算(自然也包括费用查询)的包装而已。形象地说,日志就好比油漆工人,而费用结算就是一间房子,需要油漆工人来给墙壁粉刷上美丽的色彩。如此而已。

修改后的结构类图如下:


此时,我将原来的日志方法修改为单独的类对象。同时将日志由原来分级(Simplest, Normal, Detailed)的方式,改为按各自的功能划分,然后建立日志对象(BasicLogDecorator,ErrorLogDecorator,ImplLogDecorator)。具体的修改思路和步骤,如第六部分描述。


在使用Decorator模式来实现如上需求之前,我想表明自己的态度:
1、 设计模式的重要性已经不言而喻了;
2、 不要为了模式而去学习模式,设计模式必须和项目实际开发结合;
3、 如果目前的需求很简单,不用设计模式并不是一个坏的选择;
4、 因为我们有重构;
5、 但必须记住,重构的每一步,需要以单元测试来保证;
6、 你必须深入理解设计模式,否则当需求复杂之后,你会束手无策;
7、 设计模式是人创造出来的,但既然已经有了前人的成果,为什么不用?


写到这里,诸位已经可以结束本文的阅读了。不过我还得继续下去,作业没有做完,不能交卷。


六、大结局


因为现在的需求比较复杂了,所以你在重构每一步时,必须小心翼翼。别忘了单元测试,有了它,才可以保证你的正确无误。


首先,利用“Extract Interface”原则,为装饰的对象Fee类Extract一个接口,并让Fee类实现它:
public interface IFee
{
double SettleFee(double money, int records);
}
public class Fee:IFee {}

当然,我们需要把SettleFee()方法恢复成原来的模样。记住这个过程仍然需要小心翼翼。因为,在实现这一步时,可能已经离最初的简单实现已经有一周的时间了。所以,再恢复原样的过程中,我希望仍然不要放弃使用单元测试。当然在这里,我为了行文简洁,省略了这些过程。


修改测试代码,然后在NUnit中运行它:
[Test]
pubic void Settle()
{
 IFee fee = new Fee();
 Assert.IsNotNull(fee);
 Assert.AreEqual(6,fee.SettleFee(2.0,3));
}


现在来分析日志。根据对前面A类、B类、C类日志的分析。我们不再从是否详尽的角度来分类日志,而是从日志的内容或者说日志实现的功能来分类。我们可以将日志分为基本日志类、错误日志类、实现日志类三种。


基本日志类:实现日志的基本功能,包括费用结算的耗时和结算后的结果。
错误日志类:记录可能会出现的错误消息。
实现日志类:将费用结算的具体实现记录下来,便于以后对于产品的维护。


因为日志就是Decorator模式的油漆工,它们都需要具备包装费用结算的功能,我为它们定义了一个共同的抽象类:
public abstract class LogDecorator
{
 privated IFee decoratee;
 public LogDecorator(IFee decoratee)
 {
  this.decoratee = decoratee;
 }
 public IFee Decoratee
{
  get {return this.decoratee;}
}
}


基本日志类、错误日志类、实现日志类都继承该抽象类。注意抽象类的自定义构造函数,它是实现装饰功能的关键。该构造函数负责传递一个被装饰的对象进来,并赋给属性Decoratee。这个初始化的过程,就好比刷油漆的刷子,对于所有油漆工人来说,都应该是一样的,只是他们刷的油漆不同而已。

要装饰Fee类,仅仅依靠构造函数来传递被装饰对象还不够。我们还需要把原来Fee类所做的工作,转移到装饰类中,如此才能完成装饰的功用。所以,这三个日志类,不仅要继承LogDecorator类,还需要实现IFee接口,即与Fee类实现同一个接口。


首先是基本日志类:
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;
 }
}

做到这一步时,先别急着去实现另外两个类。我们应该先做单元测试。修改单元测试代码:
[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));
}


不过这个单元测试代码似乎有点乱,我们应该根据具体的实现,对测试方法分类,同时将类对象的初始化放到[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对测试方法进行分类(也可以对测试类进行分类)。这样,我们就可以在Nunit中,根据测试的情况,选择要测试的分类,或Exclude(排除)不测试的分类。我们运行一下,看NUnit的绿灯是否都亮了?测试通过后,就可以接着实现另外的日志类了。
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;
 }
}


当然每做一步改进后,都需要修改测试代码进行单元测试。最后的单元测试代码:
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()
  {
   /*---*/
  }
 }
}

由于每一步都严格进行了单元测试,所以,我们对代码的正确性充满了信心。这也是单元测试的重要性及必要性所在。


七、真的结束了吗


从测试代码中看出,目前的解决方案还存在一个问题,就是日志对象的创建。由于日志对象可能会根据不同的情况,组合成不同的对象。如果不采取相应的方法来解决对象创建的问题,可能会造成对象管理的混乱。因此,我们还有必要引入工厂模式,专门负责日志对象的创建。


我最初考虑在工厂方法中,将这些日志类型放到一个Type[]数组中,然后再通过反射的方式创建对象。然而,由于创建日志对象的组合会很麻烦,采用这样的设计,反而会有过度设计的嫌疑。(这也是我为什么在Decorator类中使用构造函数而非采用专门的方法来设置Decoratee对象的原因。)

所以,只需要直接根据日志的情况为其分别创建相关的工厂方法就可以了。


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()
 {
  /*---*/
 }
}


经过这么多阶段的修改和完善,目前看来解决方案已经比较完善了。如果在Fee类中还有其他的方法,需要日志功能,方法仍然大同小异。因为在C#中可以同时实现多个接口,如果实现其他接口的类也要增加该日志功能,则日志的Decorator类同时还要去实现这个新的接口。好处是,你只需要修改这些实现,而调用的代码,却不用作大的修改了。因为要求提供日志功能的需求可能会不断增加,但只要日志的种类不变,用作装饰功能的日志对象个数就不会改变。


自然,本文讨论日志功能是完全站在OOP的角度来考虑的。如果引入AOP的思想,将日志看作是一个方面(Aspect),那么对于客户而言,可能会更简单。但这已不是本文要讨论的问题了。

有朋友提到本文的日志Decorator是专用的,由局限性。因为昨天在MSN上来不及交谈。我回去后又看看了本文中的代码,想到一些事。给他回了mail,同时觉得在这里也有说明的必要。所以将mail内容复制一份在这里:

昨天你提到的问题,我回去好好想了一想。吃完饭后,上网没有看见你,无法交谈。所以现在给你谈谈我的看法。 你说的问题确实存在,但我认为并非我使用Decorator模式不对,而是该模式的限制而决定的。就文章目前介绍的代码来看,日志确实仅应用于IFee接口的对象。但要将该日志扩展,也并非没有可能,而且也很灵活。 例如,我还有一个与User相关的类,其接口为IUser。由于日志的Decoratee为IFee类型。因此,为了便于Decorator的扩展。我们必须提供一个公共接口,该接口没有任何方法,名为ICommonDecoratee。要被日志装饰的类对象都必须实现该接口。

也就是Fee类在实现IFee的同时,还要实现ICommonDecoratee;同样的,User类在实现IUser类的同时,还要实现ICommonDecoratee。然后,修改Decorator抽象类LogDecorator的构造函数:

public LogDecorator(ICommonDecoratee decoratee){} //原来为IFee。

然后再为LogDecorator具体的类分别实现这些接口就可以了。考虑到各个接口的方法不同。例如IFee接口有SettleFee()方法,而IUser接口有CountUser()方法,而这些方法都必须被装饰。因此,我们还需要做重构,将日志的一些重复代码,提取为一个方法。例如将BasicLogDecoratee类中原有的SettleFee()方法中,输出起止时间的代码提取为两个方法:BeforeOperate()和AfterOperate();这样实现各个接口的方法,就比较简单了。

如果基本日志的BeforeOperate()和AfterOperate()有扩展,那么我们还可以采用Template模式,将这两个方法设为vitual方法,并通过其派生类来改写它。

所以,应用Decorator模式是没有问题的。有问题的是,随着使用日志的业务增多,那么每个日志装饰类,要实现的接口也就越来越多,随着对应的工厂方法也会增多。这才是比较麻烦的事。所以,我才说,应该在此时考虑AOP的方式。

不过,我现在觉得在文章中,唯一的问题是不该举日志这个例子。因为日志这个例子太特殊了。在一格系统中,可能所有业务都需要日志的功能。这样仅仅用Decorator模式是不够的。我当时确实没有想到更好的例子,我在项目中用到Decorator也不太多。不过,本文只是想提出某些观点,至于这个例子倒是其次了。

本文的一些思想还是读JGTM的AOP三部曲想到的,有些抄袭的嫌疑,不过也结合了我自己的一些思想。呵呵:)

原创粉丝点击