设计模式
目录
六大设计原则主要是指:
单一职责原则(Single Responsibility Principle)
- 更容易测试:具有一种职责的类将具有更少的测试用例,从而导致测试工作量大大减少。
- 松耦合 : 单个类中的功能越少,依赖性就越少。因此,它将降低耦合。
- 更容易读 : 较小的、组织良好的类比较大的类更容易被初次阅读代码的人搜索到。
- 更容易使用 :减少了错误的数量,提高了开发速度,并使你作为程序员,上班摸鱼的时间大大的增加。
- 更容易维护: 通过确保您的类只有一个责任,您可以节省大量开发应用程序的工作并创建更易于维护的体系结构。
过度使用单一职责原则的缺点
-
增加代码的数量:因为需要分解一个大类为多个小类。当每个小类功能都比较简单时,类的数量将会显著增加,使得代码量更大。
-
增加代码的复杂度:严格遵循单一职责原则并不总是可能的,有时候职责之间的联系是紧密的,分离职责会使得代码的逻辑更加复杂。
-
增加开发、维护成本:过度分解一个大类为多个小类可能会增加系统的开发和维护的成本,需要仔细权衡利弊。
单一职责原则:比如有一个检查当前状态的结构体这个函数,把视频和图片统一按视频去处理,同时检查完之后,比如时间出错了,文件段的序号出错了,他又调用对应的恢复函数,然后这个恢复的函数又会调用修改的函数写进硬盘里。相当于这个名为检查的函数干了检查,恢复,修改这三个的功能,一开始我修改起来很难。不够单从当时编写代码的角度来考虑,我想是有几个优点的。他把图片和视频在很多函数中是统一处理的,直接强转成视频的结构体,因为前面一部分重要的信息是相同的,直接用就可以了,同时他函数的复用率是比较高的,这点是拿易读和结构做了一定的牺牲。我把他分开了,让一个函数尽量干他命名的事情,也方便后面维护。虽然整体代码量会多一些,但更加易读,同时你就算删了这个函数也没问题,整个功能都能跑通,但是你原来就不行,因为他还有一些结构体里的状态字在里面发生了改变。
接口隔离原则(Interface Segregation Principle)
接口隔离原则就是客户端不应该依赖它不需要的接口,或者说类间的依赖关系应该建立在最小的接口上。
- 一个类对另一个类的依赖应该建立在最小接口之上。
- 建立单一接口,不要建立庞大臃肿的接口。
- 尽量细化接口,接口中的方法尽量少(适度)。
对于一个把数据写进硬盘的操作函数来说,如果现在新加进来一种结构,比如原来是视频,图片,现在加进来ai便签了,你需要改这个写操作的函数为了支持他,而视频和图片用不上这新加进来代码,那这个接口隔离就做的不够好了,可以再细化一层,让最后的写操作不会受到新加进来的数据结构的影响。其实就有点像linux的write函数了。
开闭原则(Open Closed Principle)
开闭原则才是精髓,其他五个都是对他的具体实现。一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。也就是说,我们应该通过扩展来实现软件功能的增加,而不是通过修改现有代码来实现,这样可以提高软件的稳定性和可复用性。
在C语言中,我们可以利用函数指针、接口结构体实现开闭原则。无论是利用函数指针还是接口结构体,实现开闭原则的核心思路都是将可变部分抽象出来,从而达到避免修改代码而扩展功能的目的。
适当使用开闭原则的优点
- 可扩展性:遵循开闭原则,软件系统可以更容易地添加新的功能模块,而不会对原有模块产生任何影响。
- 可维护性:遵循开闭原则的代码更容易维护。因为修改一个软件设计时需要充分考虑该代码对其他模块的影响,迫使程序员写出更加模块化、可维护的代码。
- 代码复用:开闭原则可以鼓励我们构建一个可重用、通用的代码库,因为新的需求只需要扩展而不需要更改原有代码。
- 提高软件质量:遵循开闭原则可以使得代码更加灵活,不需要修改原有的代码,只需要增加新的代码,有利于提高软件质量。
过度使用开闭原则的缺点
-
增加代码的复杂度:开闭原则可能导致代码过度设计,过多的使用抽象和接口,增加代码复杂度。
-
增加开发时间及成本:在某些情况下,为了扩展一个软件实体,需要大量的代码重构。这在一些情况下是必要的,但在其他情况下可能会增加开发时间和成本。
这个我在业务代码中也有看到过符合这样的设计,通过封装结构体,在里面放回调函数实现,比如一个函数要检测码流是否正常,码流有网络码流,本地码流。我需要通过回调传给他不同的函数就能在一个对外接口上实现扩展,以后还有别的码流,传进去对应的处理码流的函数就可以了,其实这个整体的实现也属于是依赖倒置原则了
里氏替换原则(Liskov Substitution Principle)
LSP要求在软件代码中,父类对象出现的地方被替换成子类可以无差错的运行。换言之,引用基类的地方必须能够透明的使用子类对象,行为不会改变。
- 继承体系模塑的应该是IS-A关系,比如猫是一种动物,子类对象可以完全替代父类对象。
- 应把基类设计成抽象类,而非具象类;应从抽象类派生子类,而不应从具象类继承。
- 子类应该实现父类的抽象方法,而不应该重写父类的已实现方法。
- 子类可以扩充新的功能,而不应该改变父类已有的功能。
- 子类不能增添任何父类没有的附加约束。
虚函数是一种C++中常用的实现里氏替换原则的方法。在C语言中也可以通过函数指针来模拟虚函数的实现。我们可以定义一个父类结构体,其中包含一些函数指针,子类可以根据自己的需求指向不同的函数,从而实现不同的行为,这也是一种里氏替换原则的应用。
迪米特法则(Law of Demeter),又叫“最少知道法则”
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
大主管和主管,主管和开发人员
这个我倒没有多少理解,因为c语言中主要通过尽量少的对外接口实现这个功能,尽量将方法定义成static,这是我个人的理解
依赖倒置原则(Dependence Inversion Principle)
依赖倒置:就是要依赖于抽象,不要依赖于具体。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
适当应用依赖倒置原则的优点
- 降低了模块之间的耦合度:提高了系统的灵活性和可维护性;
- 提高了代码的可读性:通过面向抽象编程,让代码更加易于理解;
- 降低了开发的难度:提高了开发的效率;
过度使用依赖倒置原则的缺点
- 可能会增加代码的复杂度和难度:需要在设计和实现的过程中进行充分考虑和把握;
- 影响代码开发效率:需要合理规划抽象接口的设计,过于精细的接口设计可能会影响代码的效率和开发效率;
这个例子是和师傅是看到相关代码讨论的时候聊的,具体是设备中有有线网卡,wifi网卡,4G网卡。当有线和wifi其中一个连接后,要关掉4g。
设计1:在4g模块中判断有线和无线是否连接,4g连接模块直接就依赖了这两个模块了。
设计2:抽象出一个其他网卡接口,其他网卡连接时,则退出4g模块
设计3:再从业务上分析,业务的本质时要不要开启4g,如果以后为了高码率视频引入了5g网卡,那对外的接口就要修改了,可以对外提供一个enable接口。这样就不需要依赖其他接口了。
设计4:把4g功能放到网络状态管理框架里,及网卡切换时。关闭优先级低的网卡,相当于是把4g放到了优先级最低了。
设计模式
创建型模式
对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。
工厂模式
我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
举例:通过传入不同类型来返回不同的对象,例如媒体有音频,视频,图片
抽象工厂模式
抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式同时解决了两个问题, 所以违反了单一职责原则:
- 将默认构造函数设为私有, 防止其他对象使用单例类的
new
运算符。 - 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。
如数据库实例。
建造者模式
使用多个简单的对象一步一步构建成一个复杂的对象
我们来思考如何创建一个 房屋
House对象。 建造一栋简单的房屋, 首先你需要建造四面墙和地板, 安装房门和一套窗户, 然后再建造一个屋顶。 但是如果你想要一栋更宽敞更明亮的房屋, 还要有院子和其他设施 (例如暖气、 排水和供电设备), 那又该怎么办呢?
同时可以定义主管类,对客户端隐藏创建细节。主管类里面以特定构造步骤的顺序创建产品和配置产品。
如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。
基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。
原型模式
是用于创建重复的对象,同时又能保证性能
举例:例如我有一个很复杂的类,我现在有一个已经构建好的对象了,我可以直接clone他,然后再稍微修改一些参数久可以直接用了。
并非所有对象都能通过这种方式进行复制, 因为有些对象可能拥有私有成员变量, 它们在对象本身以外是不可见的。还有一个问题你必须知道对象所属的类才能创建复制品, 所以代码必须依赖该类。 即使你可以接受额外的依赖性, 那还有另外一个问题: 有时你只知道对象所实现的接口, 而不知道其所属的具体类, 比如可向方法的某个参数传入实现了某个接口的任何对象。
结构型模式
把类或对象结合在一起形成一个更大的结构。
适配器模式
是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。
主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况。
举例:
类似转接器,比如usb转sata接口,这个转接器就有这个功能,你实现了usb转成sata的驱动就可以用电脑中原有的sata驱动了
类似电源适配器,不同国家的不一样,但产品是一样的
装饰者模式
允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
动态的将新功能附加到对象上。
装饰模式是一种用于代替继承的技术 ,无须通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。
代理模式
能为程序库、 框架或其他复杂类提供一个简单的接口。(静态代理,动态代理)
两个对象直接通信中提供控制访问(为了安全原因)的作用。用代理模式并不是为了源对象和目的对象的通信复杂,而是为了控制访问或负载均衡等目的。
算是一个中介,他掌握了他这一行业的资源,比如要找女朋友,中介那有各式各样的。你只需要找他就行了,而女朋友也是通过中介和你联系。这样对于客户端和服务端就分开了。
外观模式
主要是为了隐藏系统的复杂性,能为程序库、 框架或其他复杂类提供一个简单的接口。
ffmpeg中有这个思想在里面,当我们需要生成视频文件,我传入名字和路径就可以生成。同时会帮我配置一些基本信息
桥接模式:
可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而能在开发时分别使用。
问题的根本原因在于我们试图在两个独立的维度——形状与颜色上进行扩展。这在处理继承时是很常见的问题。
通过将继承改为组合的方式来解决这个问题。 具体来说, 就是抽取其中一个维度并使之成为独立的类层次, 这样就可以在初始类中引用这个新层次的对象, 从而使得一个类不必拥有所有的状态和行为。
桥接模式通过将继承改为组合的方式来解决这个问题。 具体来说, 就是抽取其中一个维度并使之成为独立的类层次, 这样就可以在初始类中引用这个新层次的对象, 从而使得一个类不必拥有所有的状态和行为。
将m*n个实现类转换为m+n个实现类
要求开发者一开始就针对抽象层进行设计与编程。
组合模式:
你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。(树形递归思想)
- 需求中体现的是部分与整体层次的结构时,如实现树状结构,可以使用组合模式
- 希望用户可以忽略组合对象与单个对象的不同,统一的使用组合结构中的所有对象时,就应该考虑用组合模式了
组合模式中定义的所有元素共用同一个接口。 在这一接口的帮助下, 客户端不必在意其所使用的对象的具体类。
享元模式:
在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。
简单来说,我们抽取出一个对象的外部状态(不能共享)和内部状态(可以共享)。然后根据外部状态的决定是否创建内部状态对象。内部状态对象是通过哈希表保存的,当外部状态相同的时候,不再重复的创建内部状态对象,从而减少要创建对象的数量。
行为型模式
类和对象如何交互,及划分责任和算法。
策略模式
定义: 策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
模板方法模式
定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。
模板方法模式是把一些重复的结构框架,步骤放到父类及抽象类中。而把以一些有差异的地方定义为抽象函数。让子类去实现。
观察者模式
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。
观察者模式解决的是一个对象状态改变时,如何自动通知其他依赖对象的问题,同时保持对象间的低耦合和高协作性。
迭代器模式
提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
C++STL中用的很多
责任链模式
使多个对象都有机会处理同一请求,从而避免请求的发送者和接受者之间的耦合关系,每个对象都是一个处理节点,将这些对象连成一条链,并沿着这条链传递该请求。
命令模式
将请求封装为一个对象,允许用户使用不同的请求对客户端进行参数化。
解决在软件系统中请求者和执行者之间的紧耦合问题,特别是在需要对行为进行记录、撤销/重做或事务处理等场景。
备忘录模式
允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
思考一下这些状态快照。 首先, 到底该如何生成一个快照呢? 很可能你会需要遍历对象的所有成员变量并将其数值复制保存。 但只有当对象对其内容没有严格访问权限限制的情况下, 你才能使用该方式。 不过很遗憾, 绝大部分对象会使用私有成员变量来存储重要数据, 这样别人就无法轻易查看其中的内容。
备忘录模式将创建状态快照的工作委派给实际状态的拥有者原发器对象。 这样其他对象就不再需要从 “外部” 复制编辑器状态了, 编辑器类拥有其状态的完全访问权, 因此可以自行生成快照。
状态模式
定义: 在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
简单理解,一个拥有状态的context对象,在不同的状态下,其行为会发生改变。
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用:代码中包含大量与对象状态有关的条件语句。
如何解决:将各种具体的状态类抽象出来。
访问者模式
中介者模式:
能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。
中介者模式是要是为了降低两个对象之间直接通信设计上的耦合度。源对象不需要知道被调用服务方的具体类,只需要调用中介者类,由中介者完成指向具体的类。
当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。该模式让你将对象间的所有关系抽取成为一个单独的类, 以使对于特定组件的修改工作独立于其他组件。
解释器模式
在软件开发中,会遇到有些问题多次重复出现,而且有一定的相似性和规律性。如果将它们归纳成一种简单的语言,那么这些问题实例将是该语言的一些句子,这样就可以用“编译原理”中的解释器模式来实现了。
如同正则表达式,他是一种"迷你语言"。
解释器模式实现的关键是定义文法规则、设计终结符类与非终结符类、画出结构图,必要时构建语法树
各个模式之间的区别
外观模式,中介者模式、代理模式的区别
外观模式:针对客户端和子系统之间,通过一个高层接口简化客户端访问子系统的复杂度
中介者模式:针对子系统内部模块之间,通过一个中介者简化各对象间的相互引用
代理模式:用一个代理类代表另一个类的功能,但是不改变被代理类的功能。目的是控制对被代理类的访问。