第11章 重构 API
模块和函数是软件的骨肉,而 API 则是将骨肉连接起来的关节。
易于理解和使用的 API 非常重要,但同时也很难获得。
好的 API 会把更新数据的函数与只是读取数据的函数清晰分开。
类是一种常见的模块形式。
将查询函数和修改函数分离(Separate Query from Modifier)
如果某个函数只是提供一个值,没有任何看得到的副作用,那么你需要操心的事情就少多了,测试也更容易。
任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离(Command-Query Separation)。
函数参数化(Parameterize Function)
如果两个函数逻辑非常相似,只有一些字面量值不同,可将其合并成一个函数,以参数的形式传入不同的值从而消除重复。
移除标记参数(Remove Flag Argument)
标记参数用来指示被调函数应该执行哪一部分逻辑,但标记函数隐藏了函数调用中存在的差异性。
如果调用者传入的是程序中流动的数据,这样的参数不算标记参数;只有调用者直接传入字面量值,这才是标记参数。
另外,如果参数值只是作为数据传给其他函数,这也不是标记参数;只有参数值影响了函数内部的控制流,这才是标记参数。
移除标记参数不仅使代码更整洁,并且能帮助开发工具更好地发挥作用。
保持对象完整(Preserve Whole Object)
“传递整个Record”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,就不必为此修改参数列表。
从一个对象中抽取出几个值,单独对这几个值做某些逻辑操作,这是一种代码坏味道,通常标志着计算逻辑应该被搬移到对象中。
如果几处代码都在使用对象的一部分功能,可能意味着应该用提炼类将其单独提炼出来。
以查询取代参数(Replace Parameter with Query)
函数的参数列表应该总结该函数的可变性,标示出函数可能体现出行为差异的主要方式。
参数列表越短越容易理解,且应该尽量避免重复。
如果调用函数传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。
一般而言,习惯于简化调用方,让尽可能多的责任移交给函数本身。
当然,如果以查询取代参数不是同样容易时,或者可能会给函数体增加不必要的依赖关系,那么就应该慎重考虑这项重构。
函数调用的时候应该具有引用透明性(referential transparency),即不论任何时候,只要传入相同的参数值,该函数的行为永远一致。
以参数取代查询(Replace Query with Parameter)
如果函数的实现会引入复杂或不必要的引用关系或全局变量,就应该将处理引用关系的责任转交给函数的调用者,将其替换为函数参数。
此项重构大多来源于想改变代码的依赖关系——让目标函数不再依赖于某个元素。
需要权衡的是复杂的依赖关系和参数冗长重复之间的利弊。
把“不具有引用透明性的元素”变成参数传入,函数就能重获引用透明性。
在负责逻辑处理的模块中只有纯函数,其外再包裹处理 I/O 和其他可变元素的逻辑代码。
把查询变成参数以后,函数调用者的复杂度会上升。归根结底,这是关于程序中责任分配的问题,这方面的决策既不容易,也不会一劳永逸。
移除设值函数(Remove Setting Method)
如果为某个字段提供了设值函数,就暗示着这个字段可以被改变。
如果不希望字段被改变,就不要为此提供设值函数,让其只能在构造函数中赋值,清晰地表达“构造之后不应该再更新字段值”。
以工厂函数取代构造函数(Replace Constructor with Factory Function)
构造函数只能返回当前调用类的实例,无法根据上下文返回子类实例或代理对象。
构造函数的名字是固定的,无法使用比默认名字更清晰的函数名。
构造函数只能通过特殊操作符来调用(new),在某些场景下难以使用。
工厂函数不受这些限制,工厂函数内部可以调用构造函数,也可以换成别的方式实现,比如反射。
以命令取代函数(Replace Function with Command)
附着在对象上的函数/方法是程序设计的基本构造块,不过将函数封装成自己的对象,也是一种有用的办法。
这种对象称之为“命令对象”,或者简称为“命令”。这种对象大多只服务于单一函数,获得对该函数的请求,执行该函数,就是这种对象存在的意义。
命令对象提供了更大的控制灵活性和更强的表达能力。
不过,命令对象的灵活性也是以复杂性作为代价的。所以只有在必要的时候才考虑命令对象。