支持继承的链式调用范式Method chaining Pattern with generic derivation

本文实现基于C#与Typescript.

  • 链式调用
  • 使用Generics 实现链式调用扩展
  • 使用local class优化代码结构

链式调用

链式调用方法是一个常见的编程范式,特别是在对象构造器的类上,通常是一些builder的类使用了链式调用。链式调用给复杂对象的创建参数提供了简便的调用方式。

一个常见的Method Chaining 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

class Data{
public items:Item[] = [];
public num:number = 0;
}

class DataBuilder{
protected m_data:Data = new Data();
public static create():DataBuilder{
return new DataBuilder();
}

public addItem(o:Item):DataBuilder{
this.m_data.items.push(o);
return this;
}
public addNum(n:number):DataBuilder{
this.m_data.num +=n;
return this;
}

public finish():Data{
return this.m_data;
}
}

let data = DataBuilder.create().addItem(new Item()).addNum(100).addItem(new Item()).finish();

考虑继承的情况,比如DataBuilder是在提供的一个Library中的类,同时希望使用的开发者可以对这个链式调用构造类进行扩展。

具体的情形就变为:

1
2
3
4
5
6
7
8
9
10
class CustomDataBuilder extends DataBuilder{
public static create():CustomDataBuilder{
return new CustomDataBuilder();
}
public static addCustomItem(o:Item):CustomDataBuilder{
this.m_data.items.push(o);
}
}

let data = CustomDataBuilder.create().addCustomItem(null).addNum(100).finish();

这样我们可以使用子类的方法进行链式调用,同时也可以调用到基类的addNum方法。

但是这里存在一个问题,当CustomDataBuilder的对象调用了其基类的方法后,返回的是基类DataBuilder的实例,就无法再次链式调用CustomDataBuilder的方法了。也就是:

1
let data = CustomDataBuilder.create().addNum(100).addCustomItem(null); //Error

虽然typescript会被编译成js,this依旧是CustomDataBuilder的对象可以正常执行,但是typescript的类型检测却无法通过。 同时这个pattern在C#中也是无法完成编译的。

使用Generics实现链式调用扩展

所以我们希望所有的链式调用方法返回的都是其本身的类型签名,这样我们就必须用到GenericType。

1
2
3
4
5
6
class ABuilder<T extends ABuilder<T>>{

public funcA():T{
return;
}
}

由于链式调用的需求,T必须是ABuilder<T>的子类。所以T带有T extends ABuilder<T>的约束。

同时由于我们没法限定ABuilder<T>T或其子类,所以funcA()中不能return this,所以我们引入一个T的成员。

1
2
3
4
5
6
class ABuilder<T extends ABuilder<T>>{
protected t:T;
public funcA():T{
return this.t;
}
}

由于ABuilder是泛型类,不能直接创建其对象,在typescript中let b = new ABuilder()时,泛型类T是一个空对象{}。 我们需要引入一个额外的类,来实现其泛型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ABuilder<T extends ABuilder<T>>{
protected t:T;
public funcA():T{
return this.t;
}
}
class A extends ABuilder<A>{
public static create():A{
let a = new A();
a.t = a;
return a;
}
}

let a = A.create().funcA().funcA().funcA();

实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
class B extends ABuilder<B>{
public static create():B{
let b = new B();
b.t = b;
return b;
}
public funcB():B{
return this.t;
}
}

let a = A.create().funcA().funcA().funcA();
let b = B.create().funcB().funcA().funcB();

这样我们就可以通过类的继承来扩展ABuilder

如果类B也需要被扩展,那么我们可以定义一个BBuilder<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BBuilder<T extends BBuilder<T>> extends ABuilder<T>{

public funcB():B{
return this.t;
}
}

class B extends BBuilder<B>{
public static create():B{
let b = new B();
b.t = b;
return b;
}
}

class C extends BBuilder<C>{
public static create():C{
let c = new C();
c.t = c;
return c;
}
public funcC():C{
return this.t;
}
}

let c = C.create().funcA().funcB().funcA();

使用local class优化代码结构

由于在C#中,可以使用相同的Symbol定义一个类和一个类的范型类,这样将原先的一个类拆封为两个结构上看上去依旧优雅。

1
2
3
4
5
6
public class A<T> where T:A<T>{
//...
}
public class A: A<A>{
//...
}

要进行扩展时使用A<T>,直接调用时使用A

但是在TS中泛型仅仅是Typescript Compiler的语法糖,最终编译为的JS中A<T>A共用了同一个域中的名称。所以就一定要有两个不同的名称。

我们可以使用typescript中的local class来将两个类合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class A<T extends A<T>>{
protected t:T;
public static ABuilder = class ABuilder extends A<ABuilder>{
private constructor(){
super();
this.t = this;
}
public static create():ABuilder{
return new ABuilder();
}
}
public funcA():T{
return this.t;
}
}

class B<T extends B<T>> extends A<T>{
protected t:T;
public static BBuilder = class BBuilder extends B<BBuilder>{
private constructor(){
super();
this.t = this;
}
public static create():BBuilder{
return new BBuilder();
}
}
public funcB():T{
return this.t;
}
}

let a = A.ABuilder.create().funcA().funcA().funcA();
let b = B.BBuilder.create().funcB().funcA().funcB();

Gist - C#/Typescript