设计模式

人文历史

“设计模式”这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中。
1977 年建筑界已经有人提出了设计模式。1990 年软件工程界才开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。
1995 年,GoF四人合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了 23 种设计模式,从此树立了软件设计模式领域的里程碑,所以人们也称其为「GoF设计模式」。
直到今天,狭义的设计模式还是本教程中所介绍的 23 种经典设计模式。

设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
当然,软件设计模式只是一个引导,在实际的软件开发中,必须根据具体的需求来选择:对于简单的程序,可能写一个简单的算法要比引入某种设计模式更加容易;但是对于大型项目开发或者框架设计,用设计模式来组织代码显然更好。
本笔记虽然用js去写的,但是设计模式并不是 js 的专利,它同样适用于 C++、C#、Java等其它面向对象的编程语言。
因为js不是典型的面向对象的编程语言,所以本教程还会以接近于传统语言java、php的typescript重写一遍实现,这样的话无论是前端还是后端都不会太陌生。
另外要注意的是,设计模式多以UML图来展示其运作方式,可以去了解一下相关知识

模式分类

设计模式有两种分类方法

1. 根据目的来分

根据目的、用途的不同,分为创建性模式、结构性模式、行为性模式3 种。

  1. 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF 中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
  2. 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF 中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
  3. 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。GoF 中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。

2. 根据作用范围来分

根据处理范围不同是用于类上还是主要用于对象上来分,可分为类模式和对象模式2种。

  1. 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。GoF中的工厂方法、(类)适配器、模板方法、解释器属于该模式。
  2. 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。GoF 中除了以上 4 种,其他的都是对象模式。

3. GoF的23种设计模式的功能

  • 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
  • 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
  • 工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
  • 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
  • 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
  • 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
  • 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
  • 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
  • 装饰(Decorator)模式:动态的给对象增加一些职责,即增加其额外的功能。
  • 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
  • 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
  • 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
  • 模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
  • 策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
  • 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
  • 职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
  • 状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
  • 观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
  • 中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
  • 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
  • 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
  • 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
  • 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器

行为型模式-观察者模式

在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如,股票价格与股民、微信公众号与微信用户、气象局的天气预报与听众。
在软件世界也是这样,这些如果用观察者模式来实现就非常方便。

模式的定义

指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式的角色为观察者对象和主题对象,观察者对象需要观察主题对象时,需先到主题里面进行注册,然后,当主题对象的内部状态发生变化时,把这个变化通知所有的观察者。

优缺点

  • [优]降低代目标和观察的耦合度,利于代码解耦

观察者模式在观察目标和观察者之间建立一个抽象的耦合,降低了目标与观察者之间的耦合关系。被观察者角色所知道的只是一个具体观察者列表,每一个具体观察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体观察者,它只知道它们都有一个共同的接口。

  • [优]观察者模式支持广播通讯。被观察者会向所有的登记过的观察者发出通知,
  • [缺]若观察者和观察目标之间有循环依赖,它们之间进行循环调用,导致系统崩溃
  • [缺]若有大量观察者,广播性能有问题,代码会造成阻塞,可考虑多线程或者异步
  • [缺]针对前段某些框架,违背其单项数据流的思想,具体在比较可能就是eventBus与redux之间的区别了。

模式的结构

