C++面向对象设计模式

什么是设计模式

每一个模式描述了一个在我们周围不断重复发生问题以及该问题的解决方案核心。这样你就可以一次又一次的使用该方案而不必要做重复的劳动。

面向对象的两种思想

底层思维:语言实现方面的思想,封装,继承,多态。
抽象思维:隔离变化(将变化带来的影响减为最小),各司其职(接口一样,但是具体实现不一样),

解决复杂性的问题

分解问题:将复杂的问题分解为许多简单的问题,一个一个解决
抽象问题:把握问题的核心思想,构建范化模型,先不考虑细节。

变化 和 复用

在设计模式中,要保证不断变化的过程中,程序的复用性还是能够支持变化
在对这个事物进行抽象之后,抽象后的模型中越不变化越好。也就是细节上的处理要放到对应的具体模型中。

面向对象的原则

依赖倒置原则(DIP)

高层模块(稳定)不应该依赖底层模块(变化),二者都应该依赖于抽象(稳定)。
	理解:两个不同的模块不应该有关联,两个模块应该通过抽象模型进行关联。
抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)。
	理解:抽象模型不能出现细节的处理,即抽象的模型不能考虑也不会调用具体的实现。

开放封闭原则(OCP)

对扩展开放,对更改封闭。
	理解:出现新的变化考虑怎加,而不是考虑重构。
类模块应该是可以扩展的,但是不可以修改的。

单一职责原则(SRP)

一个类应该仅有一个引起它变化的原因。
变化的方向隐含着类的责任。
	理解:一个类的功能越简单,其责任的方向就越明确,当一个类的功能太多可以考虑分成多个类。

Liskov替换原则(LSP)

子类(派生类)必须能够替换的他们的父类(基类)(is-A).
继承表达类抽象。
	理解:派生类必须能够完成基类的所有功能,不能去除基类的功能。如果想要去除基类功能应该考虑使用组合。

接口隔离原则(ISP)

不应该强迫客户程序依赖他们不用的方法。
接口应该小而完备。
	理解:在接口的实现上,类的函数成员如果是自己使用那么就使用private,只给子类使用那么就protected,给客户程序使用才能用public。不能暴露太多依赖给客户选择,最好单一。
	进一步的理解:
		接口隔离是客户需要什么方法,提供相应的方法即可,有时我们实现的接口共功能丰富,但是客户需要的接口功能只是我们已经实现的接口中的某几个,我们就需要使用接口隔离。

优先使用对象组合,而不是类继承。

类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
继承在某种程度上破坏了封装性,子类父类耦合度高。
而对象的组合则只要求被组合的对象具有良好定义的接口,耦合度低。

封装变化点

使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生影响,从而实现层次间的松耦合。
	理解:在底层的思想中封装是封装数据和方法,但是在抽象的思维中封装是封装变化点。

针对接口编程,而不是针对实现编程。

不将变量的类型声明为某个特定的具体类,而是声明某个接口。
客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
减少系统中各部分的依赖关系,从而实现“高内聚,松耦合”的类型设计方案。

重构模式

虽然23三种设计模式是今典的,但是使用者应该针对具体问题进行重构,重构技巧:

静态–>动态

早绑定–>晚绑定

早绑定:晚实现的程序调用早实现的程序。
晚绑定:早实现的程序调用晚实现的程序。
早实现时稳定,且难以修改。晚实现可以容错,所以在实现过程中,要达到晚实现的程序配合早实现的程序开发。

继承–> 组合

编译时依赖–> 运行时依赖

紧耦合–>松耦合

模式分类

从目的来看

创建型模式:解决对象创建工作
结构型模式:需求变化时,使用继承还是对象组合。
行为型模式:对象与对象之间的职责。

从范围来看

类模式:处理类与子类的静态关系
对象模式:处理对象间的动态关系

从封装变化角度对模式进行分类

