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 的一部分提供给调用方,这样调用方可以在不查看具体实现的情况下,基于默认行为做扩展。
- 把回调函数提取成类,通过类的继承,可以将默认行为作为基类的方法提供给调用方。
- 一个不使用类的更轻量的做法,将默认行为作为回调函数的参数提供给调用方。
发表回复