前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计模式笔记

设计模式笔记

作者头像
腾讯大讲堂
修改2020-04-29 16:35:39
1K0
修改2020-04-29 16:35:39
举报

| 导语 “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决” “Any problem in computer science can be solved by anther layer of indirection.”

设计模式这个词源于城市建筑设计,由Alexander提出:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心”。引用《head first设计模式》书中的一句话 --“把模式装进脑子里,然后在你的设计和已有的应用中,寻找何处可以使用它们”。也就是说学习设计模式不能纸上谈兵,学习新的设计模式,要去思考在以前的代码中哪里可以用到。并且对比设计模式之间的差异来加深理解。

设计模式分为三类,创建型,结构型和行为型。创建型比较好理解,它抽象了实例化过程,将系统与实例的创建解耦。实例由专门的工厂来创建,从而使系统针对实例的抽象接口编程,不依赖任何具体的实现。结构型和行为型有点难以理解,GoF的解释是,结构型模式涉及到如何组合类和对象以获得更大的结构;行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。

大部分行为型和结构型设计模式的特点还是挺明显的,但是有少部分的界限就没那么清晰。比如说代理模式属于结构型模式,但是它也承担了职责的分配。它通过一个代理类,直接处理客户请求,但是把大部分实际职责交给原始的工作类。将设计模式划分为三种类型,可以理解为是划分出一种层级,帮助模式的使用者记忆和理解。

GoF提到的23种设计模式中有的特点是比较鲜明的,它们有明显的中间层,并且与其它设计模式不容易混淆,但是有的却不那么容易理解其意图。下面就按照是否容易理解分成两类来记录这些设计模式。

那些容易理解,有明显中间层的设计模式

单件模式

单件模式很好理解,就是这么一个类,它在任何情况下都只会产生一个实例(多线程异常情况除外)。当然只会产生一个实例不是绝对的,我们可以改成只会产生两个或者任何其它有限个。单件模式的产生就是为了取代我们之前使用全局变量的情况。直接使用全局变量有很多局限性,比如可能会被实例化多次,不能灵活扩展成多个,难以支持延迟实例化(就是到真正使用时才实例化)等。

中间层的思考:单件模式在系统和全局变量之间中添加了一个中间层,之前系统直接调用全局变量,而使用单件模式后,系统使用类静态方法Instance来获取全局实例。在Instance函数中可以做很多事情,包括延迟实例以及指定实例数量,甚至返回不同的子类实例。

适配器模式

下图这个转接头就是适配器最好的例子。中国香港的iphone插头是三个脚的,而内地的插头却是两个脚的,中国香港插头没法直接插到我们平时使用的插板上面。一般会去淘宝上面买个转接头,这个转接头的一面可以插入中国香港的插头,另一面可以插到我们平时使用的两脚插板上去,完美地解决了中国香港插头和内地插板不适配的问题。

适配器模式要解决的就是新加模块与系统已有接口不匹配的问题。通过新增一个适配器Adapter,它保存一个新模块Adaptee的实例,并向外提供系统已有接口,适配器Adapter内部使用新模块Adaptee的实例来实现系统接口。

中间层的思考:适配器模式在新加模块和已有系统之间加入了一个中间层 -- Adapter,来解决接口不兼容的问题。比如我们如果要在系统中新加一种网络日志,网络日志库提供了一套与系统中已有的本地日志库不一样的接口,此时就可以添加一个适配层,将网络日志库进行封装,提供一套与已有本地日志库一样的接口,这样系统可以轻松地在本地日志库和网络日志库之间切换,而不用修改太多代码。

外观模式

从外观模式这个名字没法直观看出它到底是做什么的,消息后台有一个注册代理就是外观模式的一个例子。在它注册代理存在以前,手Q4.5从登陆到拉完全部消息并更新消息Tab耗时会超过30秒,因为手Q登陆需要负责向消息后台分别拉取未读好友消息和群讨论消息,拉取群/讨论组的消息比较复杂,需要先拉去一遍群组seq,然后同本地每一个群组seq比较判断出哪些群组有未读消息,再分别去把未读群组消息拉回来。整个过程非常复杂和耗时。为了解决这个问题,消息后台搭建了一个注册代理模式,手Q登陆只需要发一个登陆请求给注册代理,注册代理会把上述拉消息的脏活累活全都干了,把最终的结果返回给手Q,这样手Q的登陆时间缩小到了几秒。这里注册代理就相当于外观模式,它向手Q屏蔽了消息后台关于好友消息和群/讨论组的复杂细节,只暴露给手Q一个简单的请求和回复包接口。