组件协作类:
Template Method (框架与应用问题,主流程和分支关系)
C++:基类从稳定的流程中,提炼出变化点,做成虚函数留给子类继承。
C :主流程预留一些函数接口或是回调函数,后续具体实现。
体现重构思想:早绑定-->晚绑定。(不用调用我,让我来调用你)
体现原则:依赖倒置原则,具体细节依赖于抽象。底层依赖于高层模块。
Template Method 的要点就是基类已经是实现了主流程,但是分支流程会根据不一样的具体事物,不一样的操作,留出虚函数接口,使后续的派生类重载。
Strategy(算法问题,多个实现算法)
C++:将策略(方法)从类中提取出来当作基类,让具体的策略(方法)继承基类。从而实现多态。
体现原则:开放封闭原则,单一性原则,封装变化点原则,将变化点提取出来,将各个功能提取出各个类,每个类的责任明确。
Strategy 的要点是 当出现 if(){...} if else { ...}多变时 表示考虑使用策略模式。策略模式简单直观就是消除 if(){...} if else { ...},该语法是 C 语言时代的分而治之思想。
Observer / Event (框架与应用问题,一个对象与多个对象关系)
	在增加一个需求时,我们都想着在已有的类中增加一个具体对象。这违背了依赖导致原则,这个对象就是一个具体的实现,当这个对象出现变动时,我们又需要更改对象。
	创建一个抽象观察者类,将观察者对应的方法提取出来,创建一个通知者类,通知者抽象类对应通知观察者类。具体的通知者继承抽象通知者类,具体的观察者继承抽象通知者,具体通知者提供添加观察者接口,具体通知者利用抽象通知接口通知抽象观察者,抽象观察者通过多态对应到具体的类。
	Observer 的要点是通知者不需要知道观察者是什么具体对象,通知者根据 容器指针 不停的通知观察者,观察者将自身的地址放入通知者的 容器指针。
单一职责:
Decorator(类的主要变化和修饰变化问题)
以 IO流为例:流分别有文件流,网络流,内存流。文件流又有缓存文件流,加密文件流,网络流也有缓存网络流,加密网络流,等等。当我们实现各个流对象的时,会发现有越来越多的类被创建。
修饰模式:区分哪些是主要类,哪些是修饰类。主类和修饰类在编译时就已经确认好,但是他们合成的类在运行时,根据需要组合成为各种不一样的类,设计模式提倡在运行时确定关系,比在编译时确认关系好。
修饰模式的要点:我需要修饰你,就必须有你一样的方法(继承你 + 虚函数),给客户一种使用不变的感觉,在方法中增加自己的修饰,但是我只是修饰你,你的方法还是需要执行的,所以又要包含你(组合+指针指向你),在我的修饰完成后调用你的方法。
Bridge (一个类中出现两者以上的变话方向)
以一个聊天窗口为例,假设聊天窗口有界面显示功能,和发送数据功能,简单版实现就是他们是一个聊天窗口类,但是具体实现又分为手机办聊天窗口(继承聊天窗口),电脑版聊天窗口(继承聊天窗口),在手机版聊天窗口会根据手机来做一些发消息会震动(继承手机版),和发送消息不振震动功能(继承手机版),电脑版也一样。这样子会照成很多类的创建(在编译时创建)。
在Decorator 模式中,要把类的创建放在运行时,那就时用组合的方法,但是会发现聊天窗口会难以继承。在设计原则中,我们的类最好要有单一职责的原则,所以将界面显示功能独立为一个类,发送数据功能独立为一个类。两个类的变化不相互影响,但是两者要有组合的关系,就是一个类的基类有指向另一个类的基类指针,将两种变化绑定在了一起,就是 Bridge(桥)模式。
桥模式的要点就是,将类的功能单一话,且他们基类要有组合(一个类中由指向另一个类的指针)的关系。当出现多个变化纬度,就用多个指针指向各个变化纬度的基类。
对象创建:解决 new 创建对象的紧耦合
Factory Method (需求变化创建对象的类型会发生变化)
在八大原则中,要面向接口编程而不是针对具体细节编程,和依赖倒置原则中,具体细节应该依赖抽象。所以在创建对象时,我们不能针对具体细节去创建。
Factory Method 要点 创建一个虚工厂类,类中有一个纯虚函数,让具体的类去继承这个接口。在具体类的虚函数中实现这个创建。工厂创建不是完全解决具体的创建对象问题,而是将具体创建对象延后 或是  不在该类中具体实例化,最后还是有地方要具体创建。出于内存管理,虚工厂类还得提供内存得释放。
缺点:创建的对象参数要相同,
Abstract Factory(一系列相互依赖的对象)
在一个类中组合了多个对象,且类内的多个对象是相互关联的,当类内的对象创建时步对应就会出现问题,Abstract Factory 就是为了解决创建类内多个一系列相互关联的对象。
Abstruct Factory :要点是将相关性的对象的创建放在一个工厂类中,不同的方法创建不同的对象。实现了一系列的类的高内聚。在类中组合的多对象创建时就只传一个工厂类。
优缺点:可以解决一系列的类的变化,不能解决一系列对象的变化,就是这一系类的属性如果变动可以满足,但是在这一系列的对象个数发生变化那么就不满足。
Prototype(克隆,比较复杂的对象,且具有状态需求)
Prototype 放方法和 Factory Method 的区别在与 Prototype 将 工厂基类和接口基类合并在一起。 提供了创建自己。
用法上:factory method 在创建的时候比较简单,但是 Prototype在创建一个结构化复杂的类时比较好用,且需要记录一定状态时,使用Prototype会更好。
Builder(分步骤构建过程,步骤稳定,各个步骤不稳定)
通过一系列的算法,创建出一个对象。三个基类:接口对象基类(A),针对接口创建对象的基类(一系列算法的接口)(B),固定创建对象的顺序基类(C)。 C类创建时接受B类的派生类对象,根据方法顺序调用C类的派生类方法,创建出 A类的派生类对象。
Builder 使用在 一个分步骤构建一个对象,且构建过程是稳定的,构建过程的方法是不稳地的。
Prototype 是一次性构建一个复杂对象,且需要对象的某些状态
Factory Method 是一次性构建一个对象,对象比较简单。
对象性能:
Singleton(单例)
为了性能的保障,我们要确保使用者始终使用的是一个对象,设计者要确保使用者不管怎么使用都会只获取到一个对象。
将该对象的 构造函数和拷贝构造函数设计为private(派生则为protected)。移动构造不设置私有是因为函数返回时需要使用到。一般这种对象是在堆中。
经验:在加锁的地方如果有判断,可以使用两次判断,把锁提到第一次判断的里面。
		但是会发生 内存读写reoder的安全隐患(编译器会优化指令执行顺序,我们正常的思维是对象诞生,才会复制给指针,但是汇编在运行时可能是先赋值指针,在构造对象,会产生其他线程在对象没有诞生就使用现象)
		c++关键字  volatile 告诉编译器不要优化。 volatile 告诉编译器该变量易变性,不可优化性,按顺序性。
