关键词:C++、设计模式
Reference:卡码网KamaCoder - 设计模式精讲 - https://kamacoder.com/designpattern.php
创建型设计模式
单例模式
单例模式是创建型设计模式,保证一个类只有一个实例,并提供全局访问方法实现这个实例。
单例模式的使用情形
- 资源共享:多个模块共享某个资源的时候,比如需要一个全局的配置管理器来存储和管理配置信息、亦或是使用单例模式管理数据库连接池。
- 只有一个实例。
- 懒加载:对象创建本身就比较消耗资源,而且可能在整个程序中都不一定会使用。
单例模式的优点
- 全局控制:保证只有一个实例,这样就可以严格的控制怎样访问它以及何时访问它。
- 节省资源:避免多次创建了相同的对象,从⽽节省了系统资源,⽽且多个模块还可以通过单例实例共享数据。
- 懒加载:可以实现懒加载,需要时才实例化。
单例模式的基本原则
- 不允许外部代码创建实例。
- 唯一实例保存在私有静态变量中。
- 通过公有静态方法获取唯一实例。
单例模式的实现
- 饿汉式:类加载时就完成了实例创建。
- 懒汉式:需要使用实例时在创建。
- 多个线程同时获取实例时,并且在同一时刻检测到实例没有被创建,就可能会同时创建实例,从而导致多个实例被创建。这个时候需要使用同步机制。
单例模式参考代码:
1 | class Singleton { |
单例模式设计题
【设计模式专题之单例模式】1.小明的购物车 (kamacoder.com):
小明去了一家大型商场,拿到了一个购物车,并开始购物。请你设计一个购物车管理器,记录商品添加到购物车的信息(商品名称和购买数量),并在购买结束后打印出商品清单。(在整个购物过程中,小明只有一个购物车实例存在)。
输入包含若干行,每行包含两部分信息,分别是商品名称和购买数量。商品名称和购买数量之间用空格隔开。
输出包含小明购物车中的所有商品及其购买数量。每行输出一种商品的信息,格式为 “商品名称 购买数量”。
参考代码:
Singleton/main.cpp(github.com)
Singleton/main.cpp (gitee.com)
工厂方法模式
简单工厂模式:将产品的创建过程封装在一个工厂类中,把创建对象的流程集中在这个工厂类里面。
- 三个主要角色,工厂类、抽象产品、具体产品。
工厂方法模式是创建型设计模式。
- 简单工厂模式只有一个工厂类,负责创建所有产品,如果要添加新的产品,通常需要修改工厂类的代码。
- 工厂方法模式引⼊了抽象工厂和具体工厂的概念,每个具体工厂只负责创建一个具体产品,添加新的产品只需要添加新的工厂类而无需修改原来的代码。
工厂方法模式的角色:
- 抽象工厂:一个接口,包含一个抽象的工厂方法。
- 具体工厂:创建具体产品。
- 抽象产品:产品的接口。
- 具体产品:实现抽象产品接口,是工厂创建的对象。
工厂方法模式的使用情形
工厂方法模式使得每个工厂类的职责单一,每个工厂只负责创建一种产品。当创建对象涉及一系列复杂的初始化逻 辑,而这些逻辑在不同的子类中可能有所不同时,可以使用工厂方法模式将这些初始化逻辑封装在子类的工厂中。
工厂方法模式的实现
参考代码:
1 |
|
工厂方法模式设计题
【设计模式专题之工厂方法模式】2.积木工厂 (kamacoder.com):
小明家有两个工厂,一个用于生产圆形积木,一个用于生产方形积木,请你帮他设计一个积木工厂系统,记录积木生产的信息。
输入的第一行是一个整数 N(1 ≤ N ≤ 100),表示生产的次数。
接下来的 N 行,每行输入一个字符串和一个整数,字符串表示积木的类型。积木类型分为 “Circle” 和 “Square” 两种。整数表示该积木生产的数量。
对于每个积木,输出一行字符串表示该积木的信息。
参考代码:
FactoryMethod/main.cpp(github.com)
FactoryMethod/main.cpp(gitee.com)
抽象工厂模式
抽象工厂模式是创建型设计模式。抽象工厂模式可以确保一系列相关的产品被一起创建,这些产品能够相互配合使用。
在工厂方法模式中,每个具体工厂只负责创建单一的产品。但是如果有多类产品呢,比如说“手机”,一个品牌的手机有高端机、中低端机之分,这些具体的产品都需要建立一个单独的工厂类,但是它们都是相互关联的,都共同属于同一个品牌,这就可以使用到抽象工厂模式。
抽象工厂模式包括多个抽象产品、多个具体产品、一个抽象工厂和多个具体工厂,每个具体工厂负责创建一组相关产品。
简单工厂、工厂方法、抽象工厂的区别
-
简单工厂模式:一个工厂方法创建所有具体产品;
-
工厂方法模式:一个工厂方法创建一个具体产品;
-
抽象工厂模式:一个工厂方法可以创建一类具体产品。
工厂方法模式的使用情形
抽象工厂模式能够保证一系列相关的产品一起使⽤,并且在不修改客户端代码的情况下,可以方便地替换整个产品系列。但是当需要增加新的产品类时,除了要增加新的具体产品类,还需要修改抽象工厂及其所有的具体工厂类,扩展性相对较差。
典型的应用场景是使用抽象工厂模式来创建与不同数据库的连接对象。
抽象工厂模式的实现
遵循以下步骤:
- 定义(一个或多个)抽象产品,声明产品的公共方法。
- 实现具体产品类。
- 定义抽象工厂,声明一组可用于创建产品的方法。
- 实现具体工厂。
- 客户端中使用抽象工厂和抽象产品。
参考代码:
1 |
|
抽象工厂模式设计题
【设计模式专题之抽象工厂模式】3. 家具工厂 (kamacoder.com):
小明家新开了两个工厂用来生产家具,一个生产现代风格的沙发和椅子,一个生产古典风格的沙发和椅子,现在工厂收到了一笔订单,请你帮他设计一个系统,描述订单需要生产家具的信息。
输入的第一行是一个整数 N(1 ≤ N ≤ 100),表示订单的数量。
接下来的 N 行,每行输入一个字符串,字符串表示家具的类型。家具类型分为 “modern” 和 “classical” 两种。
对于每笔订单,输出字符串表示该订单需要生产家具的信息。
modern订单会输出下面两行字符串
modern chair
modern sofa
classical订单会输出下面两行字符串
classical chair
classical soft
参考代码:
AbstractFactory/main.cpp(github.com)
AbstractFactory/main.cpp(gitee.com)
建造者模式
建造者(生成器)模式是创建型设计模式。主要思想是将对象的构建过程分为多个步骤,每个步骤定义一个抽象接口,具体构建过程有具体建造者类完成,同时有一个指导者类负责协调建造者的工作。
建造者模式有以下角色:
- 产品:被构建的负责对象,包含多个组成部分。
- 抽象建造者:定义构建产品各部分的抽象类和一个返回复杂产品的方法。
- 具体建造者:实现抽象类的方法,构建产品各部分。
- 指导者:调用具体建造者的方法,按照一定顺序或逻辑构建。
建造者模式的使用情形
比如 Junit 中的测试构建器 TestBuilder,构建测试对象。
建造者模式的优点和缺点
- 将一个复杂对象的构建与其表示分离。
- 同样的构建过程可以创建不同的表示。
- 适用于复杂对象的创建。
- 当产品的构建过程发⽣变化时,可能需要同时修改指导类和建造者类,这就使得重构变得相对困难。
建造者模式的实现
参考代码:
1 |
|
建造者模式设计题
【设计模式专题之建造者模式】4. 自行车加工 (kamacoder.com):
小明家新开了一家自行车工厂,用于使用自行车配件(车架 frame 和车轮 tires )进行组装定制不同的自行车,包括山地车和公路车。
山地车使用的是Aluminum Frame(铝制车架)和 Knobby Tires(可抓地轮胎),公路车使用的是 Carbon Frame (碳车架)和 Slim Tries。
现在它收到了一笔订单,要求定制一批自行车,请你使用【建造者模式】告诉小明这笔订单需要使用那些自行车配置吧。
输入的第一行是一个整数 N(1 ≤ N ≤ 100),表示订单的数量。
接下来的 N 行,每行输入一个字符串,字符串表示客户的自行车需求。
字符串可以包含关键词 “mountain” 或 “road”,表示客户需要山地自行车或公路自行车。
对于每笔订单,输出该订单定制的自行车配置。
参考代码:
原型模式
原型模式是创建型设计模式。核心思想是基于现有的对象创建新的对象。
原型模式包含两个重点模块:
- 抽象原型类,且具有克隆自身的方法。
- 具体原型类,实现克隆方法,复制当前对象并返回一个新对象。
原型模式的使用情形
通过原型模式复制对象可以减少资源消耗,提高性能,尤其在对象的创建过程复杂或对象的创建代价较大的情况下。
当需要频繁创建相似对象、并且可以通过克隆避免重复初始化工作的场景时可以考虑使用原型模式。
在克隆对象的时候还可以动态地添加或删除原型对象的属性,创造出相似但不完全相同的对象,提高了灵活性。
原型模式的例子:
- Java 提供了 Object 类的
clone()
方法,可以实现对象的浅拷贝。类需要实现Cloneable
接口并重写clone()
方法。 - 在 .NET 中,ICloneable 接口提供了 Cloneable 接口并重写
Clone
方法,可以用于实现对象的克隆。 - Spring 框架中的 Bean 的作用域之一是原型作用域(Prototype Scope),在这个作用域下,Spring 框架会为每次请求创建⼀个新的 Bean 实例,类似于原型模式。
原型模式的实现
参考代码:
1 | class Prototype { |
原型模式设计题
【设计模式专题之原型模式】5. 矩形原型 (kamacoder.com):
公司正在开发一个图形设计软件,其中有一个常用的图形元素是矩形。设计师在工作时可能需要频繁地创建相似的矩形,而这些矩形的基本属性是相同的(颜色、宽度、高度),为了提高设计师的工作效率,请你使用原型模式设计一个矩形对象的原型。使用该原型可以快速克隆生成新的矩形对象。
首先输入一个字符串,表示矩形的基本属性信息,包括颜色、长度和宽度,用空格分隔,例如 “Red 10 5”。
然后输入一个整数 N(1 ≤ N ≤ 100),表示使用原型创建的矩形数量。
对于每个矩形,输出一行字符串表示矩形的详细信息,如 “Color: Red, Width: 10,Height: 5”。
参考代码:
Prototype/main.cpp(github.com)
结构型设计模式
适配器模式
适配器模式是结构型设计模式。它将一个类的接口转换成客户希望的另一个接口,充当两个不同接口的桥梁,让不兼容的类一起工作。
适配器模式的基本角色有:
- 目标接口:客户端希望使用的接口。
- 适配器类:实现客户端使用的接口,包含一个需要适配的类实例,起到转接扩展的作用。
- 被适配者:需要被适配的类。
适配器模式的使用情形
扮演着补救和扩展角色。
不同的项目和库可能使用不同的日志框架,不同的日志框架提供的 API 也不同,因此引⼊了适配器模式使得不同的 API 适配为统一接口。
Spring MVC 中,HandlerAdapter
接口就是适配器模式,将处理器适配到框架中,使得不同类型的处理器能够统一处理请求。
适配器模式的实现
1 | // 目标接口 |
适配器模式设计题
【设计模式专题之适配器模式】6. 扩展坞 (kamacoder.com):
小明购买了一台新电脑,该电脑使用 TypeC 接口,为了确保新电脑可以使用现有的USB接口充电器和数据线,他购买了一个TypeC到USB的扩展坞。
请你使用适配器模式设计并实现这个扩展坞系统,确保小明的新电脑既可以通过扩展坞使用现有的USB接口充电线和数据线,也可以使用TypeC接口充电。
题目包含多行输入,第一行输入一个数字 N (1 < N <= 20),表示后面有N组测试数据。
之后N行都是一个整数,1表示使用电脑本身的TypeC接口,2表示使用扩展坞的USB接口充电。
根据每行输入,输出相应的充电信息。
参考代码:
代理模式
代理模式是结构型设计模式,用于控制对其他对象的访问。
代理模式允许一个对象(代理)充当另一个对象(真实对象)的接口,以控制对这个对象的访问。
- 通常用于在访问某对象时引入间接层(中介作用),可以在访问对象时添加额外的控制逻辑,比如限制访问权限,延迟加载。
比如进行文件加载,为了避免直接访问“文件"对象,可以新增一个代理对象,代理对象中有一个对“文件对象"的引用,在代理对象的 1oad
方法中,可以在访问真实的文件对象之前进行一些操作,比如权限检查,
然后调用真实文件对象的 1oad
方法,最后在访问真实对象后进行其他操作,比如记录访问日志。
代理模式的角色有:
- 抽象主题:抽象类,声明真实主题和代理对象实现的业务方法。
- 真实主题:定义了代理类所代表的真实对象。
- 代理类。
代理模式的使用情形
代理模式可以在实际操作的前后添加一些额外的操作,但在多个对象交互之间可能会增加复杂性且降低性能。
代理模式在许多工具和库中也有应用:
- Spring 框架的 AOP 模块;
- Java 提供动态代理机制;
- Android 的 Glide 框架 使用代理模式实现图片的延迟加载。
代理模式的实现
1 | // 抽象对象类 |
代理模式目的是控制对对象的访问,同时还可以加入一些额外的逻辑;
适配器模式目的是使不兼容的对象能够协同工作,将一个类的接口转换成另一个类的接口。
代理模式的设计题
【设计模式专题之代理模式】7-小明买房子 (kamacoder.com):
小明想要购买一套房子,他决定寻求一家房屋中介来帮助他找到一个面积超过100平方米的房子,只有符合条件的房子才会被传递给小明查看。
第一行是一个整数 N(1 ≤ N ≤ 100),表示可供查看的房子的数量。
接下来的 N 行,每行包含一个整数,表示对应房子的房屋面积。
对于每个房子,输出一行,表示是否符合购房条件。如果房屋面积超过100平方米,输出 “YES”;否则输出 “NO”。
参考代码:
装饰模式
装饰模式是结构型设计模式。
- 在不定义子类的情况下动态的给对象添加⼀些额外的功能。
举个例子,假设有一个基础的图形类,想要为图形类添加颜色、边框、阴影等功能,如果每个功能都实现一个子类,就会导致产生大量的类。这时就可以考虑使用装饰模式来动态地添加,而不需要修改图形类本身的代码,这样可以使得代码更加灵活、更容易维护和扩展。
装饰模式包含四个角色:
- 组件:抽象类,是具体组件和装饰者的父类,定义了具体组件需要实现的方法。
- 具体组件:实现组件的具体方法,是被装饰的对象。
- 装饰类:一个抽象类,给具体组件添加功能,但具体功能由具体装饰者完成,包含一个组件对象引用。
- 具体装饰类:扩展实现装饰类,负责向组件对象添加新的行为。
装饰模式的使用情形
- 不希望使用继承生成子类,给现有的类添加附加功能时;
- 动态的添加和覆盖功能。
Java 的 I/O 库 中,装饰模式用于增强 I/O 的功能。
装饰模式的实现
1 | class Component { |
装饰模式的设计题
【设计模式专题装饰模式】8-咖啡加糖 (kamacoder.com):
小明喜欢品尝不同口味的咖啡,他发现每种咖啡都可以加入不同的调料,比如牛奶、糖和巧克力。他决定使用装饰者模式制作自己喜欢的咖啡。
请设计一个简单的咖啡制作系统,使用装饰者模式为咖啡添加不同的调料。系统支持两种咖啡类型:黑咖啡(Black Coffee)和拿铁(Latte)。
多行输入,每行包含两个数字。第一个数字表示咖啡的选择(1 表示黑咖啡,2 表示拿铁),第二个数字表示要添加的调料类型(1 表示牛奶,2 表示糖)。
根据每行输入,输出制作咖啡的过程,包括咖啡类型和添加的调料。
参考代码:
Decorator/main.cpp(github.com)
外观模式
外观模式是结构型设计模式。
- 定义一个高层接口,使得子系统更容易使用,同时也隐藏了子系统。
外观模式的角色有:
- 外观类:对外的一个统一的高层接口。
- 子系统类:实现子系统的功能,处理外观类指派的任务。
外观模式的使用情形
外观模式隐藏了系统的复杂性,使得客户端不需要直接与子系统交互,只需与外观接口交互即可。
但是如果要添加子系统或者修改子系统的行为,那么需要修改外观类,违背“开闭原则”。
使用外观模式的例子:
- Spring 框架的
ApplicationContext
可以看作是外观。 - JDBC 提供了一个用于数据库交互的接口,
DriverManager
类。 - Android 系统的 API。
外观模式的实现
参考代码:
1 | class SubSystemA { |
外观模式的设计题
【设计模式专题之外观模式】9-电源开关 (kamacoder.com):
小明家的电源总开关控制了家里的三个设备:空调、台灯和电视机。每个设备都有独立的开关密码,分别用数字1、2和3表示。即输入1时,空调关闭,输入2时,台灯关闭,输入3时,电视机关闭,当输入为4时,表示要关闭所有设备。请你使用外观模式编写程序来描述电源总开关的操作。
第一行是一个整数 N(1 <= N <= 100),表示后面有 N 行输入。
接下来的 N 行,每行包含一个数字,表示对应设备的开关操作(1表示关闭空调,2表示关闭台灯,3表示关闭电视机,4表示关闭所有设备)。
输出关闭所有设备后的状态,当输入的数字不在1-4范围内时,输出Invalid device code.
参考代码:
桥接模式
桥接模式是结构型设计模式。其 UML 图很像一座桥。
- 将抽象部分与实现部分分离,通过组合建立两个类之间的联系,而不是继承。
桥接模式的角色有:
- 抽象:抽象类,定义抽象部分的接口,维护一个对实现的引用。
- 修正抽象:对抽象类进行扩展。
- 实现:定义实现的接口,抽象化接口的实现。
- 具体实现:实现接口的具体类,实现具体操作。
举个例子,图形编辑器中,每一种图形都需要蓝色、红色、黄色不同的颜色。
- 不使用桥接模式,可能需要为每一种图形类型和每一种颜色都创建一个具体的子类;
- 使用桥接模式可以将图形和颜色两个维度分离,两个维度都可以独立进行变化和扩展,如果要新增其他颜色,只需添加新的 Co1or 子类,不影响图形类;反之亦然。
桥接模式的使用情形
使用情况:
- 一个类存在两个独立变化的维度,且两个维度都需要扩展时;
- 不希望使用继承时。
适用于多个独立变化维度,需要灵活扩展的系统。
桥接模式的实现
1 | // 实现 |
桥接模式的设计题
【设计模式专题之桥接模式】10-万能遥控器 (kamacoder.com):
小明家有一个万能遥控器,能够支持多个品牌的电视。每个电视可以执行开机、关机和切换频道的操作,请你使用桥接模式模拟这个操作。
第一行是一个整数 N(1 <= N <= 100),表示后面有 N 行输入。
接下来的 N 行,每行包含两个数字。第一个数字表示创建某个品牌的遥控和电视,第二个数字表示执行的操作。
其中,0 表示创建 Sony 品牌的电视,1 表示创建 TCL 品牌的遥控和电视;
2 表示开启电视、3表示关闭电视,4表示切换频道。
对于每个操作,输出相应的执行结果。
参考代码:
组合模式
组合模式是结构型设计模式。
- 将对象组合成树状结构来表示部分和整体的层次关系。
组合模式使得客户端可以统一处理单个对象和对象的集合。
组合模式的角色有:
- 组件:根节点,定义组合中所有对象的通用接口,定义共性内容。
- 叶子:实现组件的内容,表示组合中的叶子对象。
- 合成:存储子部件,实现对子部件的相关操作,比如添加、删除、获取子组件等。
比如,省份中包含了多个城市,如果比喻成一个树形结构,城市就是叶子节点,它是省份的组成部分,而省份就是合成节点,可以包含其他城市。省份和城市都是组件,它们都有一些共同的操作,比如获取信息。
通过组合模式,整个省份的获取信息操作可以一次性执行,无需关心省份中的具体城市。
组合模式的使用情形
可以使得客户端统一处理单个对象和组合对象。适用于任何需要构建具有部分-整体层次结构的场景,比如组织架构管理、文件系统的文件和文件夹组织等。
组合模式的实现
1 | // 组件 |
组合模式的设计题
【设计模式专题之组合模式】11-公司组织架构 (kamacoder.com):
小明所在的公司内部有多个部门,每个部门下可能有不同的子部门或者员工。
请你设计一个组合模式来管理这些部门和员工,实现对公司组织结构的统一操作。部门和员工都具有一个通用的接口,可以获取他们的名称以及展示公司组织结构。
第一行是一个整数 N(1 <= N <= 100),表示后面有 N 行输入。
接下来的 N 行,每行描述一个部门或员工的信息。部门的信息格式为 D 部门名称,员工的信息格式为 E 员工名称,其中 D 或 E 表示部门或员工。
输出公司的组织结构,展示每个部门下的子部门和员工
参考代码:
Combination/main.cpp(github.com)
Combination/main.cpp(gitee.com)
享元模式
享元模式是结构型设计模式。
- 对象被设计为可共享的,可被多个上下文使用。
认识并区分内部状态和外部状态:
- 内部状态:指那些可以被多个对象共享的状态,存储在享元对象内部,对于所有享元对象都是相同的,这部分状态通常是不变的。
- 外部状态:享元对象依赖的、可变的部分,这部分状态不存储在享元对象内部,而是使用享元对象时通过参数传递给对象。
享元模式的角色有:
- 抽象享元类:所以具体享元类的共享接口,包含对外部状态的操作。
- 具体享元类:继承实现享元接口,包含内部状态。
- 享元工厂类:创建并管理享元对象,当用户请求时,提供实例。
- 客户端:维护外部状态,在使用享元对象时,将外部状态传递给享元对象。
享元模式的使用情形
享元模式适用于包含大量相似对象,并且这些对象的内部状态可以共享。
具体的应用场景包括文本编辑器,图形编辑器,游戏中的角色创建,这些对象的内部状态比较固定(外观,技能,形状),但是外部状态变化比较大时,可以使用。
享元模式的实现
1 | class FlyWeight { |
享元模式的设计题
【设计模式专题之享元模式】12-图形编辑器 (kamacoder.com):
在一个图形编辑器中,用户可以绘制不同类型的图形,包括圆形(CIRCLE)、矩形(RECTANGLE)、三角形(TRIANGLE)等。现在,请你实现一个图形绘制程序,要求能够共享相同类型的图形对象,以减少内存占用。
输入包含多行,每行表示一个绘制命令。每个命令包括两部分:
图形类型(Circle、Rectangle 或 Triangle)
绘制的坐标位置(两个整数,分别表示 x 和 y)
对于每个绘制命令,输出相应图形被绘制的位置信息。如果图形是首次绘制,输出 “drawn at”,否则输出 “shared at”。
参考代码:
FlyWeight/main.cpp(github.com)
行为型设计模式
观察者模式
观察者模式(发布-订阅模式)是行为型设计模式。
- 定义了一种一对多的依赖关系,多个观察者对象同时监听一个主题对象,当主题对象的状态发生变化时,所有依赖于它的观察者都得到通知并被自动更新。
观察者模式有两个角色:
- 主题:被观察的对象,维护一组观察者,自身变化时通知观察者。
- 观察者:观察主题的对象,当主题发生变化,会得到通知。
具体可以设计为四个角色:
- 抽象主题:抽象类,提供注册、删除和通知观察者的方法,通常包含一个状态。
- 抽象观察者:抽象类,包含一个更新方法。
- 具体主题:主题的具体实现,维护一个观察者列表,实现抽象类的方法。
- 具体观察者:观察者的具体实现,每个具体观察者都注册到具体主题中,实现抽象类方法。
观察者模式可以将主题和观察者之间的关系解耦,主题只需要关注自己的状态变化,而观察者只需要关注在主题状态变化时需要执行的操作,两者互不干扰,并且由于观察者和主题是相互独立的,可以轻松的增加和删除观察者,这样实现的系统更容易扩展和维护。
观察者模式的使用情形
观察者模式特别适用于一个对象的状态变化会影响到其他对象,并且希望这些对象在状态变化时能够自动更新的情况。
- 图形用户界面中,按钮、滑动条等组件的状态变化可能需要通知其他组件更新,这使得观察者模式被广泛应用于 GUl 框架,比如 Java 的 Swing 框架。
- 前端开发中,比较典型的例子是前端框架 Vue,当数据发生变化时,视图会自动更新。
- 分布式系统中,观察者模式可以用于实现节点之间的消息通知机制,节点的状态变化将通知其他相关节点。
观察者模式的实现
1 | class Observer { |
观察者模式的设计题
【设计模式专题之观察者模式】13. 时间观察者 (kamacoder.com):
小明所在的学校有一个时钟(主题),每到整点时,它就会通知所有的学生(观察者)当前的时间,请你使用观察者模式实现这个时钟通知系统。
注意点:时间从 0 开始,并每隔一个小时更新一次。
输入的第一行是一个整数 N(1 ≤ N ≤ 20),表示学生的数量。
接下来的 N 行,每行包含一个字符串,表示学生的姓名。
最后一行是一个整数,表示时钟更新的次数。
对于每一次时钟更新,输出每个学生的姓名和当前的时间。
参考代码:
策略模式
策略模式是行为型设计模式。
- 定义一系列算法(完成相同工作,实现不同),并将每个算法封装起来,可以相互替换,算法的变化不会影响使用算法的客户。
策略模式的角色有:
- 策略类:抽象类,定义所有支持的算法。
- 具体策略类:实现策略类的方法。
- 上下文类:包含一个策略实例,并在需要时调用策略对象方法。
举个例子,电商网站对于商品的折扣策略有不同的算法,比如新用户满减优惠,不同等级会员的打折情况不同。
-
一般情况下,产生大量的 if-e1se 语句,并且如果优惠政策修改时,还需要修改原来的代码,不符合开闭原则。
-
可以将不同的优惠算法封装成独立的类来避免大量的条件语句,如果新增优惠算法,可以添加新的策略类来实现,客户端在运行时选择不同的具体策略,而不必修改客户端代码改变优惠策略。
策略模式的使用情形
使用策略模式的情形:
-
当一个系统根据业务场景需要动态地在几种算法中选择一种时,例如,根据用户的行为选择不同的计费策略。
-
当代码中存在大量条件判断,条件判断的区别仅仅在于行为。
在已有的工具库中,Java 标准库中的 Comparator 接口就使用了策略模式,通过实现这个接口,可以创建不同的比较器(指定不同的排序策略)来满足不同的排序需求。
策略模式的实现
1 | class Strategy { |
策略模式的设计题
【设计模式专题之策略模式】14. 超市打折 (kamacoder.com):
小明家的超市推出了不同的购物优惠策略,你可以根据自己的需求选择不同的优惠方式。其中,有两种主要的优惠策略:
- 九折优惠策略:原价的90%。
- 满减优惠策略:购物满一定金额时,可以享受相应的减免优惠。
具体的满减规则如下:
满100元减5元
满150元减15元
满200元减25元
满300元减40元
请你设计一个购物优惠系统,用户输入商品的原价和选择的优惠策略编号,系统输出计算后的价格。
输入的第一行是一个整数 N(1 ≤ N ≤ 20),表示需要计算优惠的次数。
接下来的 N 行,每行输入两个整数,第一个整数M( 0 < M < 400) 表示商品的价格, 第二个整数表示优惠策略,1表示九折优惠策略,2表示满减优惠策略
每行输出一个数字,表示优惠后商品的价格
参考代码:
命令模式
命令模式是行为型设计模式。
- 允许将请求封装成一个对象(命令对象,包含执行操作所需的所有信息),并将命令对象按照一定顺序存储在队列中,再逐一调用执行,命令支持反向操作、撤消重做。
命令模式的角色有:
- 抽象命令类:抽象类,定义执行操作的接口。
- 具体命令类:实现命令,执行具体操作。
- 接收者类:接受并执行命令的对象。
- 调用者:发起请求的对象。不关心命令的具体实现。
使用时创建具体的命令对象和接收者对象,将其组装起来。
命令模式的使用情形
命令模式在需要将请求封装成对象、支持撤销和重做、设计命令队列等情况下,都是一个有效的设计模式。
-
撤销操作:需要支持撤销操作,命令模式可以存储历史命令,轻松实现撤销功能。
-
队列请求:命令模式可以将请求排队,形成一个命令队列,依次执行命令。
-
可扩展性:可以很容易地添加新的命令类和接收者类,而不影响现有的代码。新增命令不需要修改现有代码,符合开闭原则。
-
但是对于每个命令,都会有一个具体命令类,这可能导致类的数量急剧增加,增加了系统的复杂性。
命令模式同样有着很多现实场景的应用:
- 比如 Git 中的很多操作,如提交(commit)、合并(merge)等,都可以看作是命令模式的应用,用户通过执行相应的命令来操作版本库。
- Java 的 GUI 编程中,很多事件处理机制也都使用了命令模式。例如,每个按钮都有一个关联的Action,它代表一个命令,按钮的点击触发 Action 的执行。
命令模式的实现
1 | // 抽象命令类 |
命令模式的设计题
【设计模式专题之命令模式】15-自助点餐机 (kamacoder.com):
小明去奶茶店买奶茶,他可以通过在自助点餐机上来点不同的饮品,请你使用命令模式设计一个程序,模拟这个自助点餐系统的功能。
输入第一行是一个整数 n(1 ≤ n ≤ 100),表示点单的数量。接下来的 n 行,每行包含一个字符串,表示点餐的饮品名称。
输出执行完所有点单后的制作情况,每行输出一种饮品的制作情况。如果制作完成,输出 “XXX is ready!”,其中 XXX 表示饮品名称。
参考代码:
中介者模式
中介者模式也称为调停者模式,是行为型设计模式。
- 通过一个中介对象来封装一组对象之间的交互,从而使得这些对象之间不需要相互引用。
中介者模式的角色有:
- 抽象中介者:抽象类,定义中介者接口,用于各个具体同事对象之间的通信。
- 具体中介者:实现抽象类方法,协调各个具体同事对象的交互关系。
- 抽象同事类:抽象类,定义同事类接口,维护一个对中介者对象的引用,用于通信。
- 具体同事类:实现抽象类方法,每个具体同事类只知道自己的行为,不了解其他同事类的情况。
与代理模式区别
中介者模式与代理模式在表述上类似,但是解决不同类型的问题:
- 中介者模式通过一个中介者对象,使得系统中的其他对象通过中介者进行通信交互,降低了系统各个对象间的直接耦合。
- 代理模式通过一个代理类,使得客户端可以与目标对象进行通信,且可以在调用实际目标对象方法前后进行额外的操作,控制对象的访问。
中介者模式的使用情形
中介者模式使得同事对象不需要知道彼此的细节,只需要与中介者进行通信,简化了系统的复杂度,也降低了各对象之间的耦合度,但是这也会使得中介者对象变得过于庞大和复杂,如果中介者对象出现问题,整个系统可能会受到影响。
中介者模式适用于当系统对象之间存在复杂的交互关系或者系统需要在不同对象之间进行灵活的通信时使用,可以使得问题简化,
中介者模式的实现
1 | class Colleague; |
中介者模式的设计题
【设计模式专题之中介者模式】16-简易聊天室 (kamacoder.com):
小明正在设计一个简单的多人聊天室系统,有多个用户和一个聊天室中介者,用户通过中介者进行聊天,请你帮他完成这个系统的设计。
第一行包括一个整数N,表示用户的数量(1 <= N <= 100) 第二行是N个用户,比如User1 User2 User3,用空格分隔。第三行开始,每行包含两个字符串,表示消息的发出者和消息内容,用空格分隔。
对于每个用户,输出一行,包含该用户收到的所有消息内容。
参考代码:
备忘录模式
备忘录模式是行为型设计模式。
- 允许在不暴露对象实现的情况下捕获对象的内部状态并在对象之外保存这个状态,以便可以还原状态。
备忘录的角色有:
- 发起人:需要还原状态的对象,负责创建备忘录,使用备忘录记录。
- 备忘录:存储发起人对象的内部状态,包含发起人部分或全部状态信息,但对外不可见,仅发起人可见。
- 管理者:负责(一个或多个)存储备忘录对象,但不了解其内部结构。
备忘录模式的使用情形
备忘录模式在保证了对象内部状态的封装和私有性前提下可以轻松地添加新的备忘录和发起人,实现“备份”,不过备份对象往往会消耗较多的内存,资源消耗增加。
备忘录模式常常用来实现撤销和重做功能,比如在 Java Swing GUI 编程中,javax.swing.undo
包中的撤销(undo)和重做(redo)机制使用了备忘录模式。UndoManager
和 UndoableEdit
接口是与备忘录模式相关的主要类和接口。
备忘录模式的实现
1 | // 备忘录 |
备忘录模式的设计题
【设计模式专题之备忘录模式】17-redo计数器应用 (kamacoder.com):
小明正在设计一个简单的计数器应用,支持增加(Increment)和减少(Decrement)操作,以及撤销(Undo)和重做(Redo)操作,请你使用备忘录模式帮他实现。
输入包含若干行,每行包含一个字符串,表示计数器应用的操作,操作包括 “Increment”、“Decrement”、“Undo” 和 “Redo”。
对于每个 “Increment” 和 “Decrement” 操作,输出当前计数器的值,计数器数值从0开始 对于每个 “Undo” 操作,输出撤销后的计数器值。 对于每个 “Redo” 操作,输出重做后的计数器值。
参考代码:
模板方法模式
模板方法模式是行为型设计模式。
- 定义一个算法骨架,将一些步骤的实现延迟到子类。
模板方法模式使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
举个简单的例子,做一道菜通常都需要包含至少三步:准备食材;亨饪过程;上菜。
不同菜品的亨饪过程是不一样的,但是可以先定义一个“骨架”,包含这三个步骤,亨饪过程的过程放到具体的炒菜类中去实现,这样,无论炒什么菜,都可以沿用相同的炒菜算法,只需在子类中实现具体的炒菜步骤,从而提高了代码的复用性。
模板方法模式的角色有:
- 模板类:一个模板方法和若干个基本方法构成。
- 模板方法定义逻辑的骨架,按照顺序调用包含的基本方法。
- 基本方法通常是抽象方法,由子类实现。
- 基本方法还包含一些具体方法,它们是算法的一部分但已经有默认实现,可以在具体子类中继承或重写。
- 具体类:继承自模板类,实现在模板类中定义的抽象方法。
模板方法模式的使用情形
模板方法模式将算法的不变部分被封装在模板方法中,而可变部分算法由子类继承实现,这样做可以提高代码的复用性,但是当算法的框架发生变化时,可能需要修改模板类,这也会影响到所有的子类。
总体来说,当算法的整体步骤很固定,但是个别步骤在更详细的层次上的实现可能不同时,通常考虑模板方法模式来处理。如:
- Spring 框架中的
JdbcTemplate
类使用了模板方法模式,其中定义了一些执行数据库操作的模板方法,具体的数据库操作由回调函数提供。 - Java 的 JDK 源码中,
AbstractList
类也使用了模板方法模式,它提供了一些通用的方法,其中包括一些模板方法。具体的列表操作由子类实现。
模板方法模式的实现
1 | // 模板类 |
模板方法模式的设计题
【设计模式专题之模板方法模式】18-咖啡馆 (kamacoder.com):
小明喜欢品尝不同类型的咖啡,她发现每种咖啡的制作过程有一些相同的步骤,他决定设计一个简单的咖啡制作系统,使用模板方法模式定义咖啡的制作过程。系统支持两种咖啡类型:美式咖啡(American Coffee)和拿铁(Latte)。
咖啡制作过程包括以下步骤:
- 研磨咖啡豆 Grinding coffee beans
- 冲泡咖啡 Brewing coffee
- 添加调料 Adding condiments
其中,美式咖啡和拿铁的调料添加方式略有不同, 拿铁在添加调料时需要添加牛奶Adding milk。
多行输入,每行包含一个数字,表示咖啡的选择(1 表示美式咖啡,2 表示拿铁)。
根据每行输入,输出制作咖啡的过程,包括咖啡类型和各个制作步骤,末尾有一个空行。
参考代码:
TemplateMethod/main.cpp(github.com)
TemplateMethod/main.cpp(gitee.com)
迭代器模式
迭代器模式是行为型设计模式。
- 提供一种统一的方式访问一个聚合对象中的各个元素,而不暴露该对象的内部表示。
迭代器模式的角色有:
- 迭代器抽象类:定义访问和遍历元素的接口。
- 具体迭代器:实现抽象迭代器。
- 抽象聚合类:定义创建迭代器接口,创建迭代器对象。
- 具体聚合类:实现抽象聚合类方法。
迭代器模式的使用情形
迭代器模式使用很广泛。客户端不需要知道集合的内部结构,只需要关心迭代器和迭代器接口就可以完成元素的访问。如:
- Java 的集合类,
ArrayList
、LinkedList
。 - Python 的
iter()
、next()
。 - C++ 中 STL 的迭代器,
begin()
、end()
。
迭代器模式的实现
1 | // 抽象迭代器 |
迭代器模式的设计题
【设计模式专题之迭代器模式】19-学生名单 (kamacoder.com):
小明是一位老师,在进行班级点名时,希望有一个学生名单系统,请你实现迭代器模式提供一个迭代器使得可以按顺序遍历学生列表。
第一行是一个整数 N (1 <= N <= 100), 表示学生的数量。
接下来的 N 行,每行包含一个学生的信息,格式为 姓名 学号
输出班级点名的结果,即按顺序遍历学生列表,输出学生的姓名和学号
参考代码:
状态模式
状态模式是行为型设计模式。
- 将对象每个状态的行为封装在一个具体类中,使得每个状态类相互独立,对象从而可以在运行时动态改变。
状态模式的角色有:
- 抽象状态类:抽象类,封装 Context 的一个特定状态相关的行为。
- 具体状态类:为每一个具体状态实现一个行为。
- Context 类:维护一个具体状态的子类实例,实例定义当前状态。
状态模式的使用情形
适用于一个对象在不同状态下有不同的行为。
适用于有限状态机的场景,其中对象的行为在运行时可以根据内部状态的改变而改变。
在游戏开发中,Unity 3D 的 Animator 控制器就是一个状态机。它允许开发人员定义不同的状态(动画状态),并通过状态转换来实现角色的动画控制和行为切换。
状态模式的实现
1 | // 抽象状态类 |
状态模式的设计题
【设计模式专题之状态模式】20-开关台灯 (kamacoder.com):
小明家有一个灯泡,刚开始为关闭状态(OffState)。台灯可以接收一系列的指令,包括打开(“ON”)、关闭(“OFF”)和闪烁(“blink”)。每次接收到一个指令后,台灯会执行相应的操作,并输出当前灯泡的状态。请设计一个程序模拟这个灯泡系统。
第一行是一个整数 n(1 <= n <= 1000),表示接收的命令数量。
接下来的 n 行,每行包含一个字符串 s,表示一个命令(“ON”、“OFF"或"blink”)。
对于每个命令,输出一行,表示执行该命令后灯泡的状态。
参考代码:
责任链模式
责任链模式是行为型设计模式。
- 允许构建一个对象链,请求从链的一端进入,沿着链上的对象依次处理,直至链上某个对象能够处理该请求。
责任链模式的角色有:
- 处理者:定义一个处理请求的接口,包含一个处理请求的抽象方法和一个指向下一处理者的链接。
- 具体处理者:实现处理请求的方法,判断能否处理,能处理则处理,否则传递下一处理者。
责任链模式的使用情形
责任链模式优点有:
-
降低耦合度:将请求的发送者和接收者解耦,每个具体处理者都只负责处理与自己相关的请求,客户端不需要知道具体是哪个处理者处理请求。
-
增强灵活性:可以动态地添加或删除处理者,改变处理者之间的顺序以满足不同需求。
但是由于一个请求可能会经过多个处理者,这可能会导致一些性能问题,并且如果整个链上也没有合适的处理者来处理请求,就会导致请求无法被处理。
实际使用有 Java 开发中过滤器的链式处理,以及 Spring 框架中的拦截器,都组装成一个处理链对请求、响应进行处理。
责任链模式的实现
1 | // 处理者 |
责任链模式的设计题
【设计模式专题之责任链模式】21-请假审批 (kamacoder.com):
小明所在的公司请假需要在OA系统上发布申请,整个请求流程包括多个处理者,每个处理者负责处理不同范围的请假天数,如果一个处理者不能处理请求,就会将请求传递给下一个处理者,请你实现责任链模式,可以根据请求天数找到对应的处理者。
审批责任链由主管(Supervisor), 经理(Manager)和董事(Director)组成,他们分别能够处理3天、7天和10天的请假天数。如果超过10天,则进行否决。
第一行是一个整数N(1 <= N <= 100), 表示请求申请的数量。
接下来的N行,每行包括一个请求申请的信息,格式为"姓名 请假天数"
对于每个请假请求,输出一行,表示该请求是否被批准。如果被批准/否决,输出被哪一个职级的人批准/否决。
参考代码:
ResponsibilityChain/main.cpp(github.com)
ResponsibilityChain/main.cpp(gitee.com)
解释器模式
解释器模式是行为型设计模式。
- 定义了语言的文法,并且建立一个解释器解释句子。
解释器模式的角色有:
- 抽象表达式:定义了解释器抽象类,包含解释器方法。
- 终结符表达式:语法中不能再分解为更小单元的符号。
- 非终结符表达式:复杂表达式,由终结符和其他非终结符组成。
- 上下文:解释器之外的全局信息,存储解释器中间结果,也可以向解释器传递信息。
比如表达式 1+1
,数字 1
是终结符,而运算符 +
需要两个操作数,属于非终结符。
解释器模式的使用情形
当需要解释和执行特定领域或业务规则的语言时,可以使用解释器模式。例如:
- SQL 解释器;
- 正则表达式解释器。
但是需要注意的是解释器模式可能会导致类的层次结构较为复杂,同时也可能不够灵活,使用要慎重。
解释器模式的实现
1 | // 抽象表达式 |
解释器模式的设计题
【设计模式专题之解释器模式】22-数学表达式 (kamacoder.com):
小明正在设计一个计算器,用于解释用户输入的简单数学表达式,每个表达式都是由整数、加法操作符+、乘法操作符组成的,表达式中的元素之间用空格分隔,请你使用解释器模式帮他实现这个系统。
每行包含一个数学表达式,表达式中包含整数、加法操作符(+)和乘法操作符(*)。 表达式中的元素之间用空格分隔。
对于每个输入的数学表达式,每行输出一个整数,表示对应表达式的计算结果。
参考代码:
Interpreter/main.cpp(github.com)
Interpreter/main.cpp(gitee.com)
访问者模式
访问者模式是行为型设计模式。
- 在不改变对象结构的前提下,对对象中的元素进行新的操作。
访问者模式的角色有:
- 抽象访问者:抽象类,声明访问者可以访问的元素,以及声明访问方法。
- 具体访问者:实现了抽象类的方法。
- 抽象元素:定义方法接受访问者的访问。
- 具体元素:实现抽象元素的方法。
- 对象结构:元素的集合,负责遍历元素,并调用元素的接受方法。
访问者模式的使用情形
访问者模式结构较为复杂,但是访问者模式将同一类操作封装在一个访问者中,使得相关的操作彼此集中,提高了代码的可读性和维护性。
常用于对象结构比较稳定,但经常需要在此对象结构上定义新的操作,这样就无需修改现有的元素类,只需要定义新的访问者来添加新的操作。
访问者模式的实现
1 | class Visitor; |
访问者模式的设计题
【设计模式专题之访问者模式】23-图形的面积 (kamacoder.com):
小明家有一些圆形和长方形面积的土地,请你帮他实现一个访问者模式,使得可以通过访问者计算每块土地的面积。
图形的面积计算规则如下:
- 圆形的面积计算公式为:3.14 * 半径 * 半径
- 矩形的面积计算公式为:长 * 宽
第一行是一个整数 n(1 <= n <= 1000),表示图形的数量。
接下来的 n 行,每行描述一个图形,格式为 “Circle r” 或 “Rectangle width height”,其中 r、width、height 是正整数。
对于每个图形,输出一行,表示该图形的面积。
参考代码: