Hilshire / blog

temporary blog
2 stars 0 forks source link

在 TypeScritp 中使用混入 #30

Closed Hilshire closed 4 years ago

Hilshire commented 5 years ago

为了方便阅读,下文将 混入的类称为mixin,将混入的目标类称为target

虽然饱受争议,但是 js 的原型继承确实有一些优势。比如说,它可以非常方便地实现混入。ts作为 js 的方言,支持混入是理所当然的。但是 Ts 却似乎在这里遇到了难题。下面的代码来自官方文档 mixins

// Disposable Mixin
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }

}

// Activatable Mixin
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

class SmartObject implements Disposable, Activatable {
    constructor() {
        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
    }

    interact() {
        this.activate();
    }

    // Disposable
    isDisposed: boolean = false;
    dispose: () => void;
    // Activatable
    isActive: boolean = false;
    activate: () => void;
    deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);

let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);

////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name));
        });
    });
}

这可能是你所能想到的最不优雅的实现了。它依靠一个自定义的 applyMixins 函数,而这个函数则把 javascript 的内部机制暴露无遗。显然不是每个人都喜欢这种做法。于是我们就有了这些:

Advanced Mixins on Typescript

Mixin Classes in TypeScript

The mixin pattern in TypeScript – all you need to know

而这里面提出的方法大致是相同的。但是,为了理解其中的用法,我们需要了解 Ts 究竟遇到了什么困难

为什么需要混入?

我们使用混入的目的在于:

  1. 希望能抽离一部分类的方法。这样,可以把这些方法混入不同的类里面。
  2. 同个 class 可以混入不同的类
  3. 这些方法依然可以访问被混入的类的公共属性。

第二是混入的重点。如果我们只需要混入一段代码,只需要把需要将 mixin 作为父类即可。

第三点则比较微妙:使用普通的继承,我们也可以实现这一点。但是这样的写法是非常脆弱的,这意味着父类知道子类里有什么。下面的代码的问题显而易见,没人会用这种方式进行继承。

// Bad
class Base {
    speak() { console.log(this.name) }
}
class Man extends Base {
    constructor(name) {
        super(name)
        this.name = name
    }
}
const man = new Man('tom')
man.speak() // tom

而在 ts 中,使用 js 风格的混入会报错,因为被混入的类并不会有目标类的属性声明。对于强类型语言这可以说是理所当然的。

而第三点的问题很容易解决。我们可以声明一个 base 类,按照 base -> mixin -> target 的方式继承。这样从语义上来说甚至更清晰一点。但是,如何方便的混入多个 mixin 就有点让人头疼了。

所以,解决的方法是什么呢?

class Point {  
   constructor(public x: number, public y: number) {}
}

class Person {  
   constructor(public name: string) {}
}

// mixin 部分开始
// 构造函数类型
type Constructor<T> = new(...args: any[]) => T;

// 包装函数,返回一个子类
// 这里 Constructor 的泛型,可以约束 mixins 扩展的对象
function Tagged<T extends Constructor<{}>>(Base: T) {  
   return class extends Base {
       _tag: string;
       constructor(...args: any[]) {
           super(...args);
           this._tag = "";
       }
   }
}
// mixin 结束。现在我们有了一个包装函数可以用来混入了。
// 就像下面这样
const TaggedPoint = Tagged(Point);

let point = new TaggedPoint(10, 20);  
point._tag = "hello";

class Customer extends Tagged(Person) {  
   accountBalance: number;
}

let customer = new Customer("Joe");  
customer._tag = "test";  
customer.accountBalance = 0;  

简单来说,我们写了一个包装函数,这个函数会创建 mixinmixin 会继承 base。当我们需要混入的时候,就使用包装函数包装一下,它将返回 mixin 的子类,也就是我们要的 target

那么,加入我们需要混入多个 mixin 呢?

// 是的,Activatable, Tagged, Timestamped 都是 mixin
const SpecialUser = Activatable(Tagged(Timestamped(User)));
const user = new SpecialUser("John Doe");