面向过程(Procedural Programming)是一种以过程(函数)为中心的编程思想,强调将问题分解为一系列步骤(过程/函数),通过函数调用和数据传递来实现程序功能。
主要特征
以函数为基本单位:程序由多个函数组成,每个函数完成特定任务。
数据与操作分离:数据结构和操作数据的函数是分开的。
自顶向下设计:先整体后局部,逐步细化。
强调算法和流程:关注如何一步步完成任务。
面向对象(Object-Oriented Programming, OOP)是一种以对象为中心的编程思想,将数据和操作数据的方法封装在对象中,通过对象之间的交互来实现程序功能。
面向对象编程(OOP)有四大核心特征:封装、继承、多态、抽象。
封装(Encapsulation)是将数据(属性)和操作数据的方法(函数)绑定在一起,并隐藏对象的内部实现细节,只对外提供必要的接口。
特点
数据隐藏:通过访问控制符(private、protected、public)控制成员的可见性
接口统一:对外提供统一的访问接口
降低耦合:减少模块间的依赖关系
继承(Inheritance)允许一个类(子类/派生类)获得另一个类(父类/基类)的属性和方法,实现代码复用和层次化设计。
特点
代码复用:子类自动拥有父类的成员
层次结构:建立类之间的层次关系
扩展性:子类可以添加新的属性和方法
重写机制:子类可以重写父类的方法
多态(Polymorphism)是指同一个操作作用于不同的对象时,可以有不同的解释和执行结果。主要通过虚函数和函数重载实现。
编译时多态:函数重载、运算符重载
运行时多态:虚函数、动态绑定
特点
接口统一:相同的接口,不同的实现
动态绑定:运行时确定调用哪个函数
扩展性强:易于添加新的类型
抽象(Abstraction)是指从具体事物中提取共同特征,忽略非本质的细节,形成概念的过程。在编程中通过抽象类和接口实现。
特点
关注本质:突出重要特征,隐藏实现细节
简化复杂性:将复杂问题分解为简单概念
提供框架:定义标准接口和规范
实现方式
抽象类:包含纯虚函数的类
接口:只包含纯虚函数的类
面向对象的四大特征相互配合,共同构成了面向对象编程的理论基础:
封装保证了数据的安全性和模块的独立性
继承实现了代码的复用和层次化设计
多态提供了灵活的接口和强大的扩展能力
抽象简化了复杂问题,提供了标准化的设计框架
这四个特征使得面向对象编程具有高内聚、低耦合、易维护、易扩展的优点。
在面向对象编程中,类与类之间存在多种关系,这些关系定义了类之间的交互方式和依赖程度。以下是五种主要的类关系:
继承(Inheritance/Is-a)是面向对象编程的核心概念之一,表示"是一个"的关系。子类继承父类的属性和方法,并可以扩展或重写父类的功能。
特点
强耦合关系:子类与父类紧密相关
代码复用:子类自动获得父类的所有公有和保护成员
多态性:支持运行时多态
单向依赖:子类依赖父类,但父类不依赖子类
组合(Composition/Has-a)表示"拥有"的关系,是一种强拥有关系。组合中的部分对象不能独立于整体对象存在,整体对象负责部分对象的生命周期管理。
组合是一种最强的关联关系,表现为整体与局部的关系,整体部分会负责局部的销毁。
在代码层面上:使用的是成员子对象。
特点
强拥有关系:部分对象的生命周期完全依赖于整体对象
不可分离:部分对象不能独立存在
级联删除:整体对象销毁时,部分对象也被销毁
封装性强:部分对象通常是私有的
聚合(Aggregation/Has-a)表示"拥有"的关系,但是一种弱拥有关系。聚合中的部分对象可以独立于整体对象存在,整体对象不负责部分对象的创建和销毁。
聚合是一种稍微强一点的关联关系,表现为整体与局部的关系,整体并不负责局部对象的销毁。
在代码层面上:使用指针或引用。
特点
弱拥有关系:部分对象可以独立存在
可分离:部分对象可以脱离整体对象
共享所有权:多个整体对象可以共享同一个部分对象
生命周期独立:整体对象销毁不影响部分对象
关联(Association/Has-a)表示类之间的"使用"关系,是一种相对松散的关系。关联的对象在逻辑上相关,但生命周期独立,可以相互引用。
关联关系,是两个类之间最简单、最单纯的关系(仅仅表明两个类之间是有关系的)。
如果两个类之间没有关系,那就不能满足面向对象的特点(对象与对象之间进行交互,使得彼此之间的状态发生变化)。
而关联关系包括两种:双向的关联关系与单向的关联关系。
但是不论哪种关联关系,两个类之间彼此并不负责对方的生命周期。注意在语义上是A has B的关系,一种固定的关系。
在代码上的表现形式是:使用引用或者指针。两种关联关系的例子:
双向的关联关系
双向的关联关系(使用直线),比如客户与订单之间的关系。客户拥有一个或多个订单,每个订单都会属于一个客户。
彼此知道对方的存在,但是彼此并不负责对方的生命周期。
在代码层面上:使用的是指针或者引用。
特点
松散耦合:对象之间相对独立
双向或单向:可以是双向关联或单向关联
多重性:一对一、一对多、多对多关系
生命周期独立:对象可以独立创建和销毁
依赖(Dependency/Uses)是最弱的关系,表示一个类使用另一个类,但不拥有它。依赖通常体现在方法参数、局部变量、静态方法调用等场景中。
依赖是两个类之间的一种不确定的关系,语义上是一种A use B的关系,这种关系是偶然的,临时的,并非固定的 。
代码表现形式:
B作为A的成员函数参数;(或者B类引用、B类指针作为A的成员函数参数)
B作为A的成员函数的局部变量;
B作为A的成员函数的返回值;
A的成员函数调用B的静态方法。
(总之,只与A的成员函数相关,与A的数据成员无关)
特点
最弱耦合:临时性的使用关系
不持有引用:不保存对方的引用或指针
临时交互:通常在方法调用期间发生
易于解耦:容易修改和替换
关系类型 | 耦合强度 | 生命周期依赖 | 典型表现 | UML表示 | 代码特征 |
---|---|---|---|---|---|
继承 | 最强 | 子类依赖父类 | "is" | 空心三角箭头 | class Child : public Parent |
组合 | 强 | 部分依赖整体 | "has" | 实心菱形 | 成员对象,构造时创建 |
聚合 | 中等 | 相对独立 | "has" | 空心菱形 | 成员指针,外部创建 |
关联 | 较弱 | 独立 | "has" | 直线箭头 | 成员指针,可双向 |
依赖 | 最弱 | 独立 | "use" | 虚线箭头 | 参数、局部变量、静态调用 |
在进行面向对象设计的时候,需要考虑类与类之间的关系,这样可以让类之间的关系更加明确。但是,除此之外,在进行面向对象设计的时候,还需要注意满足一定的设计要求,也就是面向对象的设计原则,只有遵循一定的原则,才能更好的满足软件的设计需求,更好的满足变化。
一个优良的系统设计要求:低耦合与高内聚。
耦合:强调的是类与类之间、或者模块与模块之间的关系。
内聚:强调的是类内部、或者模块内部的关系。
简单来说就是类之间的关联性降低一些,类内部关联性提高一些(比如一个类只干一件事情)。
可以这样理解,如果一个函数的功能太多,那么出现问题的时候一定不方便排查,对于类而言也是同样的道理。
单一职责原则 (Single Responsibility Principle, SRP):一个类应该只有一个引起它变化的原因,即一个类只应该承担一种职责。
一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。
核心是:解耦与增加内聚性。
在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,相当于将这些职责耦合在一起,当其中一个职责变化时可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
要点
每个类只负责一个功能领域中的相应职责
类的职责要单一,不能将太多的职责放在一个类中
当需要修改某个职责时,不会影响其他职责
作用
降低类的复杂度
提高类的可读性和可维护性
降低变更引起的风险
提高代码的可重用性
开闭原则 (Open-Closed Principle, OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
开闭原则是面向对象的可复用性设计的第一块基石,它是最重要的面向对象设计原则。
核心思想:对抽象编程,而不对具体编程,因为抽象相对稳定。
在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。
开闭原则就是指软件实体应尽量在不修改原有代码的情况下进行扩展。任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时,应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在C++中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在C++语言中提供了抽象类的机制,可以通过它定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
要点
当应用的需求改变时,在不修改软件实体的源代码的前提下,扩展模块的功能
通过抽象化来实现开闭原则
使用继承、多态、组合等方式来扩展功能
作用
提高代码的可扩展性
降低维护成本
提高代码的稳定性
里氏替换原则 (Liskov Substitution Principle, LSP):所有引用基类的地方必须能透明地使用其子类的对象,即子类必须能够替换其基类。
里氏代换原则表明:
如果一个软件实体使用的是一个基类对象,那么把它替换成派生类对象,程序将不会产生任何错误和异常;(反过来则不成立)
如果一个软件实体使用的是一个派生类对象,那么它不一定能够替换成基类对象直接使用。
核心思想:派生类必须能够替换其基类。派生类可以扩展基类的功能,但不能改变基类原有的功能。
表现为:派生类可以实现基类的抽象方法(纯虚函数),也可以覆盖基类的普通虚函数——表现多态,但不能隐藏基类的普通成员函数。
要点
子类可以扩展父类的功能,但不能改变父类原有的功能
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
子类中可以增加自己特有的方法
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
作用
增强程序的健壮性
提高代码的可维护性
保证继承关系的正确性
接口隔离原则 (Interface Segregation Principle, ISP):客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
核心思想:使用多个小的专门的接口,而不要使用一个大的总接口。 根据接口隔离原则,当一个接口太大时需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
要点
使用多个专门的接口,而不使用单一的总接口
接口应该小而专一
避免接口污染
提高系统的灵活性和可维护性
作用
降低类之间的耦合度
提高代码的可维护性
依赖倒置原则 (Dependency Inversion Principle, DIP):高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
简单来说,依赖倒置原则要求针对接口编程,不要针对实现编程。
如果说开闭原则是面向对象设计的目标,那么依赖倒置原则就是面向对象设计的主要实现机制之一。
依赖倒置原则要求在程序代码中传递参数时或在关联关系中尽量引用层次高的抽象层类。(基类往派生类方向代表从上往下,从高到低)
也就是使用抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体的派生类来做这些事情。
要点
面向接口编程,而不是面向实现编程
通过抽象使各个类或模块不相互影响
降低类之间的耦合性
提高系统的稳定性
作用
提高代码的可维护性和可扩展性
便于单元测试
迪米特法则 (Law of Demeter, LoD) / 最少知识原则:一个对象应该对其他对象保持最少的了解,即一个类应该对自己依赖的类知道得最少。
迪米特法则要求每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
一个软件实体应当尽可能少地与其他实体发生相互作用。如果一个系统符合迪米特法则,那么当其中的某一个模块发生修改时就会尽量少地影响其他模块,扩展会相对容易。
应用迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。当两个类之间直接通信的时候,会造成高度依赖的后果(高耦合)。解决此问题的办法,尽量避免两个类直接接触(低耦合),通过一个第三者做转发。
在迪米特法则中,对于一个对象,其朋友包括以下几种: (1)当前对象本身(this) (2)以参数形式传入到当前对象方法中的对象 (3)当前对象的成员子对象 (4)如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友 (5)当前对象的成员函数中创建的对象
任何一个对象如果满足上面的条件之一,就是当前对象的"朋友",否则就是"陌生人"。在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与"陌生人"发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。
迪米特法则要求在设计系统时应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中一个对象需要调用另一个对象的方法,可以通过"第三者"转发这个调用。简而言之,就是通过引入一个合理的"第三者"来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计时要注意下面几点:
在类的划分上应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改不会对关联的类造成太大影响;
在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
在类的设计上,只要有可能,一个类型应当设计成不变类;
在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
迪米特原则又称为最少知道原则。核心思想:降低耦合程度。
要点
只与直接的朋友通信
朋友间也是有距离的
自己的就是自己的
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用
作用
降低类之间的耦合度
提高模块的相对独立性
提高类的可重用率
降低系统的复杂度
组合复用原则 (Composite Reuse Principle, CRP):尽量使用组合/聚合的方式,而不是使用继承来达到复用的目的。
组合复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用功能的目的。简而言之,在复用时要尽量使用组合/聚合关系(关联关系),少用继承。
在面向对象设计中可以通过两种方法在不同的环境中复用已有的设计和实现:即通过组合/聚合关系或通过继承。
首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时需要严格遵循里氏替换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的某些内部细节对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承 —— final语法)。
xxxxxxxxxx
61class A final
2{};
3
4class B
5: public A //error
6{};
由于组合/聚合关系可以将已有的对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使成员对象的内部实现细节对于新对象不可见,所以这种复用又称为"黑箱"复用。相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作。
组合复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。
核心思想:在复用时要尽量使用组合/聚合关系(关联关系),少用继承。
要点
优先使用组合而不是继承
通过组合来实现代码复用
避免继承层次过深
提高系统的灵活性
作用
维持类的封装性
降低依赖关系
减少继承层次
提高灵活性和可扩展性