中间层的思考:很明显可以看出外观模式的中间层就是在客户和复杂的子系统之间提供了一套简洁的接口,让客户对子系统的了解程度和耦合达到最小。在外观模式中也不会限制客户直接使用子系统类,但这会增加耦合性,所以需要在系统的易用和可扩展性之间作出取舍。

模式迷思:有没有觉得外观模式和适配器模式有点像?它们都提供了一个中间层,中间层负责封装了一些对象,然后提供了一套接口,做的事情简直一模一样!冷静想一想,这两个模式的结构确实是基本上一致的,但是它们的目的却完全不一样。适配器模式的目的是为了兼容新模块和老系统,而加入中间层做适配。而外观模式的目的是为了降低系统使用某个外接系统的成本和耦合。再看看设计模式的定义 --“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心”。也就是说设计模式是由两部分组成的,即它描述的问题以及解决方案。两个设计模式的解决方案可能是相同的,甚至可以说所有设计模式的解决方案都是相同的(添加中间层),但是所有设计模式描述的问题绝对是不相同的。后续还会看到很多设计模式,感觉它们的解决方案(类图)很相似,这个时候就得想想这些设计模式的出发点 -- 它们描述的是个什么问题?

享元模式

享元是什么意思?简单说就是共享元素。享元模式是运用共享技术有效地支持大量的细粒度对象。举个例子,我们经常使用word来编辑文档,会使用到一些精美的字体。精美字体库中的每个字可能都比较大,估计有几百B到几K。写一篇文章不可能对每一个字都保存一份精美字体的拷贝,不然一篇10000字的文章就有几M。一般的做法会把精美字体放到一个库里面,然后写文章的时候,只去引用字体库里面的字体,而不是直接使用字体库里面的拷贝。这样一篇文章只需要存储每个字的排版信息和引用的字体信息就可以了,大大减小了所需的内存空间。当我们在系统中发现很多同类对象都是要被重复创建和使用的时候,就可以想到用享元模式来处理。享元模式通常是使用一个工厂类来负责细粒度的管理和创建工作。如下图中的FlyweightFactory,就是一个享元工厂。

中间层的思考:享元模式在系统和直接使用细粒度对象之间加入了一个享元工厂,这个工厂就是中间层。系统不再直接创建和使用对象,而是由这个工厂来负责创建对象,工厂内部对同一对象只会创建一次,保证对象都是共享的。

模式迷思:享元模式会和简单工厂模式有点像,都用到了工厂的概念。其实享元模式里面就是用到了简单工厂模式来管理细粒度对象。享元模式解决的问题是对象的共享,而工厂模式解决的问题是如何封装对象的创建过程。明白它们两解决的问题,就知道它们是两种完全不一样的模式。但是它们却可以完美地结合在一起,协同解决问题。

代理模式

从这个模式的名字可以很容易看出来它是做啥的,就是增加一个代理,来帮我们做一些附加的事情。比如春运买火车票,一票难求。我们经常会去找火车票代理(即黄牛)帮我们买票。向火车站和向黄牛买票的基本流程都是一样的,包括提供始发站,乘车人,乘车时间等信息,然后出票付款。但是黄牛作为代理,在真正向火车站买票之前还做了很多事情。他们会通过各种手段提高买票的成功率,比如做刷票外挂,找后门,去火车站找熟人等。代理就是这样一个在客户和访问实体之间提供附加服务的存在。它提供的接口与访问实体是一致的,所以客户基本上感知不到是在访问代理还是实体。代理的应用有远程代理(访问的实体在其它地方,代理封装网络操作),虚代理(根据需要创建开销很大的对象),保护代理(控制对原始对象的访问),智能代理(添加对原始对象的引用计数或者锁等机制)。

中间层思考:在系统和访问实体之间添加了一个代理,这个代理就是中间层。代理提供了和访问实体一样的接口,系统在不用改变调用方式的情况下,可以增强访问实体的功能,对访问实体添加保护等。

