服务热线
0376-910729354
技术文章
Technical articles御姐力作,深入浅出,妙趣横生,值得一看!## 引言你好,接待来到设计模式的世界,这一篇我将用一种引导、启迪的思路去讲述设计模式。在法式员的世界里,设计模式就相当于武侠世界的剑招、套路。掌握了招式,你的武学修为会获得极大提升,最终到达无招胜有招的境界。
+ 首先,我会告诉大家设计模式是什么,不是什么。+ 然后,简朴先容一下设计模式的分类,简朴枚举一下各设计模式。
+ 接着,论述面向工具设计一个很是重要的设计原则:**合成复用原则**,它是焦点原则,提高复用一直是软件工程师的不懈追求,它贯串于设计模式一书。+ 最后,从实用出发,我会详细形貌两个最经典最常用的设计模式:单例和视察者。我不只是先容这两种模式的用途和实现方式,还会联合自己事情实践,抛出限制与约束,提醒注意点,以及跟其他模式的配合方式。
希望你学完这一节,可以举一反三,在实际项目中用好设计模式,为社会做孝敬。## 什么是设计模式一门工程一定会有许多实践性的履历总结。
就好比造大桥,人们会总结拱桥有哪些部件组成,有什么特点,有什么适用场所,悬索桥又有什么部件、特点、使用场所。这些从实践中提炼出来的修建模式又可以指导新泛起的需求,好比去设计一个某市长江大桥,你会思考有哪个成熟的模式可以适用,在这个模式下,又要如何凭据实际需求定制化地设计各个部件。
软件工程也是如此。设计模式是设计模式是软件开发人员在软件开发历程中面临的一般问题的解决方案,是被重复使用,多数人知晓的,经由分类编目的代码设计履历的总结。+ 设计模式是一般问题的解决方案。分析多种多样的详细需求,经常会发现结构上和行为上具有的共性,经常会发生相似的设计。
设计模式是脱离了详细需求的,某类共性问题的解决方案。+ 设计模式是法式设计的履历总结。在其适用规模内正确地使用设计模式通常会发生高质量的设计。
+ 设计模式弥补了编程语言的缺陷。设计模式实现了建立时多态、双重分配等在主流编程语言中不直接提供的功效。反过来,近年来设计思想和设计模式的生长也影响了新兴语言的语言规范。
+ 设计模式是软件工程师的一套术语。完整地形貌一个设计通常要花费相当的篇幅,通过对设计归类,可以便于快速表达设计的特点。## 设计模式不是什么+ 不是普适原则。
设计模式并不是如SOLID设计原则一样是放之四海而皆准的普适的原则。每个设计模式都有其适用场景,必须凭据实际情况分析决议接纳哪种设计模式或不使用设计模式。
在一个软件项目中设计模式并不是用得越多越好,切合实际需求的高质量的奇特设计也是好设计。+ 不是严格规范。
设计模式是履历的总结,允许凭据实际需要改变和革新。接纳了设计模式并不意味着类的结构甚至命名都要与模式严格切合。
在应用设计模式时应着重吸取其设计思路,凭据实际需求举行设计。尤其是许多设计模式中的名称过于宽泛,在实际项目中并不适适用作类名。+ 不是详细类库。
设计模式有助于代码复用,但模式自己并不是可直接复用的代码。在设计模式中担任特定角色的并不是特定的一个类,通常需要在详细设计中联合详细需求来实现。现代编程语言中的模板、泛型等语言特性有助于写出越发通用的代码,但对于许多设计模式,完全通用的代码库既难实现,又难使用。
+ 不是行业解决方案。并没有说哪个模式特别适合互联网、哪个模式专门针对自动化。设计模式关注软件结构内在的共性,而与详细的业务领域无关。
有工程师言必称设计模式,生搬硬套设计模式,之后又泛起反设计模式的思潮,认为设计模式是骗局,无助于软件质量提升。我认为,无论是神化设计模式亦或是反设计模式都是走极端,都是错误的。设计模式为我们解决一些通用性的问题提供了良好借鉴,且在大多数情况下,行之有效。
设计模式并不停对通用,在实际项目中如何决议用哪个设计模式或是不用设计模式,很是磨练工程师的水平和履历。## GOF设计模式设计模式的盛行源于一本叫《设计模式:可复用面向工具软件的基础》的书,这本书的作者是4个博士,也叫GOF(Gang of Four),软件设计模式一词由作者从修建设计领域引入盘算机科学。
书中先容了 23 种设计模式。这些模式可以分为三大类:+ 建立型模式:单例、原型、工厂方法、抽象工厂、制作者+ 结构型模式:署理、适配、桥接、装饰、外观、享元、组合+ 行为型模式:模板方法、计谋、下令、职责链、状态、视察者、中介者、迭代器、会见者、备忘录、解释器## 合成复用原则对于软件复用来说,组合优于继续,在软件复用时,优先思量组合关系,其次才思量继续关系。面向工具设计的特点之一是继续,子类包罗父类的所有属性和方法,因此一个很自然的想法是为了复用父类的代码而继续。
可是实践发现,用继续关系来实现软件的复用有许多缺点,一般来说更为合理的方式是,用多个工具的组合关系来实现复用。+ 继续关系是子类“是一个”父类的关系,但如果是为了复用父类的已有功效来实现子类的新功效,经常会违反里氏替换原则。+ 组合关系更容易处置惩罚有多个可复用模块的情况。
多重继续会导致结构庞大不易维护。+ 组合关系更灵活易扩展,只要使用适当的设计模式,使用者和被使用者都可被修改、扩展、替换。
+ 组合关系可以提供运行时的灵活性。可以在运行时指定一个模块的底层实现,或者运行时替换一个工具的内部实现。为了体现它的重要性,这里我们看一个详细的例子。
我们知道,行列是一种先进先出的数据结构。在行列上可以执行添加和删除一个元素的操作,添加元素称为入队,删除元素称为出队,而且元素出队的顺序与入队的顺序相同。显然,行列可以用双向链表来实现,那么,我们要不要把行列设计成双向链表的子类呢?咋一看,可以让queue私有继续list,隐藏掉list所有的方法,然后实现行列的push方法挪用list的push_pack方法,行列的pop方法挪用list的pop_front方法。
很是简朴直接。可是,这种实现方式是有问题的。
到底啥问题?一言两语也讲不清楚,你自己想去吧。因此,C++和Java的尺度库都没有接纳这种继续的方式实现行列。
在C++的stl中,queue被设计成一个容器适配器。只要是是实现了push_back、pop_front的容器,都可以作为queue的底层容器。
stl中就提供了2种可以套用queue的容器,是list和deque。list就是双向链表。deque的实现是数组指针的数组,与list相比淘汰了内存分配的次数。在JDK中,Queue是一个interface,实现了Queue接口的有LinkedList、ArrayDeque、ConcurrentLinkedQueue、LinkedBlockingQueue等许多详细类。
为了体现它的重要性,这里我将用一个实例来加深你对它的印象。如果设计一个网络组件库,HttpConnection应该继续TcpConnection吗?HttpConnection不再能够提供切合TcpConnection的功效,不能看成TcpConnection使用。思量read方法,若直接袒露TcpConnection的read方法,则破坏内部结构;若提供基于HTTP协议的read方法,又无法做到功效跟父类一致。
Http协议能够使用差别的下层协议,例如TCPv6。继续自TcpConnection就失去了这种扩展性。
如果设计另一个类"HttpOverTcp6Connection",会导致二者有大量的重复代码,而这些代码恰恰是实现HTTP协议自己的功效,应复用为好。如果希望一个法式在IPv4和IPv6网络下都可使用,需要做许多的事情来实现在运行时(而非编译时)凭据设置文件或用户输入选择HttpConnection或HttpOverTcp6Connection。继续关系表达类的对外提供的功效,而非类的内部实现。
Java中HttpURLConnection继续URLConnection,与之并列的是JarURLConnection,二者都提供了凭据URL建设毗连并通信的功效。**下面以2个常用的设计模式为例,说明它们的应用场景和应用价值,让大家有一个比力直观详细的感受。**## 单例模式单例模式是指,某个类卖力建立自己的工具,同时确保只能建立单个工具。
单例模式最简朴的设计模式,也是最容易用错的设计模式。### 如何实现单例模式单例模式很是简朴,这个模式中只包罗一个类。实现单例模式的重点是治理单例实例的建立。
+ C++,可以通过static局部变量的方式,也可以通过static指针成员变量的条件建立方式做到(即每次GetInstance的时候判空,如果为空则new,否则直接返回)。Java可以用static指针成员变量的方式。
+ 通常为了制止使用者错误建立多余的工具,单例的结构函数和析构函数声明为私有函数。+ 多线程情况下,建立单例的代码需要审慎处置惩罚并发的问题。一般做法是双重检查加锁(即每次判空的时候先判空一次,如果为空则加锁再次判空)。
C++的静态局部变量可以保证线程宁静,java要使用synchronized实现。+ 多种单例,如果有依赖关系,需要仔细处置惩罚构建顺序。
C++的静态局部变量在法式首次运行到变量声明处时执行其结构函数。Java的静态变量初始化发生在类被加载时。### 单例模式的利益+ 使用简朴,任何需要用到类实例的地方,直接用类的GetInstance()方法就便利的获取到实例。+ 可以制止使用全局变量,让开发者有更好的OOP感,且可以让法式员更好地控制初始化顺序。
+ 它隐藏了工具的构建细节,且能制止多次构建引起的错误。### 单例模式的探讨从原则上说,一个类应努力提供它应有的功效,而不应对它的使用者做出过多限制。而单例模式限制这个类的工具只存在唯一实例。
因此单例模式只应在确有须要的情况下使用:+ 技术上必须保证此工具全局唯一,例如代表应用自己、工具治理器、全局服务等。+ 法式中多处依赖此工具,接纳单例模式能使代码获得极大简化,例如全局设置选项。
要制止凭据一时的详细需求将某类设计为单例,而极大地限制了可扩展性。例如一个选课系统如果把学校信息设计为单例,未来想要支持跨校选课时就比力难题。尤其注意,一旦某个类设计为单例,就会形成在法式各处随意地引用这个工具的一种倾向。
这正是单例模式的便利之处,但如果并不希望一个类有如此广泛的耦合关系,则应制止将其设计为单例。此外,由这种便利性会引发更倒霉的倾向。在未经仔细设计的系统中,随着需求变换和系统演进,单例类可能会无控制地扩展,包罗种种难以归类的数据成员和各个模块的中转方法。
### 替代方案通常有以下方法可以制止使用单例模式:+ 享元模式。例如Android SDK使用activity.getApplication() ,制止“Application.getSingleton() ”。
这样取得Application实例并不像单例模式那么利便,从而限制了Application的耦合性。而通过Activity获取Application是切合逻辑的设计,大多数真正需要用到Application的场所并不影响使用。+ 静态方法。例如Unity引擎的物体查询接口是GameObject.Find(name) ,而不是由好比“GameObjectManager”的单例类提供。
静态方法只提供单一的功效,而且挪用时的写法比单例模式越发简练。但须注意,只有逻辑上与某个类有精密联系的功效才适互助为静态方法。静态方法如果滥用,会导致软件结构实际上酿成了面向历程的设计。## 视察者模式视察者模式,当一个工具发生改变时,把这种改变通知给其他多个工具,从而影响其他工具的行为。
又称订阅模式、事件模式等。### 视察者模式的组成视察者模式中包罗两个角色:+ 被视察者,它维护视察者列表,并在自身发生改变时通知视察者。
也可称为公布者、事件源等。+ 视察者,它将自身注册到被视察者维护的视察者列表,并在吸收到被视察者的通知时做出响应。视察者也称订阅者。
### 如何实现视察者模式被视察者的接口应包罗3个方法:增加视察者、删除视察者、向视察者发送通知。其中,增加视察者、删除视察者通常由视察者挪用,用于讲明哪些视察者工具需要获得通知。发送通知方法通常由被视察者挪用,因此可以思量界说为protected方法。发送通知方法应遍历自身的视察者列表,逐一挪用视察者的吸收通知方法。
这3个方法功效较为明确,可以用抽象类、模板、泛型等技术提供通用实现。视察者的接口需要提供吸收通知方法,以供被视察者挪用。差别的详细视察者类型实现各自的吸收通知方法,实现当被视察者发生改变时,视察者应做出的响应。由于视察者接口只有一个方法,在C#语言中deligate来取代,在C++中可以用std::function取代,这样进一步解耦了差别类型的视察者,其不必派生自同一个公共接口。
固然,当系统中的视察者简直有所联系时,则不应该过分追求解耦,显式界说一个视察者接口或抽象类可以使结构更为清晰、严谨。视察者模式经常与下令模式配合使用。下令模式是,将一个请求封装为一个工具,使发出请求的责任和执行请求的责任支解开。
接纳下令模式,将通知或事件封装成工具,可以使视察者和被视察者之间进一步解耦。例如,如果不希望在被视察者的运行历程中穿插执行视察者的函数,则可以生存下令稍后执行。### 视察者模式的特点和适用场景每种设计模式都有其最适合的应用场景,如果正确使用,可以资助理清庞大的耦合关系,简化设计。
但如果在不合适的场景中生搬硬套,则会把原本简朴的事情搞庞大,并不能真正解决需求。视察者模式也不破例,在实际项目中,必须详细问题详细分析,考察需求是否切合视察者模式的特点,决议是否选用视察者模式。
+ 视察者模式适合一对多的关联关系。一个被视察者可以有零个或多个视察者。固然,一个法式中被视察者可以有多个,每个被视察者都有自己的一对多关系,而相互之间没有关联。
+ 逻辑上的依赖关系是单向的。被视察者往往可以独立运行,并不依赖视察者。而视察者的顺利运行依赖于被视察者的推动,脱离被视察者就运行不起来了。+ 挪用关系与逻辑关系是反向的。
逻辑上被视察者不依赖视察者,但有事件发生时却是被视察者挪用了视察者的方法。下面我们用一个例子来看如何应用视察者模式来解决详细的需求,以及使用视察者模式带来的利益。
我们假设需求是这样:某个应用法式中有多处要用到定时执行的功效,就是到一个牢固的时间需要执行一个特定的函数。很自然,多处要用到的功效应该提炼出来作为一个子模块。
但另一方面,我们又不希望这个定时模块与每一个用到了定时功效的其他模块都有很强的耦合。视察者模式可以资助我们设计定时模块,既能服用,又有低耦合性。这里我们的示例实现如下。
为了突出展示视察者模式,我对需求做了一定简化,我们的定时模块牢固在天天上午9点触发,不支持自界说时间。#include <iostream>#include <list>//简朴闹钟,天天早上9点响class AlarmClock { public: class Alarm { public: virtual ~Alarm() {} virtual void onClockAlarmed() = 0; }; private: static const int TimeZone = 8; // 北京时间东8区 static const int AlarmHour = 9; std::list<Alarm*> alarms; time_t tomorrow; public: AlarmClock() { //将tomorrow设置为明天9点钟 time_t now = time(0); tomorrow = now - now % 86400 - TimeZone * 3600 + AlarmHour * 3600; if (tomorrow < now) tomorrow += 86400; } AlarmClock(AlarmClock&) = delete; void setAlarm(Alarm* alarm) { alarms.push_back(alarm); } void unsetAlarm(Alarm* alarm) { alarms.remove(alarm); } void advance() { tomorrow += 86400; for (auto alarm : alarms) { alarm->onClockAlarmed(); } } void update(time_t now) { while (now >= tomorrow) { advance(); } }};// 资深法式员张三class TestZhangSan : public AlarmClock::Alarm { public: ~TestZhangSan() {} TestZhangSan(AlarmClock& clock) { clock.setAlarm(this); } // 开始了996的一天 void onClockAlarmed() { std::cout << "Zhang San is going to work..." << std::endl; }};// 隔邻上夜班的王叔叔class TestLaoWang : public AlarmClock::Alarm { public: ~TestLaoWang(){} TestLaoWang(AlarmClock& clock) { clock.setAlarm(this); } // 下班回家睡觉 void onClockAlarmed() { std::cout << "Lao Wang is going to bed..." << std::endl; }};int main(int argc, char **argv){ AlarmClock clock; TestZhangSan zhang(clock); TestLaoWang wang(clock); time_t now = time(0); now -= now % 3600; for (int i = 0; i < 24; i++) { std::cout << "Now:" << ctime(&now); clock.update(now); now += 3600; } return 0;}// Java 代码import java.util.Calendar;import java.util.List;import java.util.LinkedList;//简朴闹钟,天天早上9点响public class AlarmClock { public static interface Alarm { void onClockAlarmed(); } private static final int AlarmHour = 9; private final List<Alarm*> alarms = new LinkedList<>(); private Calendar tomorrow; public AlarmClock() { //将tomorrow设置为明天9点钟 tomorrow = Calendar.getInstance(); boolean addDay = tomorrow.get(Calendar.HOUR_OF_DAY) >= AlarmHour; tomorrow.set(Calendar.HOUR_OF_DAY, AlarmHour); tomorrow.set(Calendar.MINUTE, 0); tomorrow.set(Calendar.SECOND, 0); tomorrow.set(Calendar.MILLISECOND, 0); if (addDay) { tomorrow.add(Calendar.DAY_OF_MONTH, 1); } } public void setAlarm(Alarm alarm) { alarms.add(alarm); } public void unsetAlarm(Alarm alarm) { alarms.remove(alarm); } public void advance() { tomorrow += 86400; for (Alarm alarm : alarms) { alarm.onClockAlarmed(); } } public void update(Calendar now) { while (now >= tomorrow) { advance(); } } // 资深法式员张三 private class TestZhangSan : public Alarm { public: TestZhangSan(AlarmClock& clock) { clock.setAlarm(this); } // 开始了996的一天 public void onClockAlarmed() { System.out.println("Zhang San is going to work..."); } } // 隔邻上夜班的王叔叔 private class TestLaoWang : public Alarm { public TestLaoWang(AlarmClock& clock) { clock.setAlarm(this); } // 下班回家睡觉 public void onClockAlarmed() { System.out.println("Lao Wang is going to bed..."); } } public static void main(String []args){ AlarmClock clock = new AlarmClock(); TestZhangSan zhang = new TestZhangSan(clock); TestLaoWang wang = new TestLaoWang(clock); Calendar now = Calendar.getInstance(); now.set(Calendar.MINUTE, 0); now.set(Calendar.SECOND, 0); now.set(Calendar.MILLISECOND, 0); //冒充时间经由了24小时 for (int i = 0; i < 24; i++) { System.out.println("Now:" + now.getTime()); clock.update(now); now.add(Calendar.HOUR_OF_DAY, 1); } }}在这个例子中,AlarmClock类是被视察者,Alarm接口及其详细子类是视察者。根据视察者模式,被视察者AlarmClock维护了它的视察者的列表。
其时间举行到新一天的早晨,AlarmClock的状态发生变化,也就是发生了一个事件,这时AlarmClock挪用每个Alarm的方法。这样,Alarm的详细子类工具,即每个希望定时执行的模块,就能够在正确的时间获得执行。由于接纳了视察者模式,AlarmClock与其它模块之间只通过Alarm接口交互,AlarmClock只引用Alarm,而不需要体贴每个Alarm到底是哪个详细类,也不体贴挪用Alarm后究竟会执行哪些操作。如果Alarm的详细子类需要修改,我们并不需要修改AlarmClock类。
如果有新的模块需要用到定时功效,只需要让新模块实现Alarm接口即可。这就是视察者模式降低耦合性的作用。
因为这个例子中被视察者只有一个,因此被视察者的抽象接口被省略了。而且我们没有使用Observer、Subject等很是宽泛的名字,而是联合实际情况,视察目的就是详细类AlarmClock类,视察者被称为Alarm。这样使得整个设计很是自然,没有生搬硬套设计模式的痕迹,哪怕是没有学过设计模式的人也能够看懂。
这就是在详细应用设计模式时经常应该做的剪裁和调整。需要指出,这个例子是为了能够清晰演示视察者模式而专门假设的场景。你可以实验把例子举行扩展。如果希望支持为每个Alarm指定差别的执行时间,应如何设计?如果张三多件事情需要划分定时执行,又应如何设计?在实际项目中,业务需求一定会更为庞大,工程师需要在庞大需求中识别出在那里使用哪种设计模式能够带来利益,这是需要磨炼提升的能力。
实际项目的设计也会凭据需求做出更多的调整,多一些类或少一些类,经常看起来跟最初学习设计模式时看到的很纷歧样。因此学习设计模式重在掌握思想,不能生搬硬套。无招胜有招。
## 总结短短一篇文章,想要讲清设计模式的所有内容险些是不行能完成的任务,所以我没有逐一解说,而是联合我自己事情中遇到过的问题,来带你重新认识设计模式,为你树立它的重要性的看法,制止陷入细节泥潭,欢喜的时间过太快,又是时候说拜拜,最后,恭喜大家,你已经掌握了设计模式,去干一番对人类有益的事业吧。本篇由御姐供稿,版权息争释权归御姐所有,文章内容代表御姐意见,本农民自媒体对文章看法不持态度,喜欢的话就关注点赞转发吧,“码砖杂役”微信民众号,你身边的土味砖家。
本文来源:海德体育-www.tzhongniu.com
地址:陕西省西安市闽清县用克大楼9652号
电话:0376-910729354
邮箱:admin@tzhongniu.com
Copyright © 2002-2021 www.tzhongniu.com. 海德体育科技 版权所有 备案号:ICP备99818263号-5