前言

关键字
抽象类、抽象成员、abstract、模板模式

参考资料

  • 「百度文库」中国象棋规则英文版 Chinese Chess:链接

    notes

抽象类

抽象类
含义:仅表示一个抽象的概念,用于提取子类共有的成员
特点:不能实例化

实例化
在面向对象的编程中,把用类创建对象的过程称为实例化。

abstract
在类名前面加上 abstract 关键字,表示该类是一个抽象类。

1-3. 抽象类 - 图1image.png

  1. class Chess {}
  2. // 马
  3. class Knight extends Chess {}
  4. // 炮
  5. class cannon extends Chess {}
  6. // 兵
  7. class Pawn extends Chess {}
  8. const knight1 = new Knight();
  9. const cannon1 = new cannon();
  10. const pawn1 = new Pawn();
  11. const chess1 = new Chess();

**const chess1 = new Chess()**
Chess应该是一个抽象类,这么写应该是不被允许的,在 TS 代码中,我们可以通过 abstract 关键字来限定 Chess 是一个抽象类。

  1. abstract class Chess {}
  2. // 马
  3. class Knight extends Chess {}
  4. // 炮
  5. class cannon extends Chess {}
  6. // 兵
  7. class Pawn extends Chess {}
  8. const knight1 = new Knight();
  9. const cannon1 = new cannon();
  10. const pawn1 = new Pawn();
  11. const chess1 = new Chess(); // ×

**abstract class Chess {}**
使用 abstract 关键字对 Chess 类进行约束,表示它是一个抽象类,不能被实例化。

image.png

抽象成员

父类中,可能知道有些成员是必须存在的,但是不知道该成员的值或实现是什么。
因此,需要有一种强约束,让继承该类的子类,必须要实现该成员。

实现:给成员赋值

抽象成员
抽象成员只能出现在抽象类中
这些抽象成员必须在子类中实现

  1. abstract class Chess {
  2. x: number = 0;
  3. y: number = 0;
  4. abstract readonly name: string;
  5. }
  6. class Knight extends Chess {
  7. readonly name: string = "马";
  8. }
  9. class cannon extends Chess {
  10. readonly name: string;
  11. constructor() {
  12. super();
  13. this.name = "炮";
  14. }
  15. }
  16. class Pawn extends Chess {
  17. get name() {
  18. return "兵";
  19. }
  20. }
  21. const ma = new Knight();
  22. const pao = new cannon();
  23. const bing = new Pawn();

**get name() { ... }**
只定义 get,未定义 set,相当于写了只读修饰符 readonly

image.png

  1. abstract class Chess {
  2. x: number = 0;
  3. y: number = 0;
  4. abstract readonly name: string;
  5. abstract move(targetX: number, targetY: number): boolean;
  6. }
  7. class Knight extends Chess {
  8. readonly name: string = "马";
  9. move(targetX: number, targetY: number): boolean {
  10. this.x = targetX;
  11. this.y = targetY;
  12. console.log(`${this.name}移动成功`);
  13. return true;
  14. }
  15. }
  16. class cannon extends Chess {
  17. readonly name: string;
  18. constructor() {
  19. super();
  20. this.name = "炮";
  21. }
  22. move(targetX: number, targetY: number): boolean {
  23. this.x = targetX;
  24. this.y = targetY;
  25. console.log(`${this.name}移动成功`);
  26. return true;
  27. }
  28. }
  29. class Pawn extends Chess {
  30. get name() {
  31. return "兵";
  32. }
  33. move(targetX: number, targetY: number): boolean {
  34. this.x = targetX;
  35. this.y = targetY;
  36. console.log(`${this.name}移动成功`);
  37. return true;
  38. }
  39. }
  40. const ma = new Knight();
  41. const pao = new cannon();
  42. const bing = new Pawn();

设计模式「模板模式」

设计模式
面对一些常见的功能场景,有一些固定的、经过多年实践的成熟方法,这些方法称之为设计模式。
即:针对一些常见问题,程序员们给出的成熟解决方案。

模板模式
有些方法,所有子类实现的流程完全一致,只是流程中的某个步骤的具体实现不一致,可以将该方法提取到父类,在父类中完成整个流程的实现,遇到实现不一致的方法时,将该方法做成抽象类

abstract class Chess {
  x: number = 0;
  y: number = 0;

  abstract readonly name: string;
  abstract move(targetX: number, targetY: number): boolean;
}

class Knight extends Chess {
  readonly name: string = "马";
  move(targetX: number, targetY: number): boolean {
    console.log('1. 边界判断');
    console.log('2. 目标位置是否存在己方棋子');
    console.log('3. 棋子移动规则判断');

    this.x = targetX;
    this.y = targetY;
    console.log(`${this.name}移动成功`);
    return true;
  }
}

class cannon extends Chess {
  readonly name: string;

  constructor() {
    super();
    this.name = "炮";
  }

