Skip to content

行为型模式 #9

Description

@edwineo

策略模式

概念:策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

概念中的策略对象就是我们平常的 if 中的逻辑,我们将不同功能的函数单独封装后,再不同的逻辑执行不同功能的函数,那如何根据不同逻辑执行不同功能的函数呢(除了大量的if-else)?

使用映射来实现,不同的策略对象则执行不同的功能函数,这就是策略模式。

示例:

// 定义一个询价处理器对象
const priceProcessor = {
  pre(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  },
  onSale(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  },
  back(originPrice) {
    if (originPrice >= 200) {
      return originPrice - 50;
    }
    return originPrice;
  },
  fresh(originPrice) {
    return originPrice * 0.5;
  },
};

// 询价函数
function askPrice(tag, originPrice) {
  return priceProcessor[tag](originPrice)
}

状态模式

概念:状态模式中,类的行为是基于它的状态改变的,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

状态模式和策略模式基本上是相似的,它们都封装行为、都通过委托来实现行为分发。

区别如下:

策略模式的行为函数不依赖主体,互相平行;

状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。

观察者模式

概念:观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

观察者模式,是所有 JavaScript 设计模式中使用频率最高的。

观察者模式有一个“别名”,叫发布 - 订阅模式(之所以别名加了引号,是因为两者之间存在着细微的差异,下面会讲到这点)。这个别名非常形象地诠释了观察者模式里两个核心的角色要素——“发布者”与“订阅者”

简单实现

在观察者模式里,至少有两个关键角色是一定要出现的——发布者和订阅者。用面向对象的方式表达的话,那就是要有两个类

发布者类的功能:增加订阅者;通知订阅者;移除订阅者

// 定义发布者类
class Publisher {
  constructor() {
    this.observers = []
    console.log('Publisher created')
  }
  // 增加订阅者
  add(observer) {
    console.log('Publisher.add invoked')
    this.observers.push(observer)
  }
  // 移除订阅者
  remove(observer) {
    console.log('Publisher.remove invoked')
    this.observers.forEach((item, i) => {
      if (item === observer) {
        this.observers.splice(i, 1)
      }
    })
  }
  // 通知所有订阅者
  notify() {
    console.log('Publisher.notify invoked')
    this.observers.forEach((observer) => {
      observer.update(this) // 一个个去执行订阅者的update函数
    })
  }
}

订阅者类的功能:被通知,被执行

// 定义订阅者类
class Observer {
    constructor() {
        console.log('Observer created')
    }

    update() {
        console.log('Observer.update invoked')
    }
}

这就是基本的发布者和订阅者类的设计和编写。接下来以一个例子,来看下他们是如何执行的

有以下场景,如果产品经理是发布者,前端后端测试都是订阅者,那么产品经理修改产品文档时则会通知这些订阅者查看文档的更新,首先产品经理需要继承 Publisher 这个类:

// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
    constructor() {
        super()
        // 初始化需求文档
        this.prdState = null
        // 产品经理还没有拉群,开发群目前为空
        this.observers = []
        console.log('PrdPublisher created')
    }
    
    // 该方法用于获取当前的prdState
    getState() {
        console.log('PrdPublisher.getState invoked')
        return this.prdState
    }
    
    // 该方法用于改变prdState的值
    setState(state) {
        console.log('PrdPublisher.setState invoked')
        // prd的值发生改变
        this.prdState = state
        // 需求文档变更,立刻通知所有开发者
        this.notify()
    }
}

作为订阅方,也需要继承 Observer 类

class DeveloperObserver extends Observer {
    constructor() {
        super()
        // 需求文档一开始还不存在,prd初始为空对象
        this.prdState = {}
        console.log('DeveloperObserver created')
    }
    
    // 重写一个具体的update方法
    update(publisher) {
        console.log('DeveloperObserver.update invoked')
        // 更新需求文档
        this.prdState = publisher.getState()
        // 调用工作函数
        this.work()
    }
    
    // work方法,用于工作
    work() {
        // 获取需求文档
        const prd = this.prdState
        // 开始基于需求文档提供的信息工作。。。
        ...
        console.log('996 begins...')
    }
}

类定义好之后,接下来就是整个执行流程:

// 创建订阅者:前端开发小A
const A = new DeveloperObserver()
// 创建订阅者:后端开发小B
const B = new DeveloperObserver()
// 创建订阅者:测试同学小C
const C = new DeveloperObserver()
// 产品经理
const productManager = new PrdPublisher()
// 需求文档出现了
const prd = {
    // 具体的需求内容
    ...
}
// 产品经理开始拉群
productManager.add(A)
productManager.add(B)
productManager.add(C)
// 产品经理发送了需求文档,并@了所有人
productManager.setState(prd)

以上就是观察者模式的一个简单实现

生产实践

项目中常见的观察者模式有:

  1. Vue的响应式系统
  2. Event Bus/Event Emitter(全局事件总线)

观察者模式和发布-订阅模式之间的区别

发布者直接触及订阅者的操作,叫观察者模式。

发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫发布-订阅模式。

总结

为什么要有观察者模式?观察者模式,解决的其实是模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。但观察者模式仅仅是减少了耦合,并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用,两者之间还是有着说不清、道不明的关系。

而发布-订阅模式,则是快刀斩乱麻了——发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。发布-订阅模式下,实现了完全地解耦。

但这并不意味着,发布-订阅模式就比观察者模式“高级”。在实际开发中,我们的模块解耦诉求并非总是需要它们完全解耦。如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么我们使用观察者模式就足够了。而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。

迭代器模式

概念:迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。

迭代器模式是设计模式中少有的目的性极强的模式。所谓“目的性极强”就是说它不操心别的,它就解决这一个问题——遍历。迭代器模式比较特别,它非常重要,重要到语言和框架都争着抢着已经帮我们实现了。

说到遍历,可能会想到数组的遍历,JS已经有很多方法来实现数组的遍历,例如 forEach,但这种方法只能遍历数组,像类数组等其他结构或集合则不能遍历。

而迭代器的定义是我们遍历集合的同时,我们不需要关心集合的内部结构,也就是一个通用的迭代器。

例如JS中的四种集合类型:Array、Object、Map、Set

我们如果要用同一套规则去遍历它们,则要使用 ES6 推出的一套统一的接口机制——迭代器(Iterator)。

ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的next方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。

在ES6中,针对Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历,原理都是一样的。例如数组,之所以能够按顺序一次一次地拿到数组里的每一个成员,是因为我们借助数组的Symbol.iterator生成了它对应的迭代器对象,通过反复调用迭代器对象的next方法访问了数组成员,像这样:

const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()

可以看出,for...of...其实就是iterator循环调用换了种写法。在ES6中我们之所以能够用for...of...遍历各种各种的集合,全靠迭代器模式在背后给力。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions