Typescript玩转设计模式 之 对象行为型模式(上)

avatar
数据可视化 @蚂蚁集团

作者简介 joey 蚂蚁金服·数据体验技术团队

继前面几篇设计模式文章之后,这篇介绍5个对象行为型设计模式。

Chain of Responsibility(职责链)

意图

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。

结构

职责链模式包含如下角色:

  • Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。
  • ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。

示例

  interface RequestData {
    name: string,
    increaseNum: number,
  }

  /**
   * 抽象处理者
   */
  abstract class Handler {
    protected next: Handler;
    setNext(next: Handler) {
      this.next = next;
    }
    abstract processRequest(request: RequestData): void;
  }

  class IdentityValidator extends Handler {
    processRequest(request: RequestData) {
      if (request.name === 'yuanfeng') {
        console.log(`${request.name} 是本公司的员工`);
        this.next.processRequest(request);
      } else {
        console.log('不是本公司员工');
      }
    }
  }

  class Manager extends Handler {
    processRequest(request: RequestData) {
      if (request.increaseNum < 300) {
        console.log('低于300的涨薪,经理直接批准了');
      } else {
        console.log(`${request.name}的涨薪要求超过了经理的权限,需要更高级别审批`);
        this.next.processRequest(request);
      }
    }
  }

  class Boss extends Handler {
    processRequest(request: RequestData) {
      console.log('hehe,想涨薪,你可以走了');
    }
  }

  function chainOfResponsibilityDemo() {
    const identityValidator = new IdentityValidator();
    const manager = new Manager();
    const boss = new Boss();
    // 构建职责链
    identityValidator.setNext(manager);
    manager.setNext(boss);

    const request: RequestData = {
      name: 'yuanfeng',
      increaseNum: 500,
    };
    identityValidator.processRequest(request);
  }

  chainOfResponsibilityDemo();

适用场景

  • 有多个对象可以处理一个请求,哪个对象处理该请求运行时自动确定,客户端只需要把请求提交到链上即可;
  • 想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求;
  • 可处理一个请求的对象集合应被动态指定;

优点

  • 降低耦合度。链中的对象不需知道链的结构;
  • 增强了职责链组织的灵活性。可以在运行时动态改变职责链;

缺点

  • 不保证被接受。一个请求可能得不到处理;
  • 如果建链不当,可能会造成循环调用,将导致系统陷入死循环;

相关模式

  • 职责链常常与Composite(组合模式)一起使用。一个对象的父对象可以作为他的后继者。

Command(命令)

意图

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

结构

命名模式包含以下角色:

  • Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
  • ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
  • Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
  • Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。

示例

简单命令

  // 点菜场景下,客户点餐后完全不需要知道做菜的厨师是谁,记载着客户点菜信息的订单就是一个命令。

  // 命令的基类,只包含了一个执行方法
  class Command {
    execute(arg?): void {}
  }

  // 厨师类,每个厨师都会做面包和肉
  class Cook {
    private name: string;
    constructor(name: string) {
      this.name = name;
    }
    makeBread() {
      console.log(`厨师 ${this.name} 在做面包`);
    }
    makeMeal() {
      console.log(`厨师 ${this.name} 在做肉`);
    }
  }

  // 简单命令只需要包含接收者和执行接口
  class SimpleCommand extends Command {
    // 接收者,在点菜系统里是厨师
    receiver: Cook;
  }

  // 做面包的命令类
  class BreadCommand extends SimpleCommand {
    constructor(cook: Cook) {
      super();
      this.receiver = cook;
    }
    execute() {
      this.receiver.makeBread();
    }
  }

  // 做肉的命令类
  class MealCommand extends SimpleCommand {
    constructor(cook: Cook) {
      super();
      this.receiver = cook;
    }
    execute() {
      this.receiver.makeMeal();
    }
  }

  // 系统启动时,将命令注册到菜单上,生成可被到处使用的命令对象
  function simpleCommandDemo(): void {
    const cook1 = new Cook('厨师1');
    const cook2 = new Cook('厨师2');

    // 生成菜单,上架销售,顾客可以选择点肉或点面包
    const breadCommand: Command = new BreadCommand(cook1);
    const mealCommand: Command = new MealCommand(cook2);

    // 客户点菜时,完全不需要知道是哪个厨师做的,只需要从菜单上点想要的菜,即下命令即可
    // 此时已经做到了命令的触发者与接收者的分离
    // 命令对象可以在整个系统中到处传递,如经过多个服务员,而不会丢失接受者的信息
    breadCommand.execute();
    mealCommand.execute();
  }

