保持函数/方法的独立性

函数式编程(Functional Programming)的一大特点是不修改外部状态,仅仅产生返回值;也不依赖外部状态,函数仅依赖输入参数。换句话说不使用函数体作用域外的变量,函数有着很强的独立性。由此带来的好处包括:

  1. 易于单元测试(Unit testing)
  2. 方便调试(Debuging)
  3. 高并发(Concurrency)
  4. 热代码部署(Hot code deployment)

下图描述了单元测试的场景:
SUT-DOC
图片来自于 xUnit Test Patterns – Refactoring Test Code一书

Setup,Exercise,Verify和Teardown是进行单元测试的典型过程。
SUT(System Under Test)指被测的函数/方法。
DOC(Dependent-On Component)是SUT依赖的组件,像关联的类,需要调用的函数/方法,上下文对象,全局对象以及运行环境等。

有过单元测试经验的人都知道,为了进行测试,往往需要编写大量的代码创造测试条件。原因就是因为DOC的存在。在函数式编程中,由于函数的独立性,DOC就不存在了。也就没有了对Indirect input(间接输入)的模拟,没有了对Indirect output(间接输出)的验证;仅使用Direct input(直接输入,就是参数),也仅验证Direct output(直接输出,就是返回值),因此单元测试就变得简单多了。

函数式编程是一种如何编写程序的方法论。并不是Lisp, Haskell,Erlang, F#这些函数式编程语言才能用的,在面向对象编程语言中也可以用这种思想。

   1:  public class Foo
   2:  {
   3:      public string Bar()
   4:      {
   5:          // get user id from session
   6:          string userID = HttpContext.Current.Session("UserID");
   7:          // get image path from configuration file
   8:          string imagePath = ConfigurationManager.AppSettings["ImagePath"];
   9:          // do some thing with userID and imagePath
  10:          // ......
  11:      }
  12:  }

这段代码中HttpContext和ConfigurationManager就是DOC,是Bar方法的间接输入。如果进行单元测试的话,我们会对代码改造下,使用接口来代替具体类。

   1:  public class Foo
   2:  {
   3:      public string Bar()
   4:      {
   5:          string userID = ISession.GetCurentUserID();
   6:          string imagePath = IConfiguration.GetImagePath();
   7:          // do some thing with userID and imagePath
   8:          // ......
   9:      }
  10:  }

使用了接口后,在进行单元测试的时候就可以通过Mock对象来模拟,避免了HttpContext和配置文件。

接口的具体实现可以通过构造函数注入,属性注入或者方法注入。也可以通过IoC或者Service locator来获取。但无论哪种方式,都没有减少对外部的依赖。我们不妨按FP的思想,把依赖的外部状态变为参数。

   1:  public class Foo
   2:  {
   3:      public string Bar(string userID, string imagePath)
   4:      {
   5:          // do some thing with userID and imagePath
   6:          //  ......
   7:      }
   8:  }

如果Foo类对应一个User的话,可以将userID参数提升到属性。

   1:  public class Foo
   2:  {
   3:      public string UserID { get; set; }
   4:      public string Bar (string imagePath)
   5:      {
   6:          // do some thing with UserID and imagePath
   7:          // .....
   8:      }
   9:  }

通过这种方式使Bar方法保持了独立性。按面向对象的说法是降低了耦合性。然而最终还是需要一个地方创建具体实例,把值传递进来。我们尽量把这些“脏活”集中起来,像交给MVC中的Controller,工厂类或工厂方法等,牺牲少部分代码而让大部分代码保持低耦性。

如果Bar方法中使用了大量配置项,那么就会造成参数列过长。这时可以使用《重构》中的引入参数对象,或者直接传入IConfigruation也是可以接受的。总之,尽量不和外部发生关系,一定要发生的话,尽量保持简单。

posted @ 2013-08-28 00:09  cypine  阅读(489)  评论(0编辑  收藏  举报