Flyweight (大量细粒度对象充斥系统)
例子:在文字显示中,每一个字都需要有一种字体与之关联,但是文字有多种多样,字体形式是可以被复用。在实现上一个文字对象应该有一个指针,指向字体得接口对象,每次创建一个文字都会实例化字体。这时候可以使用轻量级模式,使用多个文字与一个字体相关。
要点:Flyweight 模式的前提是对象是只读得
接口隔离:(稳定和变化的隔离,两个接口关系太强,但是两者都会有变动,可以增加一层稳定的接口,形成接口隔离)
Facade(门面,内部提供给外部需求)
在客户和组件的子系统有了过多的耦合,随着客户的需求变化,客户的程序和子系统的组件相应的也有需求变化,可以使用门面设计,将客户程序的需求提交到门面接口,由门面接口与子系统接口进接协调。
理解:在需求中,我们为客户提供了接口需求,但是随着技术的发展,我们内部程序可能需要优化性能,版本做了迭代,客户需求的接口不可能发生改变,客户也不可能会做改变。我们使用门面设,客户依赖的是稳定的门面接口,与具体的内部实现无关。
Facade 要求内部接口要由相互关联。松耦合,(Facade 内部)高内聚。
Proxy(代理,安全问题,或是无法相互访问)
在分布式系统中,会出现无法访问的方式,或是关于安全隐患的问题,可以使用代理模式。
Facade 是因为 便于管理而使用,Proxy 是因为无法实现而产生,但是两者都是实现了进接的访问。
Mediator(管理对象之间的关联,多个对象相互关联)
A接口的派生出两个类B 和 C,但是B 和 C这两个类想要相互依赖,但是如果两个类相互依赖就达到了紧耦合,可以使用一个 Mediator 接口类,A 指针指向 Mediator,Mediator 派生出 D ,D 类有两个指针指向 B 和 C ,当 B 想要和 C 有关联 ,是通过 B->D->C,或是 C->D->B ,这样子就达到了 B 和 C 的紧耦合。
Adapter(新环境和旧的接口不够完美,内部适应外部需求)
适配器分为:对象适配器和类适配器
对象适配器是:继承新环境接口,指针指向旧接口。(使用率 99.99999%)
类适配器是:主继承环境接口,副继承 旧接口。(麻烦,只是说明有这种表现。)
状态变化:
Memento(备忘录)
在不破坏封装性的前提下,获取一个对象的内部,并在该对象之外保存对象的状态。以后可以将该对象恢复到原来的状态。
State (状态的改变,其行为也会发生改变)
在程序开发中,我们经常会根据状态来做一些不一样的处理。这时候就出现了 if ... else .. 的程序,我们会考虑到 strategy 模式,strategy 是根据不一样的算法实现不一样的功能,但是 State 也有着异曲同工之妙,我们就会思考state 会不会在未来增加 其他状态。将状态设置为类接口,且使用 singleton 模式保证每个具体状态类有且仅有一个对象。根据不同的状态对象动态的绑定并且执行。
使用地方:在一个对象其内部的状态改变时改变其行为。
要点:本来是一个功能类接口根据状态变化而有相应的方法。我们将方法提取到状态接口中,状态接口派生出躯体状态对应具体方法。功能类接口获取当前具体状态类,执行相应的接口即可。
State 与 Strategy 的区别,是State 是多个行为,且可能会有数据成员。Strategy 是一个行为。
数据结构:(组件内部特定的数据接口,暴露会破坏封装性,将特定的数据接口封装,为客户提供统一的接口)
Composite(合成,处理深度递归的方法,使得处理单个对象和处理组合对象的方法一致)
例子:想改变文件时间节点,和改变目录下所有文件以及目录的时间节点。
定义一个改变时间节点接口 I类,方法 change(),改变文件时间节点派生为A类,实现方法change(),改变目录以及目录一下的时间节点为B类,B类中要有一个链表 指针 pList 保存当前目录下的所有文件以及子目录,在实现 change(){ ...  forChange() }, forChang(){ for(auto &p : pList) p->change() } 是遍历处理pList的各个对象。
composite要点是 将对象组合为树形接口,表示整体和部分得结构。为处理一个对象和处理组合对象提供一致得接口,使一对一或使一对多变为一对一。
Iterator(迭代器)
迭代器过时了,与现在得STL 得迭代器相比性能上差了一些,在泛型编程中,出现得多态是体现在编译时的多态,即泛型编程创建的模板类虽然只有一份,减少了程序的者的负担,但是在编译时能出现多种不具有相同功能但是不一样的类,在编译器就已经确认了。
多态有两种:静态多态和动态多态, 静态多态:函数的重载,泛型编程的模板,这两者都算是静态多态,静态多态是在编译时确定了。 动态多态即使用虚函数,在运行过程中才会知道具体的行为方法(虚函数)。在运行时才会知道指针指向的是一个什么样子的对象(指针组合对象)
要点:不暴露对象内部的实现,且提供统一的接口不同的算法在不同的集合结构进行操作。
Chain of resposibility (职责链)
在程序中,会存在请求与响应的关系,如果直接将请求者和响应者直接紧耦合,会带来后续的难以更改,可以使用职责链,即请求者将请求传递给职责链接口,职责链根据链表中的响应者,选出与之相符合的响应者进行响应。
行为变化(组件本身的变化导致组件行为发生变化)
Command(对象本身与对象行为的解耦合)
在一个类的中,成员非虚函数(行为)与对象之间的关系是非常的紧耦合,我们可以将行为提取出来当作一个对象。在运行过程中,动态绑定对象,从而达到动态绑定行为。
command 与 函数对象的比较: 两者的观点都是将行为封装为对象,但是 函数对象的重点在在于编译时配合编译器正确的静态绑定,而command 是在运行时的动态绑定。
Visitor(访问器)
在需求中,在旧的接口中,增新的行为。
visitor 是用 双重分发的机制实现了新行为的增加。
领域问题:
Interpreter
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值