在我的项目中,我在使用 mixin 的过程中经历了三个阶段。你的问题正好是我第三阶段的问题,所以我提供我的三阶段解决方案,希望对你有所帮助。
第一阶段:我只需要一个简单的mixin,我不在乎它会被添加到哪个目标类中。对目标类的规范没有要求,所以代码很简单:
type Constructor<T = {}> = new (...args: any[]) => T;
function ActivatableMixin<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActivated = false;
activate() {
this.isActivated = true;
this.deactivate();
}
deactivate() {
this.isActivated = false;
}
test() {
console.log(this.isActivated ? 'activated' : 'not activated');
}
};
}
class User {
name: string;
me: string;
constructor(name: string) {
this.name = name;
this.me = "hi";
}
}
const ActivatableUser = ActivatableMixin(User);
// Instantiate the new `ActivatableUser` class
const user = new ActivatableUser("John Doe");
// Initially, the `isActivated` property is false
console.log(user.isActivated);
// Activate the user
user.activate();
user.test();
// Now, `isActivated` is true
console.log(user.isActivated);
第二阶段:我需要能够在mixin中访问目标类的一些成员(属性或方法),所以我也要求目标类必须包含这些成员。如果一个类不包含这些成员,不要指望使用我的 mixin。这需要在第一阶段代码中添加一些规范:
type Constructor<T = {}> = new (...args: any[]) => T;
/**
* the target class must fit the Activatable Interface to use the mixin
*/
interface Activatable {
me: string
}
/**
* Note that the mixin target class specification is specified here
* <TBase extends Constructor<Activatable>>
* instead of
* <TBase extends Constructor>
*/
function ActivatableMixin<TBase extends Constructor<Activatable>>(Base: TBase) {
return class extends Base {
isActivated = false;
activate() {
this.isActivated = true;
this.deactivate();
}
deactivate() {
this.isActivated = false;
}
test() {
// with target class specification we can access members form specification
console.log(this.me)
console.log(this.isActivated ? 'activated' : 'not activated');
}
};
}
class UserNoMe {
name: string;
constructor(name: string) {
this.name = name;
}
}
// UserNoMe does not fit Activatable so can not be mixied
const ActivatableUserNoMe = ActivatableMixin(UserNoMe); // ts2345 error
class UserHasMe {
name: string;
me: string;
constructor(name: string) {
this.name = name;
this.me = "hi";
}
}
// UserHasMe fits Activatable so can be mixied
const ActivatableUser = ActivatableMixin(UserHasMe);
// Instantiate the new `ActivatableUser` class
const user = new ActivatableUser("John Doe");
// Initially, the `isActivated` property is false
console.log(user.isActivated);
// Activate the user
user.activate();
user.test();
// Now, `isActivated` is true
console.log(user.isActivated);
第三阶段:我需要在mixin中访问目标类型中的protected成员。这时候,我的第一个想法是在规范界面中将成员改为保护模式。但是接口的成员不能使用protected修饰符(这与接口的职责相违背),所以需要其他方法。最后,我使用以下方法来实现:
type Constructor<T = {}> = new (...args: any[]) => T;
/**
* The protected members that need to be accessed both in the class
* and in the mixin are extracted into this class. This class does not
* contain any implementation. In fact, it acts as an interface (because
* the interface cannot contain protected members). Next, the mixin
* specification and the target class should inherit this class.
*
*/
class ProtectedMembersCrossClassAndMixin {
protected me: string;
protected saySomething() {
throw new Error('must be implement by ')
}
}
/**
* the target class must fit the Activatable Interface to use the mixin
* with protected menmbers derived from ProtectedMembersCrossClassAndMixin
*/
interface Activatable extends ProtectedMembersCrossClassAndMixin {
you: string
}
/**
* Note that the mixin target class specification is specified here
* <TBase extends Constructor<Activatable>>
* instead of
* <TBase extends Constructor>
*/
function ActivatableMixin<TBase extends Constructor<Activatable>>(Base: TBase) {
return class extends Base {
isActivated = false;
activate() {
this.isActivated = true;
this.deactivate();
}
deactivate() {
this.isActivated = false;
}
test() {
// with target class specification we can access members within specification
console.log(this.you)
// alse access protected members
console.log(this.me);
console.log(this.isActivated ? 'activated' : 'not activated');
}
/**
* also access or override protected method members
*/
saySomething() {
super.saySomething();
console.log('hello activatable mixed');
}
};
}
class UserNo {
name: string;
you: string;
protected me: string;
constructor(name: string) {
this.name = name;
}
protected saySomething() {
console.log('oh my');
}
}
// UserNo does fit Activatable
// but not derive from ProtectedMembersCrossClassAndMixin
// so can not be mixied
const ActivatableUserNoMe = ActivatableMixin(UserNo); // ts2345 error
// this one can be mixed
class UserHas extends ProtectedMembersCrossClassAndMixin {
name: string;
you: string;
protected me: string;
constructor(name: string) {
super();
this.name = name;
this.me = "hi";
}
/**
* typescript will not warn you if you forget to implement this method
* because typescript thought it's implemented in ProtectedMembersCrossClassAndMixin
* which is not really
*/
protected saySomething() {
console.log('hello');
}
}
// UserHas fits Activatable so can be mixied
const ActivatableUser = ActivatableMixin(UserHas);
// Instantiate the new `ActivatableUser` class
const user = new ActivatableUser("John Doe");
// Initially, the `isActivated` property is false
console.log(user.isActivated);
// Activate the user
user.activate();
user.test();
// Now, `isActivated` is true
console.log(user.isActivated);
ProtectedMembersCrossClassAndMixin 的注意事项:
- 类的代码与接口的代码不同。编译后它仍然存在,所以我们应该保持类最小化,所以不要包含任何实现代码。
- 只有真正需要保护的才放到这个类里,其他的还是放到接口里
- 还有另一个层次的继承。
- 别忘了:基类和mixin规范接口的两端都应该扩展这个类
- 从此类继承的子类可能会忘记实现那些继承的成员
- 当你真正知道你需要它并了解它的缺陷时使用它