Life of xhu

About

随想: 链式调用和无状态函数

Nov 11, 2015

  |   #Language

在我看来,链式调用是函数(function)进化到方法(method)的一个重要标志

在传统的C/C++代码中,如果一段代码的逻辑复杂,很容易写出函数嵌套:

funcA(funcB(funcC(foo)))

这种从里到外的方式个人总是觉得别扭而且难以阅读,所以在后来的Ruby代码中,大部分代码都写成了:

bar.methodA.methodB.methodC

我觉得这样编程的关键,就是在编程的时候,保证在程序中流转的不是简单的数据,而是对象。基础类型的数据在很多语言里是没有方法的(Ruby除外),而如果传递的是对象的话,我们可以直接调用对象的方法,使逻辑思考和代码编写的顺序统一起来。

jQuery其实就是这个思想的践行者,相信每个学习jQuery的人都知道链式调用是jQuery的卖点之一,因为在原生JavaScript里有许多parseInt这样对链式调用不友好的函数,而一个jQuery对象在调用方法后,返回的仍然是jQuery对象,这样就可以继续调用下去,代码也会优雅很多。

我们可以用一段简短的代码来说明:

var Test = function (name) {
  this.name = name;
}

Test.prototype.say = function () {
  console.log('my name is ' + this.name);
  return this;
}

var t = new Test('xhu');
t.say().say().say();
/*
  my name is xhu
  my name is xhu
  my name is xhu
*/

无状态函数也可以叫纯函数,纯函数的意思就是对于同样的输入,函数的输出永远一致。

要做一个纯粹的函数,一个脱离了低级趣味的函数

无状态函数的一个优点就是,不论在任何条件下,只要输入一致,那么就可以确保输出的一致,这样的函数是可测试的,因为有上下文无关的特性,可以很轻易编写出测试用例,而且与系统中其他的组件的耦合性很低。

比如在Ruby中,当我们给一个类添加实例方法的时候,应该尽量在复杂的方法中避免使用@开头的类变量来传递数据,因为这样一来,方法本身的状态就和整个对象耦合在一起了,在编写测试用例的,还要保证整个对象的状态正确,这样会让测试变的寸步难行,而单元测试unit test的本意就是把代码分成一个个逻辑无关的独立单元来编写用例,所以无状态函数天生就是测试友好的,确定的断言assert对于特定的输入,结果肯定是有效的。

而在React中,无状态这个概念得到进一步的延伸,在编写component的时候,React官方推崇的做法就是,每一个component都应该是无状态的,我们用函数做类比的话,那么component的输入就是props,针对同样的props,React component期望的行为应该一致。

在之前使用React + Flux来编写应用的时候,我犯了一个新手很容易犯的错误,也就是用state来负责应用的状态,其实这个非常不正确,整个component的状态应该用props来表示,state应该内聚于一个component内部,负责内部的数据以及ui,而不应该受到component之间数据的影响。

重构项目之后使用的Redux则更加极端,在Redux的哲学里,整个项目应该都处于一个可控的状态下,也就是一个统一的rootState,react-redux的connect函数隐性地把rootState通过props绑定到每个component上,如果把component看成一个函数的话,props是输入,ui是输出,对于一个确定的rootState,component的状态也是确定的,state只能在component内部起作用

这样一来,之前component里操作数据并且可能会互相影响的state被丢弃,取而代之的是统一的数据管理,因为在React中,很多时候我们会使用setState来改变状态以及重绘ui,使用state来表示应用状态会使应用变的很不稳定,而且出了bug也不容易进行调试,当我们在Redux里使用统一的数据管理和无状态component时,追溯数据导致的bug会简单很多。