模式迷思:看到这里,会发现前面的适配器模式和代理模式非常相似。它们都将一个模块进行了一次封装,从而为系统访问提供便利,保证系统不需要改变调用接口就可以增强功能。适配器模式和代理模式最大的不同还是在于他们的出发点不同,适配器模式是为了做兼容,而代理模式的核心是增加功能。当然适配器模式在做适配的时候,也可以增加一些功能,这样它就跟代理模式非常接近了。

命令模式

命令模式针对的情形是那些界面功能需要经常改动,并且界面功能重复需要复用功能模块的情况。比如在实现网页上的按钮功能时,我们通常是直接在onButtonClick()函数中直接写逻辑,如果有另一个按钮也要实现相同的功能,我们可能会将代码拷贝一份。稍微考虑下封装的话,就把button的逻辑用一个函数包装起来,给onButtonClick调用。假如哪天需要将这个按钮的功能换成其它的,就需要直接修改onButtonClick函数,修改其中的功能逻辑或者调用其它的功能函数。这种改动对于经常需要变动的界面来说是个噩梦。命令模式将按钮执行的逻辑用一个Command抽象类封装起来,onButtonClick中调用Command抽象类的方法,这样当需要执行其它功能的时候,只需要将Command对象换成另一个子类实例。而子类的实例化可以使用工厂方法来做。

中间层思考:命令模式在界面组件和功能模块之间提供了一个中间层Command,界面组件不再直接调用功能模块,而是调用Command的抽象方法。这样将界面组件与功能模块解耦,界面组件可以灵活切换要实现的功能,功能模块的改动也不会对界面组件有影响。

模式迷思:命令模式的类图和适配器模式的类图很像,它们做的事情都是制造了一个中间层,提供给系统统一的调用接口,封装了真正干活的实体。它们的差异还是在于使用意图,命令模式是为了命令的复用和灵活切换,而适配器模式是为了在新旧接口之间做兼容。

迭代器模式

迭代器很好理解,如果使用过STL模版库的容器类,就会或多或少使用过容器类提供的迭代器。常见的容器类有List,Set,Map等,使用容器类经常需要遍历里面的所有元素,当对容器类中元素的组织方式不清楚时,要遍历元素不是一件容易的事情。迭代器就是解决这个问题的救星,它提供了一套统一的接口,如begin(), next(), end(),所有的遍历操作都可以用这套接口来实现。这套接口的具体由每个容器类来实现,容器内部才是最清楚怎么遍历自己元素的地方。正向遍历,反向遍历,中序遍历的需求可以通过由容器内部创建不同的Iterator来实现,而Iterator暴露的接口始终是一样的。

中间层的思考:通过引入Iterator迭代器这个中间层,系统可以不需要直接操作容器的所有细节,只需要知道Iterator的标准接口就能操作容器的所有元素。Iterator将系统和容器的细节解耦了,程序猿的生活也变得更加美好了。

策略模式

策略模式是对算法的封装。当系统中做同一件事情有多种方法可以实现时,就将这多种方法都独立封装起来,并暴露相同的接口,使系统在使用它们的时候可以相互切换。系统使用不同的迭代器(正序迭代器,反序迭代器,中序迭代器)可以认为就是策略模式的一个实例。不同迭代器的使用接口和方式完全一样,当系统想要切换遍历容器的方式时,只要创建一个相应的迭代器,而使用迭代器的方式完全不用改变。

中间层思考:策略模式抽象出了一个Strategy类作为中间层,系统不直接访问某一个具体的算法,而是通过访问Strategy抽象类来调用算法,这样可以动态地在运行时切换算法。

那些容易混淆,没有明显中间层的设计模式

工厂方法与抽象工厂

