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

avatar
数据可视化 @蚂蚁集团

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

本文是typescript设计模式系列文章的最后一篇,介绍了最后5个对象行为型的设计模式~

  • 观察者模式
  • 状态模式
  • 策略模式
  • 模板模式
  • 访问者模式

Observer(观察者)

意图

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

结构

观察者模式包含以下角色:

  • Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。
  • ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。
  • Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。
  • ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。

示例

推模型

目标向观察者发送关于改变的“详细信息”,而不管它们需要与否。由目标维护观察者。

  // 场景:顾客点菜后,服务员记下顾客的信息,菜做好后广播通知顾客领取

  // 观察者基类
  class Observer {
    take(msg: string): void {}
  }

  // 目标基类
  class Subject {
    set: Set<Observer> = new Set();
    // 注册回调
    add(observer: Observer): void {
      this.set.add(observer);
    }
    // 注销回调
    remove(observer: Observer): void {
      this.set.delete(observer);
    }
    // 触发所有已注册的回调
    notify(msg: string): void {
      this.set.forEach(observer => {
        observer.take(msg);
      });
    }
  }

  // 具体目标,服务员类
  class Waiter extends Subject {
    // 菜做完后通知所有注册了的顾客
    ready(): void {
      this.notify('ready');
    }
  }

  // 具体观察者,顾客类
  class Client extends Observer {
    name: string;
    // 初始化时将自身注册到目标,以便接收通知
    constructor(name: string, waiter: Waiter) {
      super();
      this.name = name;
      waiter.add(this);
    }
    take(msg: string) {
      console.log(`顾客 ${this.name} 收到了消息显示状态是<${msg}>, 到吧台领取了菜`);
    }
  }

  function observerPushDemo() {
    const waiter = new Waiter();
    // 顾客点菜后,等待服务员通知
    const bob = new Client('Bob', waiter);
    const mick = new Client('Mick', waiter);
    // 菜准备好后,服务员广播通知顾客可以到吧台领取了
    waiter.ready();
  }

拉模型

目标除了“最小通知”外什么也不送出,而在此之后由观察者显式地向目标询问细节。观察者里维护了目标对象。

  // 场景:顾客点菜后,收到通知从服务员处询问详细信息

  // 观察者基类
  class Observer {
    take(subject: Subject): void {}
  }

  // 目标基类
  class Subject {
    set: Set<Observer> = new Set();
    // 注册回调
    add(observer: Observer): void {
      this.set.add(observer);
    }
    // 注销回调
    remove(observer: Observer): void {
      this.set.delete(observer);
    }
    // 触发所有已注册的回调
    notify(): void {
      this.set.forEach(observer => {
        observer.take(this);
      });
    }
  }

  // 具体目标,服务员类
  class Waiter extends Subject {
    status = 'doing';
    // 与推模式的区别是,只发送通知,不发送详细数据
    ready(): void {
      this.status = 'ready';
      this.notify();
    }
    // 提供访问详细数据接口,让观察者访问详细数据
    getStatus(): string {
      return this.status;
    }
  }

  // 具体观察者,顾客类
  class Client extends Observer {
    name: string;
    // 初始化时将自身注册到目标,以便接收通知
    constructor(name: string, waiter: Waiter) {
      super();
      this.name = name;
      waiter.add(this);
    }
    // 与推模式的区别是,收到通知后,没有数据传入,需要从目标里读取
    take(waiter: Waiter) {
      const msg = waiter.getStatus();
      console.log(`顾客 ${this.name} 收到通知,询问服务员后发现状态是 <${msg}> 后领取了菜`);
    }
  }

  function observerPushDemo() {
    const waiter = new Waiter();
    // 顾客点菜
    const bob = new Client('Bob', waiter);
    const mick = new Client('Mick', waiter);
    // 菜准备完后,服务员通知了下所有顾客状态改变了,但没有发送内容出去,需要顾客再询问一下服务员才知道最新状态
    waiter.ready();
  }

