技术开发 频道

设计,由你掌握

  四、问题又出现了

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

0
相关文章