观察者模式

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

举一个生活中例子"聊天群",每位群员都可认为是观察者被观察者,每位群友发布的消息会被其他群友实时获取并自行判断是否参与回复。

观察者模式的关键代码是在类中使用一个数组存放各自的观察者们提供的函数。在发布者发布内容时循环调用这些函数。

下面的代码以老师和学生围绕分数为例子解释观察者模式,老师作为公布成绩的一方,他会被其他同学所观察

示例代码:

  1. type classifyType = 'student' | 'teacher';
  2. type fnType = (score: number) => void;
  3. class Person {
  4. private readonly name!: string;
  5. private readonly classify: classifyType;
  6. private list: Array<fnType>;
  7. constructor(name: string, classify: classifyType) {
  8. this.name = name;
  9. this.classify = classify;
  10. this.list = [];
  11. }
  12. // 每个人发布消息时只需维护自己“被订阅”列表。
  13. publish(score: number) {
  14. console.log(this.classify + this.name + '发布了信息');
  15. console.log('当前' + this.classify + this.name + '有' + this.list.length + '人订阅他的消息!');
  16. this.list.forEach(function (item) {
  17. item(score);
  18. });
  19. }
  20. subscribe(target: Person, fn: fnType) {
  21. console.log(`${ this.classify }:${ this.name }订阅了${ target.classify }:${ target.name }的消息。`);
  22. target.list.push(fn);
  23. }
  24. }
  25. const p1 = new Person('李', 'teacher');
  26. const p2 = new Person('张', 'student');
  27. const p3 = new Person('王','student');
  28. const p4 = new Person('古','student');
  29. // 订阅
  30. p2.subscribe(p1,(score) => {
  31. if(score > 60) {
  32. console.log('我是学生张,我对60分以上的内容敢兴趣!');
  33. }
  34. })
  35. p3.subscribe(p1,(score: number) => {
  36. if(score > 70) {
  37. console.log('我是学生王,我对70分以上的内容敢兴趣!');
  38. }
  39. });
  40. p4.subscribe(p1,(score: number) => {
  41. if(score > 80) {
  42. console.log('我是学生古,我对80分以上的内容敢兴趣!');
  43. }
  44. })
  45. // 发布
  46. p1.publish(62);

订阅-发布模式

上文了解到观察者模式在理论上是说一个对象观察另一个对象,那这个观察是怎么观察的呢?如何用代码去表示在一个对象更新时自动通知到所有依赖它的对象呢?这就要说到订阅-发布模式了!

阅读上面的代码,可以发现观察者模式就是用订阅-发布来实现的,学生订阅老师,老师发布消息。那为什么要单独提出一个叫订阅-发布的模式呢?

再说上面的代码。我们只创建了3个学生来订阅老师的消息,这看起来和运行起来比较简单,要是极端点,有上亿个学生呢?那老师在发布消息的时候要挨个调用所有列表内的函数,这看起来就有点吓人了。而且以现在代码解耦的思想下,由自己维护一个列表,又麻烦又容易出错。于是,升级版的订阅发布模式诞生了。

这种升级版的思想是把原本由自己维护的列表交由一个调度中心处理。这个过程可以类比为以前我们写了一篇博客要自己挨个通知关注自己的粉丝,现在在掘金上写的博客发布后就啥都不用管了,掘金会帮我们通知粉丝列表内的所有粉丝,掘金在这个例子起调度中心的作用。

现在开始把上面学生老师的这个例子升级一下:

  1. type classifyType = 'student' | 'teacher';
  2. type fnType = (score: number) => void;
  3. type topicType = { [key: string]: Array<fnType> }
  4. const dispatchCenter = {
  5. // 使用 hash table 的方式存储不同的主题内容
  6. topics: {} as topicType,
  7. subscribe(topic: string, fn: fnType) {
  8. if ( !this.topics[topic] ) {
  9. this.topics[topic] = [];
  10. }
  11. this.topics[topic].push(fn);
  12. },
  13. publish(topic: string, score: number): boolean {
  14. if ( !this.topics[topic] ) {
  15. return false;
  16. }
  17. for (let fn of this.topics[topic]) {
  18. fn(score);
  19. }
  20. return true;
  21. }
  22. };
  23. class Person {
  24. private readonly name!: string;
  25. private readonly classify: classifyType;
  26. constructor(name: string, classify: classifyType) {
  27. this.name = name;
  28. this.classify = classify;
  29. }
  30. publish(topic: string, score: number) {
  31. console.log(this.classify + this.name + `发布了${ topic }成绩的信息`);
  32. if ( !dispatchCenter.topics[topic] ) {
  33. console.log('这个消息无人订阅!');
  34. } else {
  35. console.log(`当前有${ dispatchCenter.topics[topic].length }人关注了${ topic }成绩的信息!`);
  36. }
  37. dispatchCenter.publish(topic, score);
  38. }
  39. subscribe(topic: string, fn: fnType) {
  40. console.log(`${ this.classify }:${ this.name }订阅了${ topic }成绩的消息。`);
  41. dispatchCenter.subscribe(topic, fn);
  42. }
  43. }
  44. const p1 = new Person('李', 'teacher');
  45. const p2 = new Person('张', 'student');
  46. const p3 = new Person('王', 'student');
  47. const p4 = new Person('古', 'student');
  48. // 订阅
  49. p2.subscribe('数学', (score) => {
  50. if ( score >= 60 ) {
  51. console.log('我是学生张,我关注60分以上的数学成绩!');
  52. }
  53. });
  54. p3.subscribe('语文', (score: number) => {
  55. if ( score >= 70 ) {
  56. console.log('我是学生王,我关注70分以上的语文成绩!');
  57. }
  58. });
  59. p4.subscribe('数学', (score: number) => {
  60. if ( score >= 80 ) {
  61. console.log('我是学生古,我关注80分以上的数学成绩!');
  62. }
  63. });
  64. // 发布
  65. p1.publish('数学', 66);

使用调度中心,可以进一步解耦学生和老师的关系,学生不用再关注具体某一位老师发布的信息,只用关注自己想要的关注的内容即可。这就像学校的成绩公告板,各科成绩分栏公示,学生要看哪科成绩就去哪科成绩的公告栏看就行了。

如果把这个公告栏改为手机app,那么可以在这个控制中心增加更多具有权限限制或筛选的功能供老师发布信息时使用。比如只发布女生的成绩,那么男生就无法获取到相应的信息,当然这只是一个举例。

总结: 订阅发布模式是观察者模式的优化版,根据代码的实现方式,也可以认为它们就是同一个模式的不同叫法。它们的作用都是为了解耦,以及在对象更新时通知依赖它的其他对象。

大多数人都喜欢称这个为发布-订阅模式,而我认为根据代码事实,都是先订阅后发布,都没有人订阅,发布也就没意义了,所以我个人觉得将这种模式称之为订阅-发布模式更为合适!

本文源代码