适用场景

  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这两者封装在独立的对象中以使它们可以各自独立地改变和复用;
  • 当一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变;
  • 当一个对象必须通知其他对象,而它又不能假定其他对象是谁。换言之,你不希望这些对象是紧密耦合的;

优点

  • 目标和观察者间的抽象耦合。一个目标所知道的仅仅是它有一系列观察者,每个都符合抽象的Observer类的简单接口。目标不需要知道任何一个观察者属于哪一个具体的类。
  • 支持广播通信。目标发现的通知不需要指定它的接收者。目标对象并不关心有多少观察者对象对自己感兴趣,唯一的职责就是通知已注册的各观察者。

缺点

  • 意外的更新。因为一个观察者并不知道其他观察者的存在,它可能对改变目标的最终代价一无所知。在目标上一个看似无害的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。由此引发的问题常常难以追踪。

相关模式

  • Mediator:通过封装复杂的更新语义,ChangeManager充当目标和观察者之间的中介者。
  • Singleton:ChangeManager可使用单例模式来保证它是唯一的并且是可全局访问的。

State(状态)

意图

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

结构

状态模式包含以下角色:

  • Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
  • State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
  • ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。

示例

  // 账户有几种状态:正常,透支,受限

  // 账户类,代表状态模式中的环境
  class Account {
    private name: string;
    private state: State;
    // 余额
    private balance = 0;
    // 初始时为正常状态
    constructor(name: string) {
      this.name = name;
      this.state = new NormalState(this);
      console.log(`用户 ${this.name} 开户,余额为 ${this.balance}`);
      console.log('--------');
    }
    getBalance(): number {
      return this.balance;
    }
    setBalance(balance: number) {
      this.balance = balance;
    }
    setState(state: State) {
      this.state = state;
    }
    // 存款
    deposit(amount: number) {
      this.state.deposit(amount);
      console.log(`存款 ${amount}`);
      console.log(`余额为 ${this.balance}`);
      console.log(`账户状态为 ${this.state.getName()}`);
      console.log('--------');
    }
    // 取款
    withdraw(amount: number) {
      this.state.withdraw(amount);
      console.log(`取款 ${amount}`);
      console.log(`余额为 ${this.balance}`);
      console.log(`账户状态为 ${this.state.getName()}`);
      console.log('--------');
    }
    // 结算利息
    computeInterest() {
      this.state.computeInterest();
    }
  }

  // 状态抽象类
  abstract class State {
    private name: string;
    protected acc: Account;
    constructor(name: string) {
      this.name = name;
    }
    getName() {
      return this.name;
    }
    abstract deposit(amount: number);  
    abstract withdraw(amount: number);  
    abstract computeInterest();  
    abstract stateCheck();
  }

  // 正常状态类
  class NormalState extends State {
    acc: Account;
    constructor(acc: Account) {
      super('正常');
      this.acc = acc;
    }
    deposit(amount: number) {
      this.acc.setBalance(this.acc.getBalance() + amount);
      this.stateCheck();
    }
    withdraw(amount: number) {
      this.acc.setBalance(this.acc.getBalance() - amount);  
      this.stateCheck();
    }
    computeInterest() {
      console.log('正常状态,无须支付利息');
    }
    // 状态转换
    stateCheck() {
      if (this.acc.getBalance() > -2000 && this.acc.getBalance() <= 0) {  
          this.acc.setState(new OverdraftState(this.acc));  
      } else if (this.acc.getBalance() == -2000) {  
          this.acc.setState(new RestrictedState(this.acc));  
      } else if (this.acc.getBalance() < -2000) {  
          console.log('操作受限');  
      }
    }
  }

  // 透支状态
  class OverdraftState extends State {
    acc: Account;
    constructor(acc: Account) {
      super('透支');
      this.acc = acc;
    }
    deposit(amount: number) {
      this.acc.setBalance(this.acc.getBalance() + amount);
      this.stateCheck();
    }
    withdraw(amount: number) {
      this.acc.setBalance(this.acc.getBalance() - amount);  
      this.stateCheck();
    }
    computeInterest() {
      console.log('计算利息');
    }
    // 状态转换
    stateCheck() {
      if (this.acc.getBalance() > 0) {
        this.acc.setState(new NormalState(this.acc));
      } else if (this.acc.getBalance() == -2000) {
        this.acc.setState(new RestrictedState(this.acc));
      } else if (this.acc.getBalance() < -2000) {
        console.log('操作受限');
      }
    }
  }

  // 受限状态
  class RestrictedState extends State {
    acc: Account;
    constructor(acc: Account) {
      super('受限');
      this.acc = acc;
    }
    deposit(amount: number) {
      this.acc.setBalance(this.acc.getBalance() + amount);
      this.stateCheck();
    }
    withdraw(ammount: number) {
      console.log('账号受限,取款失败');
    }
    computeInterest() {
      console.log('计算利息');
    }
    // 状态转换
    stateCheck() {
      if (this.acc.getBalance() > 0) {  
        this.acc.setState(new NormalState(this.acc));  
      } else if (this.acc.getBalance() > -2000) {  
        this.acc.setState(new OverdraftState(this.acc));  
      }
    }
  }

  function stateDemo() {
    const acc = new Account('Bob');
    acc.deposit(1000);  
    acc.withdraw(2000);  
    acc.deposit(3000);  
    acc.withdraw(4000);  
    acc.withdraw(1000);  
    acc.computeInterest(); 
  }