  move(targetX: number, targetY: number): boolean {
    console.log('1. 边界判断');
    console.log('2. 目标位置是否存在己方棋子');
    console.log('3. 棋子移动规则判断');

    this.x = targetX;
    this.y = targetY;
    console.log(`${this.name}移动成功`);
    return true;
  }
}

class Pawn extends Chess {
  get name() {
    return "兵";
  }

  move(targetX: number, targetY: number): boolean {
    console.log('1. 边界判断');
    console.log('2. 目标位置是否存在己方棋子');
    console.log('3. 棋子移动规则判断');

    this.x = targetX;
    this.y = targetY;
    console.log(`${this.name}移动成功`);
    return true;
  }
}

const ma = new Knight();
const pao = new cannon();
const bing = new Pawn();

存在的问题:目前的写法,存在大量的公共代码
目标:为了让我们的代码更加优雅,我们应该采用合适的方式将这些公共逻辑提取出来

abstract class Chess {
  x: number = 0;
  y: number = 0;

  abstract readonly name: string;
  abstract move(targetX: number, targetY: number): boolean;

  protected isOutSide(): boolean {
    console.log("1. 边界判断");
    return false;
  }

  protected targetHasAlly(): boolean {
    console.log("2. 目标位置是否存在己方棋子");
    return false;
  }

  protected finishMove(targetX: number, targetY: number): boolean {
    this.x = targetX;
    this.y = targetY;
    console.log(`${this.name}移动成功`);
    return true;
  }
}

class Knight extends Chess {
  readonly name: string = "马";
  move(targetX: number, targetY: number): boolean {
    this.isOutSide();
    this.targetHasAlly();
    console.log("3. 棋子移动规则判断");
    return this.finishMove(targetX, targetY);
  }
}

class cannon extends Chess {
  readonly name: string;

  constructor() {
    super();
    this.name = "炮";
  }

  move(targetX: number, targetY: number): boolean {
    this.isOutSide();
    this.targetHasAlly();
    console.log("3. 棋子移动规则判断");
    return this.finishMove(targetX, targetY);
  }
}

class Pawn extends Chess {
  get name() {
    return "兵";
  }

  move(targetX: number, targetY: number): boolean {
    this.isOutSide();
    this.targetHasAlly();
    console.log("3. 棋子移动规则判断");
    return this.finishMove(targetX, targetY);
  }
}

const ma = new Knight();
const pao = new cannon();
const bing = new Pawn();

可以先将公共逻辑提取到父类中
然后通过继承的方式在子类中调用这些公共逻辑

新的问题:

  1. 出现大量重复调用函数的代码(次要)
  2. 调用函数的顺序和方式,没有强约束,很容易出错(主要)

下面介绍如何使用模板模式来优化上述程序。

abstract class Chess {
  x: number = 0;
  y: number = 0;

  abstract readonly name: string;
  move(targetX: number, targetY: number): boolean {
    this._isOutSide();
    this._targetHasAlly();
    if (!this.rule(targetX, targetY)) return false;
    this._finishMove(targetX, targetY);
    return true;
  }

  private _isOutSide(): boolean {
    console.log("1. 边界判断");
    return false;
  }

  private _targetHasAlly(): boolean {
    console.log("2. 目标位置是否存在己方棋子");
    return false;
  }

  private _finishMove(targetX: number, targetY: number): boolean {
    this.x = targetX;
    this.y = targetY;
    console.log(`${this.name}移动成功`);
    return true;
  }

  protected abstract rule(targetX: number, targetY: number): boolean;
}

class Knight extends Chess {
  protected rule(targetX: number, targetY: number): boolean {
    console.log("3. 棋子移动规则判断");
    return true;
  }
  readonly name: string = "马";
}

class cannon extends Chess {
  protected rule(targetX: number, targetY: number): boolean {
    console.log("3. 棋子移动规则判断");
    return true;
  }
  readonly name: string;

  constructor() {
    super();
    this.name = "炮";
  }
}

class Pawn extends Chess {
  protected rule(targetX: number, targetY: number): boolean {
    console.log("3. 棋子移动规则判断");
    return true;
  }
  get name() {
    return "兵";
  }
}

const ma = new Knight();
const pao = new cannon();
const bing = new Pawn();

**rule**
抽象方法 rule 是唯一不同的逻辑,这部分需要由子类来实现。

上面这种写法对于后续的维护,帮助是很大的。
模拟场景:上述的 Chess 是很早之前写的一个类,现在接到个需求,得新建一个 King「将」类。根据语义,不难得知 King 应该是继承自 Chess 的。接下来我们只要写 class King extends Chess { },然后点击「快速修复」,vscode 编辑器就能快速帮我们生成必须要实现的内容。

class King extends Chess {
  name: string = "将";
  protected rule(targetX: number, targetY: number): boolean {
    console.log("3. 棋子移动规则判断");
    return true;
  }
}

image.png

image.png

image.png