可撤销命令

相比简单命令,除了在命令对象中保存了接收者,还需要存储额外的状态信息,如接收者上次执行操作的参数

class AdvancedCommand extends Command {
  // 接收者
  ball: Ball;
  // 额外状态信息,移动的距离
  pos: number;
  // 执行命令时候,向左移动,同时记录下移动的距离
  execute(pos: number) {
    this.pos = pos;
    this.ball.moveToLeft(pos);
  }
  // 撤销时执行反向操作
  unExecute() {
    this.ball.moveToRight(this.pos);
  }
}

宏命令

同时允许多个命令,这里不需要显式的接收者,因为每个命令都已经定义了各自的接收者

class MacroCommand extends Command {
  // 保存命令列表
  cmdSet: Set<Command> = [];
  add(cmd: Command): void {
    this.cmdSet.add(cmd);
  }
  remove(cmd: Command): void {
    this.cmdSet.delete(cmd);
  }
  execute(): void {
    this.cmdSet.forEach((cmd: Command) => {
      cmd.execute();
    });
  }
}

适用场景

  • 菜单场景。抽象出待执行的动作以参数化某对象。你可用过程语言中的“回调”函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。Commond模式是回调机制的一个面向对象的替代品。
  • 在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
  • 支持取消操作。Command的Excute操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用Unexecute和Execute来实现重数不限的“取消”和“重做“。

优点

  • 将调用操作的对象与知道如何实现该操作的对象解耦;
  • 可以将多个命令装配成一个宏命令;
  • 增加新的命令很容易,因为无需改变已有的类;
  • 为请求的撤销和恢复操作提供了一种设计和实现方案;

缺点

  • 可能会导致系统里有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。

相关模式

  • 组合模式可被用来实现宏命令
  • 备忘录模式可被用来保持某个状态,命令用这一状态来做撤销

Iterator(迭代器)

意图

提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。

结构

迭代器模式包含以下角色:

  • Iterator(抽象迭代器):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的first()方法,用于访问下一个元素的next()方法,用于判断是否还有下一个元素的hasNext()方法,用于获取当前元素的currentItem()方法等,在具体迭代器中将实现这些方法。
  • ConcreteIterator(具体迭代器):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。
  • Aggregate(抽象聚合类):它用于存储和管理元素对象,声明一个createIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。
  • ConcreteAggregate(具体聚合类):它实现了在抽象聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator实例。

示例

相对于迭代器模式的经典结构,简化了实现,去除了抽象聚合类和具体聚合类的设计,同时简化了迭代器接口。

// 迭代器接口
interface Iterator {
  next(): any;
  first(): any;
  isDone(): boolean;
}

// 顺序挨个遍历数组的迭代器
class ListIterator implements Iterator {
  protected list: Array<any> = [];
  protected index: number = 0;
  constructor(list) {
    this.list = list;
  }
  first() {
    if (this.list.length) {
      return this.list[0];
    }
    return null;
  }
  next(): any {
    if (this.index < this.list.length) {
      this.index += 1;
      return this.list[this.index];
    }
    return null;
  }
  isDone(): boolean {
    return this.index >= this.list.length;
  }
}

// 跳着遍历数组的迭代器
// 由于跳着遍历和逐个遍历,区别只在于next方法,因此通过继承简单实现
class SkipIterator extends ListIterator {
  next(): any {
    if (this.index < this.list.length) {
      const nextIndex = this.index + 2;
      if (nextIndex < this.list.length) {
        this.index = nextIndex;
        return this.list[nextIndex];
      }
    }
    return null;
  }
}

// 对同一个序列,调用不同的迭代器就能实现不同的遍历方式,而不需要将迭代方法写死在序列中
// 通过迭代器的方式,将序列与遍历方法分离
function iteratorDemo(): void {
  const list = [1,2,3,4,5,6];

  // 挨个遍历
  const listIterator: Iterator = new ListIterator(list);
  while(!listIterator.isDone()) {
    const item: number = listIterator.next();
    console.log(item);
  }

  // 跳着遍历
  const skipIterator: Iterator = new SkipIterator(list);
  while(!listIterator.isDone()) {
    const item: number = skipIterator.next();
    console.log(item);
  }
}