适用场景

  • 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为;
  • 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。有多个操作包含这一相同的条件结构。状态模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。

优点

  • 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
  • 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
  • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

缺点

  • 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。
  • 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。

相关模式

  • 享元模式解释了何时以及怎样共享状态对象;
  • 状态对象通常是单例;

Strategy(策略模式)

意图

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

结构

策略模式包含以下角色:

  • Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
  • Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
  • ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

示例

// 火车票类:环境类
class TrainTicket {
  private price: number;
  private discount: Discount;
  constructor(price: number) {
    this.price = price;
  }
  setDiscount(discount: Discount) {
    this.discount = discount;
  }
  getPrice(): number {
    return this.discount.calculate(this.price);
  }
}

// 折扣接口
interface Discount {
  calculate(price: number): number;
}

// 学生票折扣
class StudentDiscount implements Discount {
  calculate(price: number): number {
    console.log('学生票打7折');
    return price * 0.7;
  }
}

// 儿童票折扣
class ChildDiscount implements Discount {
  calculate(price: number): number {
    console.log('儿童票打5折');
    return price * 0.5;
  }
}

// 军人票折扣
class SoldierDiscount implements Discount {
  calculate(price: number): number {
    console.log('军人免票');
    return 0;
  }
}

function strategyDemo() {
  const ticket: TrainTicket = new TrainTicket(100);

  // 从环境中获取到身份信息,然后根据身份信息获取折扣策略
  const discount: Discount = getIdentityDiscount();
  // 注入折扣策略对象
  ticket.setDiscount(discount);
  // 根据策略对象获取票价
  console.log(ticket.getPrice());
}

适用场景

  • 许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
  • 需要使用一个算法的不同变体。
  • 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法有关的数据结构。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的策略类中以代替这些条件语句。

优点

  • 提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
  • 提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。
  • 提供了一种可以替换继承关系的办法。如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是,这样一来算法的使用就和算法本身混在一起,不符合“单一职责原则”,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而且使用继承无法实现算法或行为在程序运行时的动态切换。
  • 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。
  • 提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。

缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
  • 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。
  • 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。

相关模式

  • 享元: 策略对象经常是很好的轻量级对象。

Template Method(模板方法)

