设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案(设计思想、设计经验)。
一、六大原则
1、单一职责原则(Single Responsibility Principle)
类的职责应该单一,一个方法只做一件事。
(1)使用建议
两个完全不⼀样的功能不应该放一个类中,一个类中应该是一组相关性很高的函数、数据的封装。
(2)用例
网络聊天:网络络通信 & 聊天,应该分割成为网络通信类 & 聊天类。
2、开闭原则(Open Closed Principle)
对扩展开放,对修改封闭。
(1)使用建议
对软件实体的改动,最好用扩展而非修改的方式。
(2)用例
超时卖货:商品价格 —— 不是修改商品的原来价格,而是新增促销价格。
3、里氏替换原则(Liskov Substitution Principle)
就是只要父类能出现的地方,子类就可以出现,而且替换为子类也不会产生任何错误或异常。
在继承类时,一定要重写父类中所有的方法,尤其需要注意父类的 protected 方法,子类尽量不要暴露自己的 public 方法供外界调用。
(1)使用建议
子类必须完全实现父类的方法,子类可以有自己的个性。覆盖或实现父类的方法时,输入参数可以被放大,输出可以缩小(为了父类替换成子类时不会出错)
(2)用例
跑步运动员类 —— 会跑步,子类长跑运动员 —— 会跑步且擅长长跑,子类短跑运动员 —— 会跑步且擅长短跑
4、依赖倒置原则(Dependence Inversion Principle)
高层模块不应该依赖低层模块,两者都应该依赖其抽象。不可分割的原子逻辑就是低层模式,原子逻辑组装成的就是高层模块。
模块间依赖通过抽象(接口)发生,具体类之间不直接依赖。
(1)使用建议
- 每个类都尽量有抽象类,任何类都不应该从具体类派生。
- 尽量不要重写基类的方法(结合里氏替换原则使用)。
(2)用例
奔驰车司机类 —— 只能开奔驰; 司机类 —— 给什么车,就开什么车; 开车的人:司机 —— 依赖于抽象
5、迪米特法则(Law of Demeter)
又叫 “最少知道法则”。
尽量减少对象之间的交互,从而减小类之间的耦合。一个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:只和直接的朋友交流, 朋友之间也是有距离的。自己的就是自己的(如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就将其放置在本类中)
(1)用例
老师让班长点名 —— 老师给班长一个名单,班长完成点名勾选,返回结果,而不是班长点名,老师勾选。
6、接口隔离原则(Interface Segregation Principle)
客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。
(1)使用建议
接口设计尽量精简单一,但是不要对外暴露没有实际意义的接口。
(2)用例
修改密码,不应该提供修改用户信息接口,而就是单一的最小修改密码接口,更不要暴露数据库操作。
二、单例模式
一个类只能创建一个对象,即单例模式,该设计模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
- 全局只有一个实例对象,所以将单例对象放在静态区 / 堆区(保证只创建一次)。
- 为了防止其他位置创建该对象,将构造函数私有。
- 为了防止拷贝,使用 delete 修饰拷贝构造和赋值运算符重载函数。
1、饿汉模式(以空间换时间)
程序启动时就会创建一个唯一的实例对象。 因为单例对象已经确定, 所以比较适用于多线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争,提高性能 / 响应速度。
静态成员变量只能在类域外进行定义初始化,所以在 main 函数之前就将单例对象定义初始化,此时该单例对象创建在静态区上,而且仅有一个,后面就无法再创建。
想要获取该单例对象只能通过静态成员函数 getInstance() 来获取,静态成员函数可以直接访问静态成员变量 _data。
静态对象是在静态区的,它的生命周期是随着整个程序的,它的初始化构造是在程序初始化阶段完成的。
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
(1)优点
- 保证全局(整个进程)只有唯一实例对象。
- 饿汉模式一开始就创建对象,特别简单。
(2)缺点
- 假设有多个单例对象 A、B、C,要求它们之间有依赖关系,如果依次创建就无法达到,无法保证顺序可能会导致进程启动速度很慢。
为什么会称之为饿汉模式呢?
不管后面是否会用到这个单例对象,在程序一启动且还没有进入 main 函数之前就创建一个唯一的实例对象。这个过程就像一个饿汉一样,一上来就先吃(创建单例对象),所以称之为 “饿汉模式”。
2、懒汉模式
第一次使用要使用单例对象的时候创建实例对象。如果单例对象构造特别耗费时间或者资源(加载插件、加载网络资源等),可以选择懒汉模式,在第一次使用的时候才创建对象。
如果保证在使用单例对象时才进行实例化呢?不是直接实例化对象,而是定义一个对象的指针(静态资源指针),然后再通过访问接口时发现它为空指针,接着再进行 new 对象。但是这样做会出现一个有关线程安全的问题(当多线程时,进行判断为 nullptr,这时还没有调用 new,当前的线程就被切走了。下一个线程来了还是 nullptr 就又进去 new 了一个对象,然后恢复第一个线程的上下文后又 new 了一个对象,第二个 new 的就将第一个的给覆盖了,所以就出现了错误),这里就可以使用互斥锁(因为加锁需要加在一个锁上才有用,所以我们也要将锁设为静态的,然后在类外进行初始化,这样就保证了只实例化出一个对象来)。但是加锁之后又造成了锁冲突,导致串行化进行(当 new 出一个对象之后,在进行调用 getInstance 的时候,还会不停的加锁解锁),效率降低(加锁解锁是有性能消耗的),所以有了 双检测加锁(double check 检测)。但是又涉及到了代码指令顺序的问题,所以得加上一个 v 关键字来修饰。以上需要关注的因素很多,下面不采取这种方式。了解更多可参考:【C++】特殊类设计-CSDN博客
- 《Effective C++》的作者 Scott Meyers 提出的一种优雅简便的单例模式 Meyers' Singleton in C++。
- C++11 Static local variables 特性以确保 C++11 起,静态变量将能够在满足 thread-safe 的前提下唯一地被构造和析构。
翻译:
如果多个线程试图同时初始化同一个静态局部变量,则初始化只会发生一次(使用std::call_once可以为任意函数获得类似的行为)。
此功能的通常实现使用双重检查锁定模式的变体,这将已经初始化的局部静态的运行时开销减少到单个非原子布尔比较。
(1)优点
- 第一次使用实例对象时创建对象。
- 进程启动无负载。
- 多个单例实例启动顺序(通过代码顺序)自由控制。
(2)缺点
- 复杂。
- 如果不加锁是会出现线程安全的问题。但是加锁是会十分影响性能的,所以引入了双检查。那么既要保证线程安全 + 又要保证效率的问题。(原先做法)
为什么称之为懒汉模式呢?
懒汉模式又叫做延时加载模式。如果单例对象构造十分耗时或者占用很多资源,比如加载插件、初始化网络连接、读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢,所以这种情况使用懒汉模式(延迟加载)更好。
三、工厂模式
工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,以此实现创建 —— 使用的分离。
1、简单工厂模式
简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。
假设有一个工厂能生产出水果,当客户需要产品的时候就明确告知工厂要生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。
通过参数控制可以生产任何产品。
这个模式的结构和管理产品对象的方式十分简单,但是它的扩展性非常差,当需要新增产品的时候,就需要去修改工厂类新增一个类型的产品创建逻辑,违背了开闭原则。
在继承中要构成多态还有 2 个条件:
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
virtual void name() = 0; 是一个纯虚函数的声明,表示这个函数在基类中没有具体实现。基类 Fruit 包含了纯虚函数,它不能被实例化,因此它是一个抽象类。name() 方法在 Fruit 类中没有实现,任何继承自 Fruit 的类都必须提供 name() 的具体实现。
(1)优点
- 简单粗暴,易于理解。使用一个工厂生产同一等级结构下的任意产品。
(2)缺点
- 所有东西生产在一起,产品太多的话会导致代码量庞大。
- 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须要修改工厂方法。
2、工厂方法模式
在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂。
假设现在有 A、B 两种产品,则开两个工厂,工厂 A 负责生产产品 A,工厂 B 负责生产产品 B,用户只知道产品的工厂名,而不知道具体的产品信息,工厂不需要再接收客户的产品类别,而只负责生产产品。
定义一个创建对象的接口,但是由子类来决定创建哪种对象,使用多个工厂分别生产指定的固定产品。
工厂方法模式每次增加一个产品时,都需要增加一个具体产品类和工厂类,这会使得系统中类的个数成倍增加,在一定程度上增加了系统的耦合度。
(1)优点
- 减轻了工厂类的负担,将某类产品的生产交给指定的工厂来进行。
- 开闭原则遵循较好,添加新产品只需要新增产品的工厂即可,不需要修改原先的工厂类。
(2)缺点
- 对于某种可以形成一组产品族的情况处理较为复杂,需要创建大量的工厂类。
3、抽象工厂模式
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时可以考虑将⼀些相关的产品组成⼀个产品族(位于不同的产品等级结构中,功能相关联的产品组成的家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。
围绕一个超级工厂创建其他工厂。每个生成的工厂按照工厂模式提供对象。
(1)思想
将工厂抽象成两层:抽象工厂 & 具体工厂子类,在工厂子类种生产不同类型的子产品。
抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,违背了 “开闭原则”。
四、建造者模式
建造者模式是⼀种创建型设计模式,使用多个简单的对象一步一步构建成一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题。
建造者模式主要基于五个核心类实现:
- 抽象产品类。
- 具体产品类:⼀个具体的产品对象类。
- 抽象 Builder 类:创建一个产品对象所需的各个部件的抽象接口。
- 具体产品的 Builder 类:实现抽象接口,构建和组装各个部件。
- 指挥者 Director 类:统一组建过程,提供给调用者使用,通过指挥者来构造产品。
五、代理模式
代理模式指代理控制对其他对象的访问,也就是代理对象控制对原对象的引用。在某些情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用。代理模式的结构包括:一个是真正你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。
以租房为例,房东将房子租出去,但是要租房子出去,需要发布招租启示,带人看房,负责维修,这些工作中有些操作并非房东能完成,因此房东为了省事,将房子委托给中介进行租赁。
1、静态代理
在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
2、动态代理
在运行时才动态生成代理类,并将其与被代理类绑定。这就意味着,在运行时才能确定代理类要代理的是哪个被代理类。