// 内部迭代器,即在聚合内部定义的迭代器,外部调用不需要关心迭代器的具体实现,缺点是功能被固定,不易扩展
class SkipList {
  list = [];
  constructor(list: Array<any>) {
    this.list = list;
  }
  // 内部定义了遍历的规则
  // 这里实现为间隔遍历
  loop(callback) {
    if (this.list.length) {
      let index = 0;
      const nextIndex = index + 2;
      if (nextIndex < this.list.length) {
        callback(this.list[nextIndex]);
        index = nextIndex;
      }
    }
  }
}

function innerIteratorDemo(): void {
  const list = [1,2,3,4,5,6];
  const skipList = new SkipList(list);
  // 按照聚合的内部迭代器定义的规则迭代
  skipList.loop(item => {
    console.log(item);
  });
}

适用场景

  • 访问一个聚合对象的内容而无需暴露它的内部结构;
  • 支持对聚合对象的多种遍历方式;
  • 为遍历不同的聚合结构提供一个统一的接口;

优点

  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式;
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计;
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求;

缺点

  • 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性;
  • 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。

相关模式

  • 组合模式:迭代器常被应用到像组合模式这样的递归结构上;
  • 工厂方法:多态迭代器靠工厂方法来实例化适当的迭代器子类;
  • 备忘录:常与迭代器模式一起使用。迭代器可使用一个备忘录来捕获一个迭代的状态。迭代器在其内部存储备忘录;

Mediator(中介者)

意图

用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

结构

中介者模式包含以下角色:

  • Mediator(抽象中介者):它定义一个接口,该接口用于与各同事对象之间进行通信。
  • ConcreteMediator(具体中介者):它是抽象中介者的子类,通过协调各个同事对象来实现协作行为,它维持了对各个同事对象的引用。
  • Colleague(抽象同事类):它定义各个同事类公有的方法,并声明了一些抽象方法来供子类实现,同时它维持了一个对抽象中介者类的引用,其子类可以通过该引用来与中介者通信。
  • ConcreteColleague(具体同事类):它是抽象同事类的子类;每一个同事对象在需要和其他同事对象通信时,先与中介者通信,通过中介者来间接完成与其他同事类的通信;在具体同事类中实现了在抽象同事类中声明的抽象方法。

示例

租房的案例,租客和房主通过中介者联系,两者并不直接联系

  // 抽象中介者
  abstract class Mediator {
    abstract contact(message: string, person: Human): void
  }

  // 抽象同事类
  abstract class Human {
    name: string
    mediator: Mediator
    constructor(name: string, mediator: Mediator) {
      this.name = name;
      this.mediator = mediator;
    }
  }

  // 2个具体的同事类
  // 房主类
  class HouseOwner extends Human {
    contact(message: string) {
      console.log(`房主 ${this.name} 发送消息 ${message}`);
      this.mediator.contact(message, this);
    }
    getMessage(message: string) {
      console.log(`房主 ${this.name} 收到消息 ${message}`);
    }
  }

  // 租客类
  class Tenant extends Human {
    contact(message: string) {
      console.log(`租客 ${this.name} 发送消息 ${message}`);
      this.mediator.contact(message, this);
    }
    getMessage(message: string) {
      console.log(`租客 ${this.name} 收到消息 ${message}`);
    }
  }

  // 具体中介者
  class ConcreteMediator extends Mediator {
    private tenant: Tenant;
    private houseOwner: HouseOwner;
    setTenant(tenant: Tenant) {
      this.tenant = tenant;
    }
    setHouseOwner(houseOwner: HouseOwner) {
      this.houseOwner = houseOwner;
    }
    // 由中介者来设置同事对象之间的联系关系
    contact(message: string, person: Human) {
      console.log('中介传递消息');
      if (person === this.houseOwner) {
        this.tenant.getMessage(message);
      } else {
        this.houseOwner.getMessage(message);
      }
    }
  }

  function mediatorDemo() {
    const mediator = new ConcreteMediator();
    const houseOwner = new HouseOwner('财大气粗的房叔', mediator);
    const tenant = new Tenant('远峰', mediator);
    // 向中介者注册成员
    mediator.setHouseOwner(houseOwner);
    mediator.setTenant(tenant);
    // 中介的成员只需要发送信息,而不需要关心具体接受者,联系关系都维护在了中介者中
    tenant.contact('我想租房');
    houseOwner.contact('我有房,你要租吗');
  }

