深入了解设计模式和设计原则
- 一、认识设计模式
- 1.1、设计模式是什么?
- 1.2、设计模式是怎么来的?
- 1.3、设计模式解决了什么问题?
- 二、设计模式的基础
- 2.1、面向对象思想
- 2.2、设计原则
- 三、如何学习设计模式
- 3.1、明确目的
- 3.2、学习步骤
- 总结
一、认识设计模式
设计模式总共有23种,首先呢,我们来对设计模式有一个大致的了解,带着这几个问题来思考一下:
- 设计模式是什么?
- 设计模式是怎么来的?
- 设计模式解决了什么问题好?
- 怎么学习设计模式?或者说设计模式的基础是什么?
- 如何学习设计模式?
1.1、设计模式是什么?
那什么是设计模式呢?看设计模式的定义:设计模式是指在软件开发中,经过验证的,用于解决在特定环境下,重复出现的,特定问题的解决方案。
从这个定义可以看出,设计模式有很多的限定词,比如“特定”、“重复特定”等。那说明什么问题呢?说明设计模式在使用的时候它有很多的局限性。所以,学习设计模式一定要切入它的一个本质,也就是它解决一个什么问题,然后再去使用它。当我们不清楚这个设计模式解决什么问题的时候,不要轻易的去使用设计模式,所以设计模式是适用的好。
设计模式的定义换一句都能够听懂的话,就是设计模式是解决软件开发过程中一些问题的固定套路,解决问题的固定套路。因此,不要过度的去封装或者去使用设计模式,除非我们已经确定了这个设计模式,就是明确了我们的这个具体需求的变化方向;而且这个变化方向的这个点呢,经常的出现,反复的出现,那么我们才会去使用设计模式,就是要使用恰当。还有一个,就是设计模式类似于一个哲学,或者说类似武侠小说里的一个武功秘籍,一定要具备一定的工程代码量的才能够精通。但是,学习设计模式还是有必要的,我们要提前知道设计模式。
1.2、设计模式是怎么来的?
设计模式是通过慢慢迭代出来的,即它是通过满足我们设计原则之后,慢慢迭代出来的。在这里提到两个重要的关键字:
- 第一个关键字是设计原则。学习设计模式的时候首先要掌握设计原则。
- 第二个关键字是迭代。因为重复出现,所以慢慢迭代出一些固定的解决固定问题的套路。
1.3、设计模式解决了什么问题?
使用设计模式有一个非常重要的前提条件,就是具体的需求要既有稳定点又有变化点。只有在这种场景下(既有稳定点又有变化点的这种场景下),才可以使用设计模式;这个前提必须要理解好。
- 稳定点可以理解为就是不会变的东西。
- 变化点就是经常需要进行变化的,因为具体的一个软件产品,在开发当中,它是长期在运行的,长期需要维护进行开发的,它可能是未来产生的,也有可能是现在产生的;设计模式就是去应对这些变化而产生的。
先想象一下两个极端。如果全是稳定点,也就是软件代码呢它没有什么变化,这种场景下是没有必要使用设计模式的。另外一种极端呢,就是全是变化的,一直都是发生改变的,而且改变是没有固定的方向,它也没有必要使用设计模式。这种极端通常在游戏开放中比较常见,游戏开发当中呢,通常用脚本语言去解决这种全是变化点的问题,因为不需要重新编译、不需要重新启动,脚本可以在上面写很多的复杂的功能,只要重新热更新一下就行了,游戏 的逻辑、还有数据就重新加载到运行的服务器当中了。
好了,那么设计模式具体解决问题的场景是什么呢?就是希望修改少量的代码就可以适应需求的变化。使用设计模式的时候,都是希望修改少量的代码就可以适应需求的变化,如果不满足设计原则,可能总是需要进行重构(推倒重来),即代码写完了以后要推倒重来,重新写好,这个是不符合软件开发的一个需求的;软件开发希望每次是在原来的基础上修改少量的代码就可以应对未来需求的变化,设计模式就是解决这一类问题的。
举一个例子:一间整洁的房间,这个房间里有一个好动的猫,怎么保证房间的整洁呢?这个和使用设计模式要解决问题的时候的场景是一模一样的,整洁的房间代表着一个稳定的点,我们希望这个房间一直是整洁的。那么好动的猫是一个变化点,如果这个猫到处在房间走动的话,那么就会让我们整洁的房间变得不整洁?现在软件开发所要解决个问题就是怎么保证房间的整洁,怎么去解决这个问题呢?把这个猫关在一个笼子里,这样一来猫它会在有限范围内进行活动了,整洁的房间也可以保证了。整洁的房间是我们的目标,好动的猫是时时刻刻都可能发生改变的一些需求;使用设计模式让这一个变化点在有限范围内变化。
二、设计模式的基础
设计模式跟语言也是相关的,都是利用语言的特性去实现设计模式。对于C++而言,设计模式的基础有哪些呢?第一个需要有一个面向对象的思想,第二个基础是设计原则。
2.1、面向对象思想
面对对象的三个特征:
- 封装。封装的目的是为了隐藏实现细节,实现模块化;写代码的时候模块化他们互相之间不干扰,不影响(隐藏实现细节,好实现模块化)。
- 继承。C++的继承跟其他语言还不太一样,其他语言支持单继承,只有C++支持多继承。C++可以继承多个接口或者是多个类。继承主要的目的是希望无需修改原有类的基础上通过继承来实现对功能的扩展。
- 多态。这个是设计模式最重要的一个特性,很多的设计模式都需要依赖多态的实现。C++的多态有两种,第一个是静态的多态,还有一种是动态的多态。
- 静态的多态要是指函数的重载;就是同一个函数名,通过参数的不同来可以同时表现出不同的特形态。
- 动态的多态是指继承的过程当中虚函数重写。很多的设计模式主要都需要依赖这一个特性。
类模型:
上图描述的是一个多态行为,classBase
是一个基类,基类当中有两个方法,func1
和func2
,都是声明为虚函数;还有一个class Subject
类,继承于这个Base
类,也有两个方法:func2
和func3
,需要注意func2
在基类当中已经存在了,func3
它是一个独有方法,有它的一些相对应的其他属性;从它的具体的内存布局来看,在Base
类当中有虚函数,就会生成虚函数表指针。
虚函数表指针在编译期就自动生成了,当定义函数中有一个virtual
这样的关键字,那么编译器自然而然就会为它去生成一个虚函数表。因此,在编译的时候就会把这个虚函数表的地址赋给相对应的对象,就是每一个Base
类的对象都会有一个虚函数表指针。那么虚函数表它是一个什么东西呢?虚函数表就是一个一维数组,这个一维数组当中都记录了这些虚函数的地址;通过偏移就可以调到相对应的方法。
对于Base
类而言,有func1
和func2
,子类class Subject
同样也会有,因为有virtual
关键字,并且基类也有virtual
关键字,所以编译器会为类生成一个虚函数表。当构造这个类的时候,同时会把这个虚函数表的指针赋给相对应的对象,并且会在这个类的最前面的位置,也就是子类也会有一个虚函数表指针,这个虚函数表指针它会指向一个一维数组,这个一维数组会记入了相对应的函数地址,如果没有去重写基类的方法的话,比如class Subject
类没有重写Base
类的func1
,那么父类和子类的func1
是相同的,因为没有重写。当出现重写时,比如class Subject
类重写了Base
类的func2
,会发生一个替换,会替换为子类自己的;子类自己的函数就会放在虚函数表的最后,比如这里的func3
是子类独有的,就会放在第三个位置。
怎么来体现多态的行为呢?这样:
Base *obj = new Subject();
需要注意,如果子类只是普通的继承,也就是Subject
就是普通的继承Base
,没有这种虚函数重写的话,它会实现一个早绑定(就是会把这个Subject
这个类型转换为Base
类型),如果没有这种虚函数不写,那么这条语句它是属于早绑定。如果是虚函数重写了,会为它生成这种虚函数表指针,上面的语句它就属于晚绑定(实际指向的是Subject
的对象),这是语言的一种机制。晚绑定的这个obj
指向的是Subject
的对象;早绑定的话这个obj
是指向的Base
的对象。
晚绑定中调这个func2
,实际调用的是Subject
对象的func2
,因为去找的这个虚函数表指针,然后找到这个虚函数表找到了相对应的方法,根据这个偏移就找到了它。这个就是晚绑定的一个原理,大量的设计模式都是需要依赖这个动态晚绑定的特性。
类的关系:
2.2、设计原则
前面说了设计模式,怎么现在又出来了一个设计原则呢?设计原则是设计模式还没有产生之前,它已经出来了,设计原则是我们以往一些程序在开发的过程当中总结了一些开发的原则,这些原则都是针对着封装继承多态,接下来跟大家介绍一下常见的九个设计原则有哪些。在项目当中,要知道一块代码,它到底符合什么样的设计原则。
(1)依赖倒置。
- 高层模块不应该依赖低层模块,两者都应该依赖抽象;
- 抽象不应该依赖具体实现,具体实现应该依赖于抽象。
用一句话来进行总结,就是实现要依赖接口,接口也可以把它转换成一个抽象。也就是说具体实现的代码需要去依赖这一个抽象,具体去使用的这一个接口的用户也需要依赖这一个抽象。这个接口就是通常比较核心的程序员,他会抽象很多很多的接口,一些普通的码农就去把这些接口去实现它就行了。那么对于用户而言,拿着这个接口去实现功能的时候,它也是要依赖这个抽象。也就是说我们主要解决依赖,导致主要解决了一个实现与我们客户使用的一个解耦,中间用一个抽象层,用接口来抽象这一段行为。
这里的定义就是设计模式的作者总结出来的,比较抽象,但是,如果有一定的代码量,肯定一下子能看明白。没有一定代码量的人呢,这里把它抽象并举了个例子:高层跟底层。
以自动驾驶为例子。自动驾驶系统公司是高层,汽车生产厂商为低层,它们不应该互相依赖,一方变动另一方也会跟着变动;而应该抽象一个自动驾驶行业标准,高层和低层都依赖它;这样以来就解耦了两方的变动;自动驾驶系统、汽车生产厂商都是具体实现,它们应该都依赖自动驾驶行业标准(抽象)。
通俗点说:
这里举了个自动驾驶的这样一个例子,有很多的车企,现在自动驾驶的技术(人工智能这种技术)越来越成熟,当大家想要去争抢这一块领域,都想在自己的车上面去实现这种自动驾驶的系统。假设,有一个自动驾驶系统这样的一个公司,又有很多的厂商,厂商跟这些自动驾驶的公司进行约定,要实现相对应的跟厂商汽车相关的这些自动驾驶的系统,最笨的办法就是这个自动驾驶系统根据每一个公司都去开发相对应的自动驾驶系统。
那么真正的在市场能够运行下来的一种方式是首先会有一个自动驾驶的标准,接下来,这些车企(可以把它想象成我们的底层实现)都需要依赖这一个标准(也就是我们具体的一个接口),就相当于一个用户要调这个接口去实现上层的一些逻辑,自动驾驶也按照这个标准去实现它的这个自动驾。
总结起来:依赖倒置就是说上次高层模块不应该依能依赖低层模块。换一句大家都能听懂的话就是接口的使用者不要依赖具体的实现,而应该要依赖具体的接口;具体的底层的实现,不要依赖用户的使用方式或者是使用习惯,也要依赖这个具体的接口,这样子上面的变化跟下面的变化就解偶了。
(2)开闭原则。一个类应该对扩展(组合和继承)开放,对修改关闭。开辟原则主要针对的是封装和多态。对扩展开放,开放什么东西呢?主要是指变化点,对要抽象出的东西要具体的去扩展它,对变化点的修改要关闭它。在这里会列一些设计原则,先快速的了解,要掌握则需要通过具体代码来理解设计原则,这样在具体写代码的时候,知道代表着一个什么含义,会比较的清晰这个概念。
(3)面向接口。
- 不将变量类型声明为某个特定的具体类,而是声明为某个接口;
- 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口;
- 减少系统中各部分的依赖关系,从而实现 “高内聚、松耦合” 的类型设计方案。
设计模式主要是面向接口编程,具体的实现要依赖接口,具体使用接口也不要依赖具体的实现。需要注意,设计原则不是一个人总结的,设计原则是有很多人很多人去总结的,所以肯定有重复的、有重叠的部分;但是,他们要有自己的侧重点,自己特别强调的一部分。
(4)封装变化点。将稳定点和变化点分离,扩展修改变化点;让稳定点和变化点的实现层次分离。通常是指两个类的依赖越少越好,把这些变化点尽量的不要去修改。跟开辟原则也有一点类似。看具体代码的时候,就知道他们分别指向什么,以及是一个什么场景。
(5)单一职责原则。一个类应该仅有一个引起它变化的原因。职责是一个抽象的概念,是一种变化方向,就是说一个封装的类,它的职责不要太多了,如果类的职责太多,它的变化点就很多。这个和封装变化点有点像,有很多重叠的部分,主要关注它主要针对的是什么就行了。单一职责原则主要是针对封装和多态。
(6)里氏替换。子类型必须能够替换掉它的父类型;主要出现在子类覆盖父类实现,原来使用父类型的程序可能出现错误;覆盖了父类方法却没有实现父类方法的职责。主要是说的多态,就是类的虚函数重写,如下图片中显示的有具体的重写代码func2
函数,子类的这个接口要实现他的职责。
(7)接口隔离。
- 不应该强迫客户依赖于它们不用的方法;
- 一般用于处理一个类拥有比较多的接口,而这些接口涉及到很多职责;
- 客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。
(8)组合优于继承。继承耦合度高,组合耦合度低。尽量的使用组合的方式,不要使用继承的方式,它主要是指封装和动态。
(9)最小知道原则。主要是针对封装,让用户不要去选择他不需要的接口,保证最小知道原则,就是对类的了解越少越好,只要知道几个简单的接口就行了,这就是封装巧妙的地方了。
三、如何学习设计模式
接下来探讨一下如何来学习设计模式。
3.1、明确目的
什么是明确目的呢?看这篇文章的朋友分为两类,第一类是刚刚才开始的新手,可能没有接触过具体的工程代码的书写;第二类是已经在公司当中开发多年了。那么就需要明确一个学习设计模式的一个目的。
- 第一个目是对于基础者的,也就是刚刚参与开发的朋友,要知道在现有的(一个公司,他已经有设计模式了)在使用的具体代码的基础上,扩展代码。就是现在在公司里去开发,那么公司的项目已经非常成熟了,很多的逻辑都已经用设计原则,设计模式已经封装好了,此时需要解决的问题是知道这个设计模式怎么在它的上面扩展代码。
- 第二个就是做功能抽象,这是开发多年的人群了,现在要做功能抽象,那么如何选择设计模式。
3.2、学习步骤
探讨学习的步骤,也是了解设计模式的一个步骤。
(1)设计模式解决了什么问题。注意,跟前面说的设计模式解决了什么问题是不一样的,这里讲的设计模式是具体的某一个设计模式。写一个设计模式,该设计模式解决了什么问题。那么解决什么问题呢?一定要知道它的稳定点是什么,选择这个设计模式来解决问题的稳定点是什么,以及它的变化点是什么,要分析出来,这个是第一步。
(2)该设计模式的代码结构是什么样子?也就是说未来在开发的过程当中看到一段代码,通过观察这个代码结构,快速的能够判定他是什么设计模式。这也是学习的一个目的,一定要非常非常清楚。学到的某一个设计模式,它的代码结构是什么好,这个代码结构包括C、C++语言,它来实现这个设计模式,通常采用什么样的一个结构来实现好,它的代码结构是什么,一定要清楚。
(3)符合哪些设计原则。这个一定要清楚,因为在平常开发的过程当中,一开始的时候,根本不知道这个业务未来是怎么变化,那怎么办呢?首先要先写符合设计原则的代码,当写代码符合设计原则的时候,未来想要把这个代码修改成为某一个设计模式只需要改少量,就可以把设符合设计原则的代码,把它转化成设计模式。符合哪些设计原则,这个就是为什么要学习符合哪些设计原则。
(4)如何扩展代码。就是在具体现有的代码上面,怎么去扩展代码。注意,通常这种扩展代码都是应对的一个变化点,大家注意它是一个变化点,一定要把这个变化点分析出来,具体设计模式,它到底解决了一个什么样的变化点,以及它的稳定点是什么。变化点通常是扩展的方式来进行修改,只有这个样子,才能够写少量改少量的代码,以应对未来需求的变化,这个必须要掌握好。
(5)该设计模式有哪些典型应用场景。学习设计模式的时候,一定要知道它有哪些典型应用,这个主要用来解决做知识迁移,学了设计模式,未来在工作当中怎么去使用它。为什么学习某一个具体知识的时候,一定要把它的典型场景应用,把它给整理一下呢?因为自己学习的时候,学的都是比较抽象的,通常第一步去理解他,理解只是说明你理解他了,知道他的一个逻辑关系了,从理解到能够自如的去使用它,就是做知识迁移,还有一个步骤的,即需要多思考、多实践才能够来实现知识迁移,多重复,重复、重复、不断的去重复,重复的理解。再加上去总结一些典型的应用场景,就可以做知识迁移。
设计模式有哪些典型应用,怎么学习呢?
- 可以联系目前自己的工作场景,然后去分析是不是适合某一个设计模式。工作场景中恰好要使用到某一个设计模式,然后去总结一下场景,为什么要使用这个设计模式,为未来的时候做知识迁移的时候好提供帮助。
- 或者在某一个开源框架当中去总结,他使用这个设计模式解决了什么问题,然后他是具体是怎么变化使用的。
总结
思维导图:
设计模式是软件开发中常用的解决问题的模板,它是通过对软件开发中经常遇到的问题进行总结和抽象产生的。设计模式的基础是面向对象思想和设计原则,通过对这些基础知识的理解,可以更好地学习和应用设计模式。为了有效地学习设计模式,首先需要明确学习的目的,然后按照一定的学习步骤系统学习,才能够真正掌握设计模式的精髓。通过本文的介绍,读者将更加深入地认识设计模式以及学习设计模式的方法。
书籍推荐:
- 《设计模式-可复用面向对象软件的基础》
- 《重构与模式》