前言
借助一个模拟的场景来介绍接口的更多用法。
认识到接口虽然可以帮我们编写类提供约束,但是由于接口并不会生成到编译结果中,所以有时在使用接口时,ts 没法像 Java、C++ 写起来那样自然。
notes
接口用于约束类、对象、函数,是一个类型契约。
模拟一个场景
有一个马戏团,马戏团中有很多动物,包括:狮子、老虎、猴子、狗。
这些动物都具有的相同属性
- 共同的特征:名字、年龄、种类名称
- 共同的方法:打招呼
马戏团中有以下常见的技能
- 火圈表演「IFireShow」:单火圈(singleFire)、双火圈(doubleFire)
- 平衡表演「IWisdomShow」:独木桥(dumuqiao)、走钢丝(zougangsi)
- 智慧表演「IBalanceShow」:算术题(suanshuti)、跳舞(dance)
马戏团中的动物都各自有各自的技能,狮子和老虎能进行火圈表演,猴子能进行平衡表演,狗能进行智慧表演。
未使用接口
不使用接口实现时,存在的一些问题:
- 对成员函数(能力)没有强约束力
- 容易将类型和能力耦合在一起
上面提及的这两个问题,本质原因在于系统中缺少对能力的定义。
接口的含义
面向对象领域中的接口语义:表达了某个类是否拥有某种能力。
某个类具有某种能力,其实就是实现(implements)了某种接口。
implements
上面提到的技能「火圈表演」、「平衡表演」、「智慧表演」,其实就是能力,它们应该定义为接口。
class xxx implements xxx
class xxx implements xxx, xxx
class xxx extends xxx implements xxx
class xxx extends xxx implements xxx, xxx
接口命名规范
通常在命名时,会以首字母 I 开头。
类型保护函数
通过调用该函数,会触发 TS 的类型保护,该函数必须返回 boolean。
function func (xxx: 类型): xxx is 接口 {
// ...
return true; // true or false
}
func(xxx)
结果
- true 表示传入的 xxx 实现了接口
- false 表示 xxx 没有实现接口
接口和类型别名的最大区别
接口可以被类实现
类型别名不能被类实现
接口继承类
接口可以继承类,表示该类的所有成员都在接口中。
其它面向对象语言不行,这是 ts 中特有的。
codes
export abstract class Animal {
abstract type: string;
constructor(public name: string, public age: number) {}
sayHello() {
console.log(
`各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
);
}
}
将共同的东西提取到抽象类 Animal 中。
export abstract class Animal {
abstract type: string;
constructor(public name: string, public age: number) {}
sayHello() {
console.log(
`各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
);
}
}
export class Lion extends Animal {
type: string = "狮子"
}
export class Tiger extends Animal {
type: string = "老虎"
}
export class Monkey extends Animal {
type: string = "猴子"
}
export class Dog extends Animal {
type: string = "狗";
}
这些动物实例继承自抽象类 Animal
必须要实现该抽象类中规定的抽象成员 type
export abstract class Animal {
abstract type: string;
constructor(public name: string, public age: number) {}
sayHello() {
console.log(
`各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
);
}
}
export class Lion extends Animal {
type: string = "狮子";
singleFire() {
console.log(`${this.name}完成了单火圈表演`);
}
doubleFire() {
console.log(`${this.name}完成了双火圈表演`);
}
}
export class Tiger extends Animal {
type: string = "老虎";
singleFire() {
console.log(`${this.name}完成了单火圈表演`);
}
doubleFire() {
console.log(`${this.name}完成了双火圈表演`);
}
}
export class Monkey extends Animal {
type: string = "猴子";
dumuqiao() {
console.log(`${this.name}完成了独木桥表演`);
}
zougangsi() {
console.log(`${this.name}完成了走钢丝表演`);
}
}
export class Dog extends Animal {
type: string = "狗";
suanshuti() {
console.log(`${this.name}完成了算术题表演`);
}
dance() {
console.log(`${this.name}完成了跳舞表演`);
}
}
下面基于这个 animals.ts 来介绍我们没有使用接口来对“能力”进行约束所导致的一些问题。
未使用接口
import { Animal, Dog, Lion, Monkey, Tiger } from "./animals";
const animals: Animal[] = [
new Lion("狮子", 1),
new Tiger("老虎", 2),
new Monkey("猴子", 3),
new Dog("狗1", 4),
new Dog("狗2", 5),
];
// 要求1 所有动物打招呼
animals.forEach(a => a.sayHello());
// 要求2 所有具备「火圈表演」能力的动物进行「火拳表演」
// ...
由于打招呼这个方法,是公共父类 Animal 身上的方法,所有的动物都会继承,都拥有该功能。
所以对于要求1,暂时还是没有啥问题的,写起来也很正常。是否使用接口对此影响不大。
要求2 所有具备「火圈表演」能力的动物进行「火拳表演」
狮子和老虎都能进行火圈表演能力。由于此时没有接口,现在存在以下问题:
- 没法约束 Lion、Tiger 都含有 singleFire、doubleFire 方法
- 不知道哪些动物具备「火圈表演」的能力
对于问题1,我们没法强行进行约束,无非就是通过文档或者注释的形式,来简单标注一下。
对于问题2,暂时只能凭借记忆里,在前面我们提到,狮子和老虎是具备「火圈表演」能力的,我们可以借此来判断。
// 需求2 所有具备「火圈表演」能力的动物进行「火拳表演」
animals.forEach((a) => {
if (a instanceof Lion || a instanceof Tiger) {
a.singleFire();
a.doubleFire();
}
});
**a instanceof Lion || a instanceof Tiger**
这么写的问题:将「类型」和「能力」耦合在了一起
倘若有一天,马戏团中的狮子生病了,无法进行「火圈表演」,那么此时就需要将该能力给去掉。 按理来说,我们只需要修改 Lion 类,去掉它身上的 singleFire、doubleFire 函数即可「修改1」。 但是,按照上面这种写法,我们还需要修改“要求2”的写法:
a instanceof Lion || a instanceof Tiger
改为a instanceof Tiger
「修改2」 在实际开发中,如果真遇到这种情况,那么「修改2」可远远不会只有一个地方需要改,很可能是多个文件都需要修改。
使用接口
export interface IFireShow {
singleFire(): void;
doubleFire(): void;
}
export interface IWisdomShow {
suanshuti(): void;
dance(): void;
}
export interface IBlanceShow {
dumuqiao(): void;
zougangsi(): void;
}
import { IBlanceShow, IFireShow, IWisdomShow } from "./interfaces";
export abstract class Animal {
abstract type: string;
constructor(public name: string, public age: number) {}
sayHello() {
console.log(
`各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
);
}
}
export class Lion extends Animal implements IFireShow {
type: string = "狮子";
singleFire() {
console.log(`${this.name}完成了单火圈表演`);
}
doubleFire() {
console.log(`${this.name}完成了双火圈表演`);
}
}
export class Tiger extends Animal implements IFireShow {
type: string = "老虎";
singleFire() {
console.log(`${this.name}完成了单火圈表演`);
}
doubleFire() {
console.log(`${this.name}完成了双火圈表演`);
}
}
export class Monkey extends Animal implements IBlanceShow {
type: string = "猴子";
dumuqiao() {
console.log(`${this.name}完成了独木桥表演`);
}
zougangsi() {
console.log(`${this.name}完成了走钢丝表演`);
}
}
export class Dog extends Animal implements IWisdomShow {
type: string = "狗";
suanshuti() {
console.log(`${this.name}完成了算术题表演`);
}
dance() {
console.log(`${this.name}完成了跳舞表演`);
}
}
要求2 所有具备「火圈表演」能力的动物进行「火拳表演」 狮子和老虎都能进行火圈表演能力。由于此时没有接口,现在存在以下问题:
- 没法约束 Lion、Tiger 都含有 singleFire、doubleFire 方法
- 不知道哪些动物具备「火圈表演」的能力
现在已经有接口了,再回看前面提到的这两个问题。
对于问题1,现在添加上接口实现后,就有了强约束。
对于问题2,需要借助类型保护函数来实现。
// 需求2 所有具备「火圈表演」能力的动物进行「火拳表演」
animals.forEach((a) => {
if (a instanceof IFireShow) {
a.singleFire();
a.doubleFire();
}
});
**a instanceof IFireShow**
如果是在 Java、C++ 中,这种写法是完全 OK 的。可以直接通过判断 a 实例是否继承接口 IFireShow 来判断 a 是否具备 IFireShow 能力。
但是 ts 中没法这么写,因为在 ts 中,接口并不会生成到编译结果中。由于接口并不会生成到编译结果中,所以 TypeScript 没法像 Java、C++ 那样,让我们通过接口直接判断某个实例是否具备某个「能力」
// 需求2 所有具备「火圈表演」能力的动物进行「火拳表演」
const hasFireShow = (ani: object): ani is IFireShow => {
if (
(ani as unknown as IFireShow).singleFire &&
(ani as unknown as IFireShow).doubleFire
) {
return true;
} else {
return false;
}
};
animals.forEach((a) => {
if (hasFireShow(a)) {
a.singleFire();
a.doubleFire();
}
});
a 的类型被推断为 Animal & IFireShow 类型,只要是 Animal、IFireShow 中有的,实例 a 都能访问。
**ani is IFireShow**
如果函数的返回值是 true,那么表示类型保护函数认为 ani 是 IFireShow 类型
否则认为 ani 不是 IFireShow 类型
**ani as unknown as IFireShow**
断言 ani 是 IFireShow 类型。
unknown,该类型主要是为了保证类型安全出现的。在这里其实可以将 as unknown
给去掉,并不影响功能。
const hasFireShow = (ani) => {
if (ani.singleFire &&
ani.doubleFire) {
return true;
} else {
return false;
}
};
animals.forEach((a) => {
if (hasFireShow(a)) {
a.singleFire();
a.doubleFire();
}
});
需求3:去掉老虎的「火圈表演」技能,狗学会的「火圈表演」技能,让所有具备「火圈表演」技能的动物上台表演。
import { IBlanceShow, IFireShow, IWisdomShow } from "./interfaces";
export abstract class Animal {
abstract type: string;
constructor(public name: string, public age: number) {}
sayHello() {
console.log(
`各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`
);
}
}
export class Lion extends Animal implements IFireShow {
type: string = "狮子";
singleFire() {
console.log(`${this.name}完成了单火圈表演`);
}
doubleFire() {
console.log(`${this.name}完成了双火圈表演`);
}
}
export class Tiger extends Animal {
type: string = "老虎";
singleFire() {
console.log(`${this.name}完成了单火圈表演`);
}
doubleFire() {
console.log(`${this.name}完成了双火圈表演`);
}
}
export class Monkey extends Animal implements IBlanceShow {
type: string = "猴子";
dumuqiao() {
console.log(`${this.name}完成了独木桥表演`);
}
zougangsi() {
console.log(`${this.name}完成了走钢丝表演`);
}
}
export class Dog extends Animal implements IWisdomShow, IFireShow {
type: string = "狗";
singleFire() {
console.log(`${this.name}完成了单火圈表演`);
}
doubleFire() {
console.log(`${this.name}完成了双火圈表演`);
}
suanshuti() {
console.log(`${this.name}完成了算术题表演`);
}
dance() {
console.log(`${this.name}完成了跳舞表演`);
}
}
该打印结果和预期的结果可能并不一致,上面提到了,需要将老虎的火圈表演能力给去掉,但是结果中依旧有老虎。
由此可见:implements IFireShow
仅仅是对编写类时提供约束class Tiger extends Animal implements IFireShow
这种写法表示的含义,说得更加确切一些应该是:Tiger 类继承自 Animal 类,并且必须实现 IFireShow 接口。class Tiger extends Animal
这种写法也是可以实现 IFireShow 接口的,但是并没有强制约束。
为实现目前的需求3,我们还需要将 Tiger 中的 singleFire、doubleFire 给去掉才行。
接口继承类
class A {
a1: string = "";
a2: string = "";
}
class B {
b1: number = 0;
b2: number = 0;
}
interface C extends A, B {}
const c: C = {
a1: "1",
a2: "2",
b1: 1,
b2: 2,
};