关于这两个模式,从名字上难以明确地看出它们的区别。它们的目的基本是一样的,都是为了封装对象的创建过程,让系统与具体对象解耦。既然目的一样,那么差异究竟在哪里呢?在于它们的解决的问题规模和情形不一样。按照我的理解,工厂方法的问题规模一般是一种产品,而抽象工厂的问题规模是一个产品族,也就是一系列具有联系的产品。从名字上看,工厂方法它就是一个方法,它是某个需要创建产品的类的一个抽象方法,返回一个抽象的产品。抽象方法延迟到子类实现,由子类来决定具体生产哪种产品。抽象工厂就是一个工厂,通常是一个抽象类,类中定义了很多生产产品的抽象函数。比如一家电器工厂就是一个抽象类,它会生产电视机,冰箱,洗衣机等抽象产品。而海尔电器就是这个抽象工厂的具体实例,它会生产海尔牌电视机,海尔牌冰箱等。三星电器是另一个抽象工厂的实例,它会生产三星电视,三星冰箱等具体产品。

装饰模式

装饰模式的目的是在不改变已有对象的情况下,为已有对象动态地添加职责。它的目的非常明确,重点是要理解它是怎么做到的。要怎么样在不改变已有对象还能动态为对象添加职责呢。我们可以想象存在一个链表,链表中每个节点的功能就是实现一个子功能,当把链表按照顺序执行一遍后,就相当于实现了最终目标。当我们要为这个最终目标添加一些附加功能的时候,只需要在链表后面添加附加功能节点就可以了,而实现功能的方式完全不用改变,还是将链表按顺序执行一遍,执行的方式一般使用递归。对比那种通过继承父类,在子类中增加新功能的方式,装饰模式的动态添加职责方式可以有效地防止类爆炸。装饰模式真正实现使用的是递归的方式,当需要增加新功能时,就在它的递归链最外层增加一层新功能。这个模式使用还有一些限制,就是所有的功能节点都需要有共性,能够继承自同一父类,这样才能用递归的方式统一处理。

中间层思考:这个模式的中间层不明显。先来回顾一下整个过程,系统最开始访问的是众多的子类,子类的不断添加引起类爆炸。然后装饰模式将子类拆分成具有共性的功能节点,通过功能节点的自由组合来实现各种功能。可以看出装饰模式是通过改变子类的组织方式来解决问题的,似乎并没有用到中间层。

模式迷思:装饰模式和组合模式存在很大的相似程度。它们都是通过组合的方式将对象组织在一起,然后通过递归的方式去访问以实现功能。如果组合模式中每个节点只维护一个子节点的话,它就与装饰模式基本无异了,也即是说装饰模式是一个退化了的组合模式。我嘛还是从出发点找差异,装饰模式的意图是为了动态添加功能,而组合模式的意图是为了对象的聚集。模式的实现是存在很大相似程度的,因为毕竟只存在类集成和对象组合这两种组织方式,关键是要记住模式要解决的问题,当真正在项目中遇到的时候能马上想起来可以用哪个模式来解决。

组合模式

组合模式的意图不是很好理解,但是它的个性鲜明,不会和其它模式产生混淆。首先理解一些它的意图,GoF中的定义是将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。简单说就是将部分和整体使用统一的格式Component表示,这样写代码的时候不需要用许多的if去判断类型,只需要使用统一的方法去处理所有Component节点即可。比如消息后台使用的多级TLV,第一级tlv叫做elem,第二级tlv叫做attr。如果要统计某个elem下的attr个数,就需要写一个双层的for循环曲统计。假如以后在attr下面加了一个三级tlv,要统计三级tlv的话就得写一个三层的for循环。如果使用组合模式,把所有tlv都是用统一的表示格式,那么只需要用一个递归函数就可以解决问题,这就是组合模式的威力。

观察者模式

观察者模式用好莱坞法则来解释再好不过了 -- “不要给我们打电话,我们会打电话给你(don't call us, we'll call you)”。网络编程中有两种模式,同步模式和异步模式。异步模式的思想就是观察者模式的核心。异步模式是怎么工作的呢?比如某个socket现在要收包,就调用select,告诉系统要收包,然后socket就不会傻等着阻塞在收包的地方,而是直接返回腾出cpu让程序去做其他事情。等系统收到包后,就会唤醒socket来收包。同步模式是怎么做的?同样是socket要收包,它就是占着cpu傻等在那里,程序不能做其它事情了,直到收到包才继续执行下去。

消息后台Msgcenter连接了多个模块,比如最近联系人,漫游服务器,Conn,OnlinePush,MsgDb。这些模块针对不同消息(类型,号码,收发终端,终端版本,消息内容)可能会做一些特殊逻辑。这种情况下可以将多个模块抽象成观察者Observer,去监听特定的消息并做特殊的处理。在这里不同的消息类型就是事件。

