模块和函数是软件的骨肉,而 API 则是将骨肉连接起来的关节
本章将前面学过的一些重构手法结合起来,针对软件系统提供外部API使用的一些建议和优化
11.1 将查询函数和修改函数分离(Separate Query from Modifier)
1 | function getTotalOutstandingAndSendBill() { |
- 任何有返回值的函数,都不应该有看得到的副作用
- 谨防一些函数名是GetXXX的函数,内部却做了其他事情(设值、调用其他函数等)
- “看得到”指不希望做的多余作用,如果只是缓存计算值等优化手段不算
API函数设计应该符合职责单一原则,由外部使用者去组合调用,这样才能适用于更多的使用环境
11.2 函数参数化(Parameterize Function)
1 | function tenPercentRaise(aPerson) { |
- 本质是消除重复代码
- 大部分计算逻辑一致,但内部使用的字面值不一致的函数应该合并,字面值由参数传入
游戏里一些到达指定点,执行对应行为等只有单位不一样的通用逻辑也可以通过传入不同的单位去处理
11.3 移除标记参数(Remove Flag Argument)
1 | function bookConcert(aCustomer, isPremium) { |
- 标记参数:直接传入字面量值且参数值影响了函数内部的控制流
- 造成的疑惑:到底有哪些函数可以调用、应该怎么调用
- 标记参数隐藏了函数调用中存在的差异性
如果标记参数参与了实际的计算逻辑,比较难拆分,可以使用上层函数。然后将已有函数设为私有或者命名约束成不希望别人使用,比如下方案例中【deliveryDate】
1 | function rushDeliveryDate(anOrder) { |
11.4 保持对象完整(Preserve Whole Object)
1 | const low = aRoom.daysTempRange.low; |
传入完整对象的好处:
- “传递整个记录”的方式能更好地应对变化
- 缩短参数列表,让函数调用更容易看懂
- 如果有很多函数都在使用记录中的同一组数据,处理这部分数据的逻辑常会重复,可以挪到对象内部处理
需要担心的点:
- 被调函数依赖完整对象,如果两者不在同一模块更加糟糕
- 可变和不可变,如果数据出现问题,传入对象的函数会被重点拷打是否修改了不必要的数据
11.5 & 11.6 以查询取代参数(Replace Parameter with Query)、以参数取代查询(Replace Query with Parameter)
1 | availableVacation(anEmployee, anEmployee.grade); |
以查询取代参数:
- 参数列表应该尽量避免重复,并且参数列表越短就越容易理解
- 重复:如果函数内部查询这个参数也“同样容易”,那参数就重复
- 去除参数也就意味着“获得正确的参数值”的责任被转移,本来是调用者需要保证参数正确,现在是函数内部保证
- 一般来说API接口更偏向于减轻使用者负担(也可以提供多个版本)
以参数取代查询:
- 移除参数可能会给函数体增加不必要的依赖关系
- 谨慎:为了去除一个参数,在函数体内调用一个有问题的函数,或是从一个对象中获取某些原本想要剥离出去的数据
- 当引入不想要的引用关系时就应该将处理引用关系的责任转交给函数的调用者
比较明确的场景:
- 如果是另一个参数可以通过其他参数推导得出的多多使用【以查询取代参数】
- 如果一种只要传入相同的参数值,该函数的行为永远一致,这种具有【透明性】的函数建议还是【以参数取代查询】
一些需要权衡的点:
- 如果把所有依赖关系都变成参数,会导致参数列表冗长重复
- 如果作用域之间的共享太多,又会导致函数间依赖过度
像决策一样,都没有完美的解法,只能去权衡哪边带来的收益更高成本更低。
11.7 移除设值函数(Remove Setting Method)
- 如果明确不希望外部修改数据,就通过手段限制只在构造时赋值数据
- 不确定是否需要设值函数,一律当做不需要
以前老是习惯加个设值函数,但其实如无必要勿增实体,更何况可变数据对心理负担太大了
11.8 以工厂函数取代构造函数(Replace Constructor with Factory Function)
1 | leadEngineer = new Employee(document.leadEngineer, "E"); |
主要为了解决创建对应对象时,还要传入一个用户不关心的必要数据,避免构造函数的局限性(无法根据环境或参数信息返回子类实例)。
11.9 & 11.10 以命令取代函数(Replace Function with Command)、以函数取代命令(Replace Command with Function)
1 | function score(candidate, medicalExam, scoringGuide) { |
使用命令对象的优缺点:
优点:
- 与普通的函数相比,命令对象提供了更大的控制灵活性和更强的表达能力(比如可以对操作进行记录和撤回等)
- 参数绑定更加灵活,对象新增一系列set函数
缺点:
- 命令对象过多或者简单的操作不需要命令对象强大功能时造成系统的复杂度上升
与命令对象相关的设计思想
- 命令模式
- 一种数据驱动的设计模式,一般由接受者、请求者、命令组成
- 用于解决请求者和执行者之间的紧耦合问题
- 增加系统的扩展性,新命令可以很容易加入
- 常用于交互界面的撤销、操作的同步等
- 命令查询职责分离模式CQRS
- 任何一个方法都可以拆分为命令和查询两部分
- 分离了读写职责使得系统状态的改变变得清晰可查,能够提高系统的性能、可扩展性和安全性
- 适用于业务逻辑层有很多操作需要相同的实体或者对象进行操作的时候,比如基于任务的用户交互系统,通常这类系统会引导用户通过一系列复杂的步骤和操作
- 不过书中作者认为,更愿意将这里的命令称为“修改函数”(modifier)或者“改变函数”(mutator)。
- QFramework用到了相关的思想