适用场景

  • 一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解;
  • 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象;
  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类;

优点

  • 简化了对象之间的关系,将系统的各个对象之间的相互关系进行封装,将各个同事类解耦,使系统成为松耦合系统;
  • 使控制集中化。将交互的复杂性变为中介者的复杂性;
  • 减少了子类的生成;
  • 可以减少各同事类的设计与实现;

缺点

  • 由于中介者对象封装了系统中对象之间的相互关系,导致其变得非常复杂,可能难以维护。

相关模式

  • 外观模式与中介者的不同之处在于它是对一个对象子系统进行抽象,从而提供了一个更为方便的接口。它的协议是单向的,即外观对象对这个子系统类提出请求,但反之则不行。相反,中介者提供了各同事对象不支持或不能支持的协作行为,而且协议是多向的。
  • 同事对象可使用观察者模式与中介者对象通信。

Memento(备忘录)

意图

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

结构

备忘录模式包含以下角色:

  • Originator(原发器):它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。
  • Memento(备忘录):存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。
  • Caretaker(负责人):负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。

示例

案例:一个角色在画布中移动

// 备忘录类
class Memento {
  private x: number;
  private y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  getX(): number {
    return this.x;
  }
  getY(): number {
    return this.y;
  }
}

// 原发器类
class Role {
  private x: number;
  private y: number;
  constructor(name: string, x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  // 移动到新的位置
  moveTo(x: number, y: number): Memento {
    this.x = x;
    this.y = y;
    return this.save();
  }
  save(): Memento {
    return new Memento(this.x, this.y);
  }
  // 根据备忘录回退到某一个位置
  goBack(memento: Memento) {
    this.x = memento.getX();
    this.y = memento.getY();
  }
}

// 负责人,管理所有备忘录
class HistoryRecords {
  private records = [];
  // 添加备忘录
  add(record: Memento): void {
    this.records.push(record);
  }
  // 返回备忘录
  get(index: number): Memento {
    if (this.records[index]) {
      return this.records[index];
    }
    return null;
  }
  // 清除指定位置后面的备忘录
  cleanRecordsAfter(index: number): void {
    this.records.slice(0, index + 1);
  }
}

// 客户代码
function mementoDemo() {
  const role = new Role('卡通小人', 0, 0);
  const records = new HistoryRecords();
  // 记录初始位置
  records.add(role.save());
  // 移动时添加备忘录
  role.moveTo(10, 10);
  records.add(role.save());
  role.moveTo(20, 30);
  records.add(role.save());
  // 回退到初始位置
  const GO_BACK_STEP = 0;
  const firstMemento = records.get(GO_BACK_STEP);
  role.goBack(firstMemento);
  // 清除后面的记录
  records.cleanRecordsAfter(GO_BACK_STEP);
}

适用场景

  • 必须保存一个对象在某一个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态;
  • 如果一个对象用接口来让其他对象直接得到内部状态,将会暴露对象的实现细节并破坏对象的封装性;

优点

  • 保持封装边界。使用备忘录可以避免暴露一些只应由原发器管理却又必须存储在原发器之外的信息。
  • 简化原发器。相对于把所有状态管理重任交给原发器,让客户管理他们请求的状态将会简化原发器,并且使得客户工作结束时无需通知原发器。

缺点

  • 使用备忘录代价可能很高。如果原发器在生成备忘录时必须拷贝并存储大量的信息,或者客户非常频繁地创建备忘录和恢复原发器状态,可能导致很大的开销。除非封装和恢复状态的开销不打,否则该模式可能并不适合。
  • 维护备忘录存在潜在代价。管理器负责删除它所维护的备忘录,然而管理器在运行过程中不确定会存入多少备忘录,因此可能本来很小的管理器,会产生大量的存储开销。

相关模式

  • 命令模式:命令可使用备忘录来为可撤销的操作维护状态;
  • 迭代器模式:备忘录可用于迭代;

参考文档

本文介绍了5种对象行为型模式,对后续模式感兴趣的同学可以关注专栏或者发送简历至'tao.qit####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~

原文地址:github.com/ProtoTeam/b…