观察者模式的主要角色如下:

  • 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
  • 具体主题(ConcreteSubject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
  • 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
  • 具体观察者(ConcreteObserver)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。

具体实现

typescript

//------抽象主题类------
//抽象类实现出 观察者对象的增删,以及通知所有观察者
class Subject {
    //保存注册的观察者列表
    protected observers = new Set<Observer>();
    //注册观察者对象
    public add(observer: Observer): void {
        this.observers.add(observer);
    }
    //注销观察者
    public remove(observer: Observer): void {
        this.observers.delete(observer);
    }
    //通知所有注册的观察者对象
    public notifyObserver(): void {
        for (let observer of this.observers) {
            observer.update();
        };
    };
}
//具体目标类
class ConcreteSubject extends Subject {
    public state: number = 1;
    public setState(state: number): void {
        this.state = state;
        //主题变化的时候通知观察者
        this.notifyObserver();
    }; 
}
//------抽象的“观察者”------
interface Observer {
    update(): void;//抽象出了一个及时更新的方法
}
//观察者实现类
class ConcreteObserver1 implements Observer {
    public update(): void { //反应:所有的观察者必须有此方法
        console.log("ConcreteObserver1作出反应!");
    }
}
//观察者实现类
class ConcreteObserver2 implements Observer {
    public update(): void {
        console.log("ConcreteObserver2作出反应!");
    };
}
// ------测试------
//创建观察者对象
const obs1 = new ConcreteObserver1();
const obs2 = new ConcreteObserver2();
//创建目标对象
const subject = new ConcreteSubject();
//将观察者对象注册到主题所维护的观察者列表
subject.add(obs1);
subject.add(obs2);
subject.remove(obs1);
//改变主题状态
subject.setState(2);

对前端来说,并没有抽象层,因为前端的代码世界可没有abstract,interface概念。只能靠约定了,比如观察者类中必须有update方法。所以代码可精简如下:

//------主题类:抖音主播------
class TikTokAnchor {
    protected observers = new Set<any>();//关注者列表
    protected works = new Array<String>();//所有作品
    protected name: String;
    constructor(name: String) {
        this.name = name;
    }
    //发布新作品
    public ReleaseneWorks(neWorks: String) {
        console.log(`主播${this.name}发出新作品${neWorks}`)
        this.works.push(neWorks);
        this.notifyObserver(neWorks); //发布新作品时候通知观察者
    }
    //注册关注者
    public add(observer: any): void {
        this.observers.add(observer);
    }
    //注销关注者
    public remove(observer: any): void {
        this.observers.delete(observer);
    }
    //通知所有关注者(我发布新作品啦)
    public notifyObserver(neWorks: String): void {
        for (let observer of this.observers) {
            observer.update(this.name, neWorks);
        };
    };
}
//观察者类:抖音普通用户
class TikTokUser {
    protected name: String;
    constructor(name: String) {
        this.name = name;
    }
    public update(anchorName: String, neWorks: String): void { //反应:所有的观察者必须有此方法
        console.log(`普通用户${this.name}收到主播${anchorName}的新作品推送:${neWorks}`);
    };
}
// ------测试------
//创建普通用户对象
const zs = new TikTokUser('张三');
const ls = new TikTokUser('李四');
//创建主播对象
const anchor = new TikTokAnchor('张大仙');
//将普通用户添加到主播的订阅人列表中
anchor.add(zs);
anchor.add(ls);
//点击了发布新作品按钮
anchor.ReleaseneWorks('作品1');

es6的跟ts差不多就不贴上了,es5的代码其实就是拿ts去编译出的,其中新api之Set在es5中不支持,这里用了数组替代,本质也一样,如下是es5的版本

//---主题类:抖音主播---
function TikTokAnchor(){
  this.observers = []; //关注者列表
  this.works = []; //所有作品
  this.name = name;
};
//发布新作品
TikTokAnchor.prototype.ReleaseneWorks = function (neWorks) {
  console.log(`主播${this.name}发出新作品${neWorks}`);
  this.works.push(neWorks);
  this.notifyObserver(neWorks); //发布新作品时候通知观察者
};
//注册关注者
TikTokAnchor.prototype.add = function (observer) {
  this.observers.forEach(item=>{ //若关注者存在不能重复添加
    if(item.name == observer.name){throw '存在这个关注者,无法重复添加'};
  })
  this.observers.push(observer);
};
//注销关注者
TikTokAnchor.prototype.remove = function (observer) {
  var includeObservers = this.observers.some((item)=> item.name ==observer.name); //注销的关注者是否存在队列中
  if(includeObservers){ throw '不存在这个关注者,无法对其移出'};//若不存在则不能进入进行注销
  for(var i = 0; i<this.observers.length; i++){
    this.observers[i].name==observer.name&&this.observers.splice(i,1);
    break;
  }
};
//通知所有关注者(我发布新作品啦)
TikTokAnchor.prototype.notifyObserver = function (neWorks) {
  for (let observer of this.observers) {
    observer.update(this.name, neWorks);
  }
};
//---观察者类:抖音普通用户---
function TikTokUser(name) {
  this.name = name;
}
TikTokUser.prototype.update = function (anchorName, neWorks) {
  console.log(`普通用户${this.name}收到主播${anchorName}的新作品推送:${neWorks}`);
};
// ------测试------
//创建普通用户对象
const zs = new TikTokUser('张三');
const ls = new TikTokUser('李四');
//创建主播对象
const anchor = new TikTokAnchor('张大仙');
//将普通用户添加到主播的订阅人列表中
anchor.add(zs);
anchor.add(ls);
//点击了发布新作品按钮
anchor.ReleaseneWorks('作品1');

es5的Object.defineProperty()可以通过侵入setter来劫持数据进而实现观察者模式

和ts版不同的是,为了看清原理,突出重点,这里只有利用其实现观察的逻辑,并无管理Observer List(有些地方下边的notifyObserver本身看做Observer,这样理解也没错)如果你感兴趣,可以配合notifyObserver方法写套完整的观察者模式demo。

//主题的notify
const notifyObserver = (newVal,oldVal)=>{
  //通过setter劫持数据,可以感知对象的变化,然后做一些其他操作。
  //比如 先给对象添加一组观察者,然后再次通知观察者,这样就组成观察者模式
}
//主题
const subjectObj = {_state: 20};
Object.defineProperty(subject, 'state', {
  get: function() {
    return this._state;
  },
  set: function(newVal) {
   //和ts不同的是触发notifyObserver的办法,上边是setState手动调,而此处是监听setter
    notifyObserver(this._state,newVal);
    this._state = newVal;
  },
});
//修改状态测试
subjectObj.state = 18;

es6新增全局类Proxy,用法和Object.defineProperty()一样,是es6用来替代后者的,所以自然也能实现

//主题的notify
const notifyObserver = (newVal,oldVal)=>{}
const person = {
  name: '张三',
  state: 20
};
//主题
const subjectObj =  new Proxy(person, {
  set:(target, key, value, receiver)=>{
    notifyObserver(target[key],value);
    const result = Reflect.set(target, key, value, receiver);
    return result;
  }
})
subjectObj.state = 18;

一句话概括

观察者 观察 目标(也叫主题)
目标一有动静,观察者就被动接到通知
观察者通过主题指定add的方法注册成为观察者
主题通过调用notifyObserver方法来通知那些观察者们

发布者(publisher)和订阅者(subscriber)

是由观察者模式演变而来,比观察者模式更加的松耦合
观察者模式很好用,但是存问题:

  • 目标无法选择自己想要的消息发布,观察者会接收所有消息。
  • 如果有很多观察者或者主题,他们之间的依赖关系很复杂,那么就要去花费大量的精力去维护。代码耦合度很高
  • 这其中有大量的代码是重复的,也不利于封装的思想

发布/订阅模式在观察者模式的基础上,在目标和观察者之间增加一个调度中心。
订阅者(观察者)把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者(目标)发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。

//---调度中心---
class PubSub {
  constructor() {
    this.events = {};
  }
  
  on(eventName, handler) { //注册(相当于add)
    !this.events.hasOwnProperty(eventName) && (this.events[eventName] = []);
    this.events[eventName].push(handler);
  }
  //注销(相当于remove,删除的事件,若无第二个参数则删除该事件的订阅和发布)
  off(eventName, handler) {
    if (!this.events.hasOwnProperty(eventName)) {
      throw `${eventName}事件未注册,不需要移除`;
    };
    if (!handler) {
      delete this.events[eventName]; // 直接移除事件
    } else {
      const idx = this.events[eventName].findIndex(ele => ele === handler);
      // 抛出异常事件
      if (idx === undefined) {
        return new Error('无该绑定事件');
      };
      // 移除事件
      this.events[eventName].splice(idx, 1);
      this.events[eventName].length == 0 && (delete this.events[eventName]);
    };
  }
  //发射(相当于notify)
  emit(eventName, data) {
    if (!this.events.hasOwnProperty(eventName)) {
      throw `${eventName}事件未注册`;
    }
    if (this.events.hasOwnProperty(eventName)) {
      this.events[eventName].forEach(item => {
        item(data);
      });
    };
  };
}
// --- 测试---
const pubSub = new PubSub();// 创建PubSub实例
/**
  由于有了调度中心,
  观察者的注册 由原来的add到现在由调度中心on来操作
  观察者的推送 有原来的notifyObserver到现在由调度中心emit来操作
  观察者与主题的管理,现在也不需要两者直接交互,而是有调度中心内部管理
  通过以下代码可以看到,无论是注册观察者还是触发主题变更,都依赖中间的调度层(pubSub和[e1,e2])来将两者关联到一起
  调度层正是依靠这些事件名e1,e2作为主键,来触发不同的观察者
*/
//注册观察者
const Observer1 = (data) => {
  console.log(`我是Observer1-cbc1,我收到消息:${data}`);
};
const Observer2 = (data) => {
  console.log(`我是Observer2-cbc2,我收到消息:${data}`);
};
pubSub.on('e1', Observer1);
pubSub.on('e2', Observer2);
pubSub.off('e1', Observer1);
//触发主题的变更
pubSub.emit('e1', '第1次调用');
pubSub.emit('e1', '第2次调用');

两者的区别

总的来说,两者可以替换使用,功能作用是一样的。

  • 观察者模式是面向接口编程,实现松耦合。发布者者通过加入调度这一中间层实现了完全解耦

通过这种模式,发布者/订阅者只需告诉调度中心,我要发布/订阅消息名称是eventName的消息;当调度中心收到发布者发过来消息,并且消息名是eventName时,就会把消息推送给订阅了调度中心消息名是是eventName的订阅者。而观察者模式是需要观察者对象注册到目标主题中,目标的变化时,目标内直接去调用注册的观察者的update方法,才能通知到它。这个过程中主题需要知道观察者的所有list,以及其通知方法名update。知道要也就是两者本质区别。

  • 发布订阅模式还可以使得选择性接受发布者消息,而观察者模式,只要订阅,观察者全部收到通知
    接到发的消息(emit)后,调度层依靠自定义事件名e1,e2作为主键,来通知不同的(on)注册观察者

  • 通过发布订阅者模式完全解耦的操作,可以把调度中心单独封装个库,然后引入使用即可
    由于他订阅者和发布者不直接关联的特点我们完全可以把管理机制写到一个单独文件中,作为库来使用,比如火狐已经废弃的Object.observe( ),和facebook发布的fbemitter库,甚至连flux及其衍生的vuex和redux都有此模式

  • 使用场景不同,下边细说

两种模式的使用场景

根据依赖的紧密度:如果两者依赖较为紧密则用观察者模式,否则用后者。

观察者模式:我办了一个补习班,学生想来我这学习,必须先报名(add)。收齐一帮学生,开始教学(notify)。学生们听了我的课及时更新了自己的认知(update)。我和学生们紧密相连,每个人我都认识。
发布订阅模式:我在某视频站上开了一个专栏,把我的课上传上去,喜欢的同学订阅下。后续我只要把最新课程传到视频站上就好了,学生们听了我的课亦能及时新了自己的认知。我和学生们的联系不是那么大了。我也不需要认识他们。
后者比前者多了一个类似中转站的东西,省了我好多事。有学生不愿意学了 ,直接找中台退订就好了,不用找我说。我发布的新课程也由中台做广播,不用我自己再一个个通知,不会影响到我自己干其他工作。

根据依赖的复杂度:如果有很多观察者和主题且他们的关系复杂,适合发布订阅模式,否则后者。
总的来说,两者功能作用是一样的,可以替换使用,具体用哪种识情况而定。

参考

知乎用户
博客园

posted @ 2023-01-12 10:45  丁少华  阅读(53)  评论(0编辑  收藏  举报