TLDR
在设计回调函数形式的扩展点时,扩展点的默认行为也要提供给调用方,方便基于默认行为进行扩展。达到这一目的的模式有:
- 把回调函数提取成单独的类,把默认行为作为基类方法提供给调用方
- 把默认行为作为回调函数的参数提供给调用方
推敲过程
Alice:Hi,Bob,我有一个重构代码的需求,能帮我看看吗?
Bob:没问题,Show me your code 。
Alice:之前的代码是这样的
class MyClass { public update() {} public fooBehavior() {} public changeState(params) { // do something this.fooBehavior(); // finally this.update(); } }
现在,我需要允许调用方修改 changeState
要执行的行为
class MyClass { public update() {} public fooBehavior() {} public changeState(params, behaviorType?: string) { // do something if (behaviorType === 'A') { // do behavior A } else if (behaviorType === 'B') { // do behavior B } else { this.fooBehavior(); } // finally this.update(); } }
Bob:根据以往的经验,这里的 if-else 很可能会变成叠 buff 的屎山,建议把 behaviorType
换成一个回调函数,让调用方决定这里应该干些啥。
Alice:改好了,你再看看?
class MyClass { public update() {} public fooBehavior() {} public changeState(params, onChangeState?: () => void) { // do something if (onChangeState) { this.onChangeState() } else { this.fooBehavior(); } // finally this.update(); } }
Bob:这里还有些问题,调用方只能覆盖默认行为,并不能在默认行为的基础之上做扩展。
Alice:调用方也可以自己手动调用 fooBehavior
啊,这也是个公共 API 。
Bob:但是调用方必须得翻看 changeState
的具体实现才知道「fooBehavior
是 onChangeState
的默认实现,这显然是不合理的。
Alice:在使用这个类时翻看源码很正常,因为这是一个非常高级晦涩的 API,不翻看源码根本不清楚要用它来干什么。
Bob:OK,就算调用方在拓展这个地方的时候来查看了 onChangeState
的默认实现,然后把这里的代码复制过去来基于默认行为做扩展。但是,随着版本更新迭代,MyClass
中的默认实现可能会发生变更,这个时候,要怎么通知这些依赖默认行为的调用方更新呢?
Alice:好吧,这样看来确实是个问题。
Bob:我觉得这个时候可以把 onChangeState
提取成一个单独的类,它是这个扩展点的基类。调用方在进行扩展的时候,可以通过基类的方法来调用默认行为。
class MyClass { public update() {} public fooBehavior() {} public changeState(params, changeStateHook: ChangeStateHook = new ChangeStateHook()) { // do something changeStateHook.after(this); // finally this.update(); } } class ChangeStateHook { after(instance: MyClass) { instance.fooBehabior(); } } class ConsumerChangeStateHook extends ChangeStateHook { after(instance: MyClass) { if(/* some case */) { super.after(); } else { // do something else } } }
Alice:这样倒是可以满足你在上面提到的需求,但是代码看起来也太阵仗了。为了自定义这个回调函数,还得专门声明一个类!我觉得用个字段来存储默认行为,然后在注释上标明就足够了。
class MyClass { public update() {} public fooBehavior() {} public defaultOnChangeState() { this.fooBehavior(); } /** * 默认 onChangeState 是 defaultOnChangeState */ public changeState(params, onChangeState?: () => void) { // do something if (onChangeState) { this.onChangeState(); } else { this.defaultOnChangeState(); } // finally this.update(); } }
Bob:这样做确实轻量很多,但并不是一个好的「模式」。如果后面这种拥有默认行为的回调函数越来越多, MyClass
上就会有很多 defaultOnXxxx
,看起来就很糟心。不如把默认行为作为回调函数的参数提供给调用方。
class MyClass { public update() {} public fooBehavior() {} public changeState(params, onChangeState?: (defaultOnChangeState: () => void) => void) { // do something const defaultOnChangeState = () => { this.fooBehavior(); } if (onChangeState) { this.onChangeState(defaultOnChangeState); } else { defaultOnChangeState(); } // finally this.update(); } }
Alice:看起来很不错,跟我写的第一版回调函数形式 API 相比没有多几行代码,但是却有更加灵活的扩展空间。
Bob:好的,最后来总结一下:
- 回调函数的默认行为需要作为 API 的一部分提供给调用方,这样调用方可以在不查看具体实现的情况下,基于默认行为做扩展。
- 把回调函数提取成类,通过类的继承,可以将默认行为作为基类的方法提供给调用方。
- 一个不使用类的更轻量的做法,将默认行为作为回调函数的参数提供给调用方。
发表回复