意图

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

结构

模板方法包含以下角色:

  • AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。
  • ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。

示例

模板方法是基于继承的一种模式。

下面是一个组件渲染的例子,模拟React组件渲染流程。

// 组件基类
class Component {
  // 模板方法,把组件渲染的流程定义好
  setup() {
    this.componentWillMount();
    this.doRender();
    this.componentDidMount();
  }
  private doRender() {
    // 做实际的渲染工作
  }
  componentWillMount() {}
  componentDidMount() {}
}

class ComponentA extends Component {
  componentWillMount() {
    console.log('A组件即将被渲染');
  }
  componentDidMount() {
    console.log('A组件渲染完成');
  }
}

class ComponentB extends Component {
  componentWillMount() {
    console.log('B组件即将被渲染');
  }
  componentDidMount() {
    console.log('B组件渲染完成');
  }
}

// 渲染A和B组件,生命周期的流程都是相同的,已经在模板方法里定义好了的
function templateMethodDemo() {
  const compA = new ComponentA();
  compA.setup();

  const compB = new ComponentB();
  compB.setup();
}

适用场景

  • 需要控制流程的逻辑顺序时。模板方法模式广泛应用于框架设计中,以确保通过父类来控制处理流程的逻辑顺序(如框架的初始化,测试流程的设置等)

优点

  • 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。
  • 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。
  • 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。
  • 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。

缺点

  • 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。

相关模式

  • 工厂方法: 常被模板方法调用。
  • 策略模式:模板方法使用继承来改变算法的一部分。策略模式使用委托来改变整个算法。

访问者模式

意图

提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

结构

访问者模式包含以下角色:

  • Vistor(抽象访问者):抽象访问者为对象结构中每一个具体元素类ConcreteElement声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。
  • ConcreteVisitor(具体访问者):具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。
  • Element(抽象元素):抽象元素一般是抽象类或者接口,它定义一个accept()方法,该方法通常以一个抽象访问者作为参数。【稍后将介绍为什么要这样设计。】
  • ConcreteElement(具体元素):具体元素实现了accept()方法,在accept()方法中调用访问者的访问方法以便完成对一个元素的操作。
  • ObjectStructure(对象结构):对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象,如一个List对象或一个Set对象。

示例

