文章目录
- 一、面向对象设计的概念
- 4.统一建模语言:UML语言
- StartUML
- 二、类与类之间的关系
- 0.总结
- (1)类与类的五种关系
- (2)区别
- (3)面向对象 vs 基于对象
- 1.继承 (泛化耦合)
- 2.组合 (Composition)
- 3.聚合 (Aggregation)
- 4.关联
- (1)双向关联
- (2)单向关联
- 5.依赖 (Dependency)
- 三、面向对象设计的原则
- 0.总结
- 1.单一职责原则 (Single Responsibility Principle)
- 2.开放-闭合原则(Open Closed Principle)
- 3.里氏替换原则 (Liscov Substitution Principle)
- 4.接口分离原则 (Interface Segregation Principle)
- 5.依赖倒置原则 (Dependency Inversion Principle)
- 6.迪米特法则/最少知识原则(Law of Demeter -> Least Knowledge Principle)
- 7.组合复用原则 (Composite/Aggregate Reuse Principle)
- 四、设计模式
- (一)创建型模式
- 1.单例模式
- 工厂模式
- 2.简单工厂模式
- 3.工厂方法模式
- 4.抽象工厂模式
- (二)结构型模式
- 1.代理模式
- (三)行为型模式
- 1.观察者模式
- (四)其他非23种经典模式的设计模式
- 1.Pimpl模式
- 2.Reactor模式
- 3.Preactor模式
一、面向对象设计的概念
1.面向对象的分析 (OOA):分析出要做什么,将需要展示出来。
2.面向对象的设计 (OOD):把类、类中的数据成员与成员函数设计出来,类与类之间的关系。
3.面向对象的编程 (OOP):将面向对象设计中的类、数据成员、成员函数实现出来。
4.统一建模语言:UML语言
StartUML
+:public
#:protected
-:private
①画图解决不了的,用右边栏的属性(Properities)
②静态:static:下划线
③纯虚函数/抽象类:is abstract:斜体
④const、std:::写不了,不写了
⑤继承:默认是公有继承
二、类与类之间的关系
0.总结
(1)类与类的五种关系
1.继承:继承语法,强耦合
2.组合:使用成员子对象。有整体-部分的关系。整体负责局部的销毁。部分不能独立于整体存在。
3.聚合:有整体-部分的关系。整体不负责局部的销毁。部分可以独立于整体存在。
4.关联:拥有对方的指针,作为数据成员
5.依赖:只是临时使用,并不拥有
(2)区别
1.方向:继承关系是一个纵向关系,其他四种是水平关系。
2.从语义层面来看:继承关系是is、依赖关系是use、关联、组合、聚合是has
3.耦合程度:继承 > 组合 > 聚合 > 关联 > 依赖
4.继承:既强调数据成员,又强调成员函数
组合、聚合、关联:强调两个类的数据成员的关系
依赖:强调的是两个类的成员函数的关系
之前我们实现多态使用的是继承与虚函数这种面向对象的方法。现在知道了类与类之间的其他关系后,可以使用组合与依赖这种基于对象的方法实现。
上图展示了几种耦合的示例。其中汽车和交通工具属于泛化耦合,轮子和方向盘组合于汽车,汽车聚合成车队,而汽车和司机具有依赖关系。
(3)面向对象 vs 基于对象
面向对象 (Object-Oriented) 和 基于对象 (Object-Based)
面向对象代码可重用高,缺点是耦合度高
1.继承 (泛化耦合)
1.基类与派生类,基类部分会成为派生类的一部分。
可以吸收,但是不一定能访问。比如基类的private成员,派生类会吸收,但因为私有,没有访问权限。
2.类图的画法:派生类指向基类的空心三角箭头。
3.语义层面:B is A,B继承A,经理是一个员工。
4.泛化:
继承:先有基类,然后派生出新的类,也就是派生类。
泛化(一般化):先有派生类,然后将具体相同属性抽象出来,然后形成了基类。
5.类图
2.组合 (Composition)
表示整体与部分的关系,部分不能独立于整体存在。整体负责部分的生命周期。
组合(Composition):一种强的拥有关系,体现了严格的部分和整体的关系,部分和整体具有一样的生命周期。
①是一种最强的关联关系,表现为整体与局部的关系,整体负责局部对象的销毁。
②在语义层面上:A has B。
③在代码层面上表现为:数据成员以成员子对象的形式存在。
④在类图的画法上:从局部指向整体的实心菱形箭头。
⑤如:主公司和子部门、人体与各个器官。
3.聚合 (Aggregation)
表示整体与部分的关系,但部分可以独立于整体存在。整体不负责局部对象的销毁。
聚合(Aggregation):一种弱的拥有关系,体现A对象可以包含B对象,但B对象不是A对象的一部分。
①聚合是一种稍微强一点的关联关系,表现为整体与局部的关系。但是整体不负责局部对象的销毁。
②语义层面:A has B
③代码层面:使用的是指针或引用
④类图的画法上:从局部指向整体的空心菱形箭头。
4.关联
(1)双向关联
彼此知道对方的存在,但是彼此并不负责对方的生命周期。
①在语义层面上: A has B
②在代码层面上:使用的是指针或引用
③在类图的画图上:使用的是实心实线
(2)单向关联
1.定义
一个类知道另一个类,并且可以通过指针或引用访问另一个类的对象。彼此不负责对方的生命周期。没有整体与部分的概念。
①A类知道B类的存在,B类却不知道A类的存在。
A类拥有B类的指针或引用作为A类的数据成员,可以通过这个指针或引用访问B类的对象。长久持有,而不是临时使用。(临时使用的传参是依赖)
②在语义层面上:A has B
③类图画法上:A指向B的实线箭头
2.实现:
一个类包含指向另一个类的指针或引用。
3.举例:
Person类中包含指向Vehicle类的指针(Vehicle *_vehicle),Person类可以调用Vehicle类的run方法,但并不负责Vehicle对象的生命周期管理。
5.依赖 (Dependency)
1.依赖(Dependency):由于逻辑上相互协作可能,而形成的一种关系。是两个类之间的一种不确定的关系:
(1)语义上是一种A use B的关系,这种关系是偶然的,临时的,并非固定的。
(2)在类图的画法上:从A指向B的虚线箭头。
(3)在代码上表现为:
①B作为A的成员函数参数;
②B作为A的成员函数的局部变量(B作为A的成员函数的返回值);
③A的成员函数调用B的静态方法。
2.B临时的传参给A,作为A的成员函数的参数,则A依赖B
三、面向对象设计的原则
0.总结
一个优良的系统设计,遵循的要求:低耦合、高内聚
①耦合:强调的是类与类之间、模块与模块之间的关联的程度。
②内聚:强调的是类内部、模块内部的关系。
低耦合:类与类之间关系弱。当一个类发生改变时,另一个类不会受到太大的影响。
高内聚:一个类尽量只做一件事。
1.单一职责原则:一个类只负责一件事。功能分离。
2.开放闭合原则(OCP):对扩展开放,对修改关闭。
3.里氏替换原则(LSP):子类要能完全地替代父类。
4.接口分离原则:要按需提供小接口,不要提供肥大的接口,用不到的功能接口就不要提供
5.依赖倒置原则:高层模块不应该依赖于底层,即抽象不应该依赖细节,细节应依赖抽象。
6.最少知识原则:一个对象应该尽可能少地了解其他的对象,即一个对象只应与它直接关联的对象进行交互。例如,Driver 类只知道 Car 类,而不知道 Engine 类的存在。Driver 通过 Car 类的接口启动汽车,而不是直接访问 Engine 类。
7.组合复用原则:优先使用对象的组合而不是继承来实现功能的复用。将功能分解为独立的组件,然后将这些组件组合起来,实现灵活和可扩展的系统设计。
1.单一职责原则 (Single Responsibility Principle)
核心思想:一个类(模块),最好只做一件事,引起它变化的原因只有一个。
核心是:解耦与增加内聚性。
举例:
计算功能,只需要area(),就把draw()方法抽离出来,单独封装一个类。把一个大的类拆开。
2.开放-闭合原则(Open Closed Principle)
对扩展开放,对修改关闭。
核心思想:对抽象编程,而不对具体编程,因为抽象相对稳定。
举例:原本的设计,如果要添加功能,例如要求幂和开方。需要修改源代码。真正编程的时候,如果一个大的基类被修改,所有继承它的派生类都会受到影响,可能会造成意想不到的错误。
(1)若没有遵循开闭原则,所有功能都做在计算器类中。则后来的人新增功能,需要对前人写的老代码进行修改,改动别人的代码,有可能会造成问题。
(2)遵循开闭原则,将每个功能分离到一个类中。这样新增一个功能,只需要新写一个类。而不需要去改动之前的老代码。新增的模块出现问题,也好定位。
修改后:基类是抽象类,只做一个接口。具体实现交给派生类。能很好地应对变化和扩展。只需要新加类,而不需要修改哪个类的源代码,尤其是对基类的修改。
3.里氏替换原则 (Liscov Substitution Principle)
子类要能完全地替代父类。
里氏代换原则表明,在软件中将一个基类对象替换成它的派生类对象,程序将不会产生任何错误和异常,反过来则不成立。【派生类不能有隐藏,不能与基类有同名的(非虚)函数。增加自己的个性要用新的函数名】
核心思想:派生类必须能够替换其基类。派生类可以扩展基类的功能,但不能改变基类原有的功能。表现为:①派生类(子类)可以实现基类(父类)的抽象方法,表现多态,但不能隐藏基类(父类)的非抽象方法。②派生类(子类)可以有自己的个性,即派生类可以增加自己的新的方法、属性。
4.接口分离原则 (Interface Segregation Principle)
定义:客户端不应该依赖那些它不需要的接口。
核心思想:使用多个小的专门的接口,而不要使用一个大的总接口。
举例:Bird实现的功能太多了,而鸵鸟并不满足
Bird类默认是不会飞的鸟,鸵鸟来继承不会飞的鸟。
分离出fly(),形成会飞的鸟FlyingBird,乌鸦来继承会飞的鸟。
①这里看起来没有满足单一职责原则。所以实际写代码,并不一定要完美遵循所有的设计原则,否则会导致类的数量过多。
②若是以后要添加不能在陆地上行走walk()的鸟类,则这时候就必须修改框架里的基类,会违反开放闭合原则,但也没有办法,因为这是当初设计的时候有问题。
所以,在设计最初的基类时,功能既不能设计太少(满足单一职责原则,但类会过多),也不能设计太多(若子类有不需要的功能,违反接口分离原则。若要修改原本的基类,违反开放闭合原则),要酌情折中。
5.依赖倒置原则 (Dependency Inversion Principle)
定义:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依
赖于抽象。简单来说,依赖倒置原则要求针对接口编程,不要针对实现编程。
核心思想:面向接口编程,依赖于抽象。面向抽象编程。
在大多数情况下,开闭原则、里氏代换原则和依赖倒置原则会同时出现,开闭原则是目标,里氏替换原则是基础,依赖倒置原则是手段。
举例:
修改后:设计为抽象类。添加新的需求,不改变老的框架。
6.迪米特法则/最少知识原则(Law of Demeter -> Least Knowledge Principle)
定义:每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单
位。
迪米特法则要求一个软件实体应当尽可能少地与其他实体发生相互作用。如果一个系统符合迪米特
法则,那么当其中的某一个模块发生修改时就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。应用迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。当两个类之间直接通信的时候,会造成高度依赖的后果(高耦合)。解决此问题的办法,尽量避免两个类直接接触(低耦合),通过一个第三者做转发。
最少知识原则:专业的事情,交给专业的人。用最少的知识解决问题。
举例1:A方案,购房者需要了解所有的楼盘。B方案,购房者找房产中介,降低了购房者与楼盘的耦合度。
举例2:设计模式之Pimpl模式
Line类中原本是嵌套实现了Point类。现在Line中只放LinePimpl*
,在LinePimpl类中存放Point类的具体实现。这样Point类的变化不会影响到Line类,降低了模块之间的耦合程度。并且对Line类屏蔽了Point类的内部实现。
看看pfd:6.2类域.pdf
7.组合复用原则 (Composite/Aggregate Reuse Principle)
1.核心思想:复用时要尽量使用组合/聚合关系(关联关系),少用继承。即降低耦合程度。
2.举例:人继承车,不太合适。车发生了变化,人就会继承这种变化,耦合程度过于强了。
修改后:人类只用了车类的指针,单向关联。
OO真经:程序世界与实现世界中依赖的区别
程序世界中对象间的依赖是以类为单位的,这种依赖关系会随着泛化过程而被泛化到类里面去。并且,只要两个类建立了依赖,那么两个类之间的所有对象都两两依赖了。换句话说,在程序世界里,只要有一个“人”和一个“大学”发生了联系,那么这种联系就被泛化到类中了,随后,所有的“人”都可以上“任何”的大学。
四、设计模式
设计模式:它是解决特定问题的一系列套路,有一定的普遍性。它可以提高代码的可重用性、可读性、可靠性、可扩展性。
(一)创建型模式
创建型模式 (Creational patterns) 有5种
提供对象创建机制,增加现有代码的灵活性和重用。
1.单例模式
全局区域只有一个类的实例。
之前在三篇中讨论过。后续有时间会合并一下。
1.C++(week10):C++基础: 第二章 类与对象
2.设计模式
3.单例模式
工厂模式
1.简单工厂:有一个工厂类,里面每个成员函数对应生产一种产品
2.工厂方法:有一个抽象工厂类,下面派生各个具体产品的工厂。每一个产品对应一个工厂,即一个工厂仅能生产一个具体的产品。
3.抽象工厂:有一个抽象工厂类,成员函数是各个品牌。派生类是所有品牌对应的产品系列。新增一个系列很容易,但(不修改抽象工厂类的话)无法新增品牌。每个具体工厂可以生产所有品牌对应的该系列产品。每个具体工厂类负责生产一系列相关或相互依赖的产品。
2.简单工厂模式
1.定义
简单工厂模式(静态工厂方法模式):
①工厂会根据产品的名字生产出对应的产品。
②一个工厂可以生产出多个产品。
③简单工厂模式提供了专门的工厂类用于创建对象,实现了对象创建和使用的分离。
2.优点
只需要知道产品的名字就可以生产出对应的产品。
3.缺点
违反了设计原则:单一职责原则 (工厂的功能过于复杂)、开放闭合原则(增加新产品需要修改工厂源代码)、依赖倒置原则 (面向抽象编程,而不是面向具体编程)
4.类图
5.代码链接:https://github.com/WangEdward1027/DesignPatterns/blob/main/Factory/simpleFactory2.cpp
3.工厂方法模式
1.定义
工厂类作为抽象类,每一个产品对应一个派生类工厂。
2.优点
①满足了单一职责原则、开放闭合原则、依赖倒置原则
②新增代码时,只需要在Figure类和Factory类下分别派生梯形类,然后依赖。不需要修改源代码。
3.缺点
工厂的数量会随着产品数量的增加而增加,一对一。
对于有大量细分类型产品(产品族)的生产需求,不适合管理工厂。
4.类图
5.代码链接:https://github.com/WangEdward1027/DesignPatterns/blob/main/Factory/factoryMethod.cpp
4.抽象工厂模式
1.定义
2.优点
①抽象工厂,减少了工厂的数量。
②可以对一类品牌的产品进行细分
③每个工厂类可以生产产品族
3.缺点
不修改源代码,无法生产新类型品牌。只能在已有品牌上进行新系列产品的扩展。
4.类图
不适合新的品牌,但时候老品牌出新的产品系列。
两个品牌,下属两个产品。
抽象工厂忘了加析构函数
三个品牌,下属三个产品。
(二)结构型模式
结构型模式(Structural patterns)有7种
解释如何将对象和类组装成更大的结构,同时保持结构的灵活性和高效性。
1.代理模式
string写时复制COW,不能分开读和写,就用charProxy。
C++(week11): C++基础 第五章: 运算符重载、友元
(三)行为型模式
行为型模式(Behavioral patterns)11种
负责有效的沟通和对象之间的责任分配。
1.观察者模式
1.概念
观察者模式(Observer Pattern)是一种行为型设计模式,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当这个主题对象的状态发生变化时,所有依赖于它的对象都会收到通知并自动更新。这个模式常用于实现事件处理系统。
2.观察者模式的结构
观察者模式涉及到四个主要角色:
(1)主题(Subject):维护观察者列表,提供方法来增加和移除观察者,并在状态发生变化时通知所有观察者。
(2)具体主题(Concrete Subject):具体的主题类,维护具体的状态,并在状态变化时通知观察者。
(3)观察者(Observer):定义了一个更新接口,用于接收主题通知。
(4)具体观察者(Concrete Observer):实现观察者接口,定义如何响应主题的更新
3.观察者模式的主要特点
(1)松耦合:主题(Subject)和观察者(Observer)之间的耦合度很低。主题只知道观察者实现了某个接口,而不知道具体的实现细节。这使得主题和观察者可以独立地扩展和修改。
(2)动态观察者列表:观察者可以在运行时动态地加入或退出,灵活性高。
(3)广播通信:主题对象状态发生变化时,会通知所有注册的观察者,实现了一种广播通信机制。
(4)支持事件驱动模型:观察者模式适合用于需要事件通知机制的系统,如GUI应用程序中的事件处理、消息系统等。
4.实现
(1)所有关注者(attach)的,在主题更新后会得到通知(notify)。观察者会被用list链表保存。若不想被通知,则可以取消关注(detach)。
(2)定义对象的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
当一个对象发生了变化,关注它的对象就会得到通知;这种交互也称为发布-订阅(publish-subscribe)。
4.类图
5.代码链接:https://github.com/WangEdward1027/DesignPatterns/blob/main/Observer/Observer.cpp
(四)其他非23种经典模式的设计模式
1.Pimpl模式
C++(week11): C++基础 第五章: 运算符重载、友元
2.Reactor模式
后续C++线程池补充
3.Preactor模式
后续C++线程池补充