中间层思考:初看观察者模式并没有使用中间层,但其实观察者Observer引入的中间层。如果没有Observer,那么事件产生者需要知道事件监听者的具体类型,才能通知到每一个监听者。通过引入Observer这样一个抽象层,事件的产生者Subject可以不用关心通知的是哪些模块,只需要统一地调用Notify函数,其它事情通过调用统一的for循环去处理了。

状态模式

不能说状态模式一定是好的,只能说当行为action的数量是比较固定的情况下使用状态模式才是好的。实现一个状态机有两种方式,第一种是将行为action封装起来,在行为函数中处理每一种状态。第二种方式是将状态state封装,在状态state中处理每一种行为action。状态模式就是第二种方式。它将状态state封装成一个对象,在这个对象中处理每一种行为action。这种方式添加状态很方便,只需要修改与新增状态有关的几个状态对象就可以了。但是这种方式添加行为很麻烦,需要对所有状态对象做修改,来处理这种行为。所以说状态模式适用于行为action变动比较小,而状态变动比较频繁的情况。

剩下的模式

上文中还没有提到的设计模式包括生成器模式(builder),原型模式(prototype),桥接模式(bridge),职责链(chain of responsibility),解释器(interpreter),中介者(mediator),备忘录(memento),模版方法(template method),访问者(visitor)。

生成器模式:封装一个产品的构造过程,允许按步骤构造。如果产品只有一个构造过程,那么生产器模式就退化成了简单 工厂模式。生成器模式暴露出一组标准的构建产品的抽象方法,允许用户参与到构建产品的过程中,以控制产品的生成。

原型模式:当创建给定类的实例的过程很昂贵或者很复杂时, 就使用原型模式。通过创建类实例对应的prototype原型实例,后续创建类实例就直接调用原型实例的clone函数,从原型实例自身拷贝一个实例返回。原型模式时基于从0创建一个对象的成本远远高于直接拷贝对象的成本。

桥接模式:将抽象部分与它的实例部分分离,使它们都可以独立地变化。经典的例子就是窗口系统(对话框,窗口,警告框等)在多机型上面的实现。每个机型上的GUI接口都不一样,比如画图操作在不同机型上是不一样的。如果直接用在每个机型上实现一套窗口系统工作量是很大的。这个时候抽象出一套机型上GUI绘图的最小接口集合,用来实现抽象的窗口系统。这样的窗口系统移植到多机型上面只需要实现GUI的最小接口集合就行了。

职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。c++的异常处理就是一种职责链的模式,当程序出现的异常的时候,就会一层一层的往外抛,直到异常被处理。我们可以动态地添加异常处理代码,去处理可能异常。

解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。所谓的领域专用语言指的就是这种在某些领域使用的比较专业性的程序语言,为了实现这种语言,通常抽象一个解释器类去解析这种语言。

中介者模式:中介者用来集中对象之间复杂的沟通和控制方式。现实中的交换机就类似中介者模式,如果一台电脑需要加入局域网与其它电脑通信,不需要与其它每台电脑单独建立连接,只需要与交换机连接即可,交换机会帮忙处理新电脑与其它电脑的连接。

备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态。备忘录模式的关键在于封装Memento对象,只有源对象可以设置Memento和从Memento恢复,从而保证源对象的封闭性。

模版方法模式:定义一个操作中算法的骨架,而将步骤延迟到子类中。

访问者模式:当你想要为一个对象的组合增加新的能力,且封装并不重要时,就使用访问者模式。通过增加一个Visitor中间层,Visitor负责向客户提供多种多样的功能,对象组合只需要提供一个有限的接口给Visitor获取信息即可。

总结

就像优化准则之一是“不要过度优化”,设计模式也是不要在需求不明确的情况下过早和过多地使用设计模式。记住几个常用的设计模式,在实际项目中思考哪些是经常变动的,哪些可以套用上这些设计模式。

作者:罗鹏,毕业于武汉大学,目前就职腾讯,负责QQ消息后台开发。现阶段技术兴趣为分布式和代码调优,欢迎交流~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-02-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯大讲堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档