一个公司有两种员工,正式工和临时工,他们有不同的工时和薪酬结算方法。

  // 员工接口
  interface Employee {
    accept(handler: Department): void;
  }

  // 全职员工类
  class FulltimeEmployee implements Employee {
    private name = '';
    // 全职员工按周薪计算薪酬
    private weeklyWage = 0;
    // 工作时长
    private workTime = 0;
    constructor(name: string, weeklyWage: number, workTime: number) {
      this.name = name;
      this.weeklyWage = weeklyWage;
      this.workTime = workTime;
    }
    getName(): string {
      return this.name;
    }
    getWeeklyWage(): number {
      return this.weeklyWage;
    }
    getWorkTime(): number {
      return this.workTime;
    }
    // 实现接口,调用访问者处理全职员工的方法
    accept(handler: Department) {
      handler.visitFulltime(this);
    }
  }

  // 临时员工类
  class ParttimeEmployee implements Employee {
    private name = '';
    // 临时员工按时薪计算薪酬
    private hourWage = 0;
    // 工作时长
    private workTime = 0;
    constructor(name: string, hourWage: number, workTime: number) {
      this.name = name;
      this.hourWage = hourWage;
      this.workTime = workTime;
    }
    getName(): string {
      return this.name;
    }
    getHourWage(): number {
      return this.hourWage;
    }
    getWorkTime(): number {
      return this.workTime;
    }
    // 实现接口,调用访问者处理临时工的方法
    accept(handler: Department) {
      handler.visitParttime(this);
    }
  }

  // 部门接口
  interface Department {
    visitFulltime(employee: FulltimeEmployee): void;
    visitParttime(employee: ParttimeEmployee): void;
  }

  // 具体访问者——财务部,结算薪酬实现部门接口
  class FADepartment implements Department {
    // 全职员工薪酬计算方式
    visitFulltime(employee: FulltimeEmployee) {
      const name: string = employee.getName();
      let workTime: number = employee.getWorkTime();
      let weekWage: number = employee.getWeeklyWage();
      const WEEK_WORK_TIME = 40;
      if (workTime > WEEK_WORK_TIME) {
        // 计算加班工资
        const OVER_TIME_WAGE = 100;
        weekWage = weekWage + (workTime - WEEK_WORK_TIME) * OVER_TIME_WAGE;
      } else if (workTime < WEEK_WORK_TIME) {
        if (workTime < 0) {
          workTime = 0;
        }
        // 扣款
        const CUT_PAYMENT = 80;
        weekWage = weekWage - (WEEK_WORK_TIME - workTime) * CUT_PAYMENT;
      }
      console.log(`正式员工 ${name} 实际工资为:${weekWage}`);
    }
    // 临时工薪酬计算方式
    visitParttime(employee: ParttimeEmployee) {
      const name = employee.getName();
      const hourWage = employee.getHourWage();
      const workTime = employee.getWorkTime();
      console.log(`临时工 ${name} 实际工资为:${hourWage * workTime}`);
    }
  }

  // 具体访问者——人力资源部,结算工作时间,实现部门接口
  class HRDepartment implements Department {
    // 全职员工工作时间报告
    visitFulltime(employee: FulltimeEmployee) {
      const name: string = employee.getName();
      let workTime: number = employee.getWorkTime();
      // 实际工作时间报告
      let report = `正式员工 ${name} 实际工作时间为 ${workTime} 小时`;
      const WEEK_WORK_TIME = 40;
      if (workTime > WEEK_WORK_TIME) {
        // 加班时间报告
        report = `${report},加班 ${WEEK_WORK_TIME - workTime} 小时`;
      } else if (workTime < WEEK_WORK_TIME) {
        if (workTime < 0) {
          workTime = 0;
        }
        // 请假时间报告
        report = `${report},请假 ${WEEK_WORK_TIME - workTime} 小时`;
      }
      console.log(report);
    }
    // 临时工工作时间报告
    visitParttime(employee: ParttimeEmployee) {
      const name: string = employee.getName();
      const workTime: number = employee.getWorkTime();
      console.log(`临时工 ${name} 实际工作时间为 ${workTime} 小时`);
    }
  }

  // 员工集合类
  class EmployeeList {
    list: Array<Employee> = [];
    add(employee: Employee) {
      this.list.push(employee);
    }
    // 遍历员工集合中的每一个对象
    accept(handler: Department) {
      this.list.forEach((employee: Employee) => {
        employee.accept(handler);
      });
    }
  }

  function visitorDemo() {
    const list: EmployeeList = new EmployeeList();
    const full1 = new FulltimeEmployee('Bob', 3000, 45);
    const full2 = new FulltimeEmployee('Mikel', 2000, 35);
    const full3 = new FulltimeEmployee('Joe', 4000, 40);
    const part1 = new ParttimeEmployee('Lili', 80, 20);
    const part2 = new ParttimeEmployee('Lucy', 60, 15);

    list.add(full1);
    list.add(full2);
    list.add(full3);
    list.add(part1);
    list.add(part2);

    // 财务部计算薪酬
    const faHandler = new FADepartment();
    list.accept(faHandler);

    // 人力资源部出工作报告
    const hrHandler = new HRDepartment();
    list.accept(hrHandler);
  }

适用场景

  • 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。

优点

  • 增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。
  • 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
  • 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。

缺点

  • 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”的要求。
  • 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。

相关模式

  • 组合模式:访问者可以用于对一个由组合模式定义的对象结构进行操作;

参考文档

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

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