面向对象技术简述(含设计模式)
6.9.2 面向对象技术
- 面向对象 = 对象 + 分类 + 继承 + 通过消息的通信 面向对象 = 对象 + 分类 + 继承 + 通过消息的通信 面向对象=对象+分类+继承+通过消息的通信
- 其中包括:
- 对象
- 运行的实体;
- 既包含属性/数据,又包含方法/行为/操作数据的函数;
- 与其说是对象,不如说是程序模板;
- 一个对象由:对象名、属性(数据)、操作(方法)组成;
- 消息
- 对象间进行通信的一种构造(
代表着一组包含特定格式的信息
); - 对象 1 → 消息 对象 2 对象1\overset{\text{消息}}\rightarrow 对象2 对象1→消息对象2,对象2对消息进行解释并予以响应,这称为消息传递;
- 发送对象的对象不需要知道接收对象如何对请求予以相应;
- 对象间进行通信的一种构造(
- 类
- 定义了一组大体上相似的对象;
- 类是在对象之上的抽象,对象是类的具体化,是类的实例;
- 继承
- 是父类和子类之间共享数据和方法的机制;
- 一个父类可以有多个子类,但一个子类只能继承自一个父类;
- 多态
- 不同对象接收到同一消息后产生不同结果,这一现象被称为多态;
- 泛化 = 多态 , 实现 = 继承;
- 简单来说是同一操作作用于不同的对象,会有不同的解释;
- 对象
6.9.2.1 设计原则
- 单一职责原则
- 要设计目的单一的类;
- 开放-封闭原则
- 对扩展开放,对修改关闭(
股权警告
);
- 对扩展开放,对修改关闭(
- 里氏替换原则(
Liskov substitution principle
)- 子类可以替换父类;
- 依赖倒置原则
- 针对接口编程,而不是针对实现编程;
- 要依赖于抽象,而不是具体实现;
- 接口隔离原则
- 宁愿使用多个专门的接口也比使用单一的总接口要好;
- 组合复用原则
- 要尽量少用继承,要尽量使用组合;
Q:什么是组合,为什么是组合?
A:组合就是在一个对象中嵌套另一个对象作为对象成员来使用。组合非常有利于代码的复用,且关系可以动态变化,一个对象中可以包含多个不同的子对象,从而实现更为复杂的组合变化。更重要的是继承使用的太多的话,对象间的耦合程度过高,代码会变得臃肿混乱,容易一处修改就使得代码陷入bug和混乱; - 迪米特原则(
Demeter Principle
)- 一个对象应当对其他对象尽可能少的“了解”;
- 也就是,不同函数之间应避免直接访问到彼此的内部信息,需要使用的时候直接调用就好了;
6.9.2.2 设计模式的概念和分类
-
只关注软件系统的设计,与具体的实现语言无关(
软考可选择c++或者java
); -
设计模式一般会有4个要素:
- 模式名称;
- 问题;
- 解决方案;
- 效果;
-
设计模式的分类(
23种
):
- 常考的有: 工厂方法、抽象工厂、构建器、适配器、装饰、命令、中介者、观察者 工厂方法、抽象工厂、构建器、适配器、装饰、命令、中介者、观察者 工厂方法、抽象工厂、构建器、适配器、装饰、命令、中介者、观察者
-
- 创建型模式
- 抽象了实例化过程;
- 关注对象的创建过程,将对象的创建与使用分离,提供了了一个灵活的方式来创建对象,而无需指定具体的类;
- 结构型模式(
常考选择题
)- 描述如何组合类和对象以获得更大的结构;
- 采用继承机制来组合接口或者实现;
- 行为型模式
- 不仅描述对象或类的模式,还描述它们之间的通信模式;
- 关注对象之间的交互和职责分配、对象之间的交互方式;
-
- 行为类模式使用继承机制在类间分派行为;
-
- 行为对象模式使用对象复合
- 为什么要使用行为型模式:
1. 提高代码复用性: 通过封装变化的部分,减少重复代码;
2. 增强代码可维护性: 将复杂的逻辑拆分成更小的、可管理的单元
- 创建型模式
6.9.2.2.1 工厂方法模式(factory_method
)
- 在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型,本质是将子类的实例化给推迟了;
- 它建议使用特殊的工厂方法代替对于对象构造函数的直接调用(
如:new
),直接调用改在工厂方法中进行。这允许你在子类中重写工厂方法, 从而改变其创建产品的类型;仅当这些产品具有共同的基类或者接口时, 子类才能返回不同类型的产品, 同时基类中的工厂方法还应将其返回类型声明为这一共有接口;
- 【要素】:共同接口的创建方法,子类方法多态;
- 举例:
- 模式结构:
6.9.2.2.2 抽象工厂模式(abstract factory
)
- 是在工厂方法基础上的升级,应对的是某一类对象下的所有子类,而不是某一个子类(
某一个子类是工厂方法
); - 例如对于椅子、沙发、餐桌等家具来说,按照设计风格的不同可以分为中式、日式、维多利亚式、北欧式。现在我们想生产一些家具,但是要求每一批生产的都是一种生产风格。此时,就可以声明抽象工厂(一个包含了批次中所有产品构造方法的接口),如
F
u
r
n
i
t
u
r
e
F
a
c
t
o
r
y
{
+
c
r
e
a
t
e
C
h
a
i
r
(
)
,
+
c
r
e
a
t
e
S
o
f
a
(
)
,
+
c
r
e
a
t
e
T
a
b
l
e
(
)
}
FurnitureFactory\{+createChair(), +createSofa(), +createTable()\}
FurnitureFactory{+createChair(),+createSofa(),+createTable()},这些方法必须返回抽象的产品类(
即Chair、Sofa、Table
)。在这以后,应对不同设计风格的生产要求,可以通过接口创建特定类型的“工厂”对象,在这些“工厂”对象中对构造方法进行重写,如由FurnitureFactory
创建ChineseFurnitureFactory
来生产ChineseChair
、ChineseSofa
、ChineseTable
。 - 模式的结构:
6.9.2.2.3 构造器模式/生成器模式(builder
)
-
允许你使用相同的创建代码分步骤创建不同类型和形式的复杂对象;
-
生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。生成器能够让你能够分步骤创建复杂对象。 生成器不允许其他对象访问正在创建中的产品;
-
例如你要建造一间房子,需要建造墙壁、房门、管道、地板、天花板。这时候你有三种思路:
- 第一种是创建一个房子的基类(
BaseEntity
)储存一定的参数,并在其基础上进行实现或者泛化。但如果后来者要对房子添加一些基类中没有出现过的修改,出现了基类中没有的参数,构造函数也要变动,则需要修改基类,这样会使开发非常复杂; - 第二类是在基类中创建一个涵盖所有参数的超级构造函数,但这样会造成严重的资源浪费。大多数情况下,超级构造函数的参数都没有用,故调用起来非常复杂;
- 最后一种也就是额外创建一个构造器,将对象的构造过程拆分成一组“步骤”,如
buildWall()创建墙壁
、buildDoor()创建房门
、buildTube()创建管道
等。每次创建对象,只需要通过构造器对象选择性地调用特定的对象执行,按顺序一次性完成相应步骤即可。当创建不同形式的产品时,一些构造步骤可能需要不同的实现(参数不同
),则需要对应创建不同产品的构造器(如Builder1
和Builder2
);
- 第一种是创建一个房子的基类(
-
模式的结构:
在有需要的情况下,可以进一步将用于创建产品的一系列生成器步骤调用抽取成为单独的主管类(
Director
)。主管类可定义创建步骤的执行顺序, 而生成器则提供这些步骤的实现,或者说主管类构造了使用生成器接口的对象。事实上,对于客户端代码来说, 主管类完全隐藏了产品构造细节。 客户端只需要将一个生成器与主管类关联, 然后使用主管类来构造产品, 就能从生成器处获得构造结果了;
6.9.2.2.4 原型模式(Prototype
)
- 针对不想复制品依赖类或者只知道要复制对象的部分接口不知道全貌的情况(
即:从外部复制对象并非总是可行
); - 原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个克隆方法。克隆方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中(
你甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量
); - 支持克隆的对象即为原型。 当你的对象有几十个成员变量和几百种类型时, 对其进行克隆甚至可以代替子类的构造(
方法是:创建一系列不同类型的对象并不同的方式对其进行配置。 如果所需对象与预先配置的对象相同, 那么你只需克隆原型即可, 无需新建一个对象
);
事实上,这类复制与其说是克隆,不如说是有丝分裂,所有“遗传物质”(
属性、方法
)都和原型一摸一样;
- 其模式结构是:
6.9.2.2.5 单例模式(Singleton
)(问题非常多
)
- 确保【1】一个类只有一个实例, 并【2】提供一个访问该实例的全局节点。但这违反了单一职责原则;
实际上,只解决了上文描述中的其中一个的东西,都被称为单例。单例在现在非常常用;
- 实现方法:
- 在类中添加一个私有静态成员变量用于保存单例实例;
- 创建静态方法,在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例(
延迟初始化
); - 将类的构造函数设为私有(
只有一个实例
)。 类的静态方法仍能调用构造函数, 但是其他对象不能调用; - 检查客户端代码, 将对单例的构造函数的调用替换为对其静态方法的调用;
6.9.2.2.6 适配器模式(Adapter
)
-
类似于……
-
适配器是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互(
调整格式、属性等
); -
模式通过封装对象将复杂的转换过程隐藏于幕后,被封装的对象甚至察觉不到适配器的存在;
-
例如:
-
模式的结构是:
6.9.2.2.7 责任链模式(Chain of Responsibility
)
- 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者;
- 详细内容见于此处;
6.9.2.2.8 命令模式(Command
)
优秀的软件设计通常会将关注点进行分离, 而这往往会导致软件的分层(
网络层、Mybatis_plus后端
);
- 将请求封装为一个对象,从而可用不同的请求对客户进行参数化,同时将请求排队或者记录请求日志,支持可撤销的操作;
- 一个 GUI 对象传递一些参数来调用一个业务逻辑对象, 这个过程通常被描述为一个对象发送请求给另一个对象。命令模式建议 GUI 对象不直接提交这些请求。 你应该将请求的所有细节 (例如调用的对象、 方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法。命令对象负责连接不同的 GUI 和业务逻辑对象(
类似于视图和数据库
)。 此后, GUI 对象无需了解业务逻辑对象是否获得了请求, 也无需了解其对请求进行处理的方式。 只需要GUI对象触发命令即可, 命令对象会自行处理所有细节工作。; - 让所有命令实现相同的接口。 该接口通常只有一个没有任何参数的执行方法, 让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令;
有一个问题,由于接口的方法不含有任何参数,当需要接受请求的参数时,需要另寻他招:使用数据对命令进行预先配置, 或者让其能够自行获取数据(
用属性或者对象接收并调用而不是传参
); - 其模式类似于:
6.9.2.2.9 观察者模式(Observer
)
- 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新;
6.9.2.2.10 状态模式(State
)
- 将状态变成类;
- 允许一个对象在其内部状态改变时改变它的行为;
6.9.2.2.11 策略模式(Strategy
)
- 最常见的就是:定义一系列的算法,把它们一个个封装起来,并且使他们可以相互替换。此时算法独立于使用它的用户而变化;
- 本质还是【多方案切换】这一套;