Yuki 075d6cb132
feat: Iterator
Implemented a demo for the Iterator.
2025-05-16 02:55:50 +08:00

116 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 设计模式笔记
## 简单工厂Simple Factory
简单工厂将对象的创建过程与使用过程解耦,客户端只需传入参数,而不需要关心创建的细节和具体的子类类型。这样,当需要添加新的产品子类时,只需在工厂中扩展创建逻辑,无需修改客户端代码,从而实现了对变化的封装,提升了系统的可扩展性与可维护性。
## 工厂模式Factory Method
简单工厂里,我们的输入参数是一样的,如何处理建造时参数不同的情况呢?例如 `Circle` 接受圆心和半径,而 `Square` 接受两个对角的点。
工厂模式给出了解决方案。对于每个新的子类,有一个专门的工厂类生成这个新的子类,进而工厂类可以使用不同的参数构建新的子类。
工厂模式相对简单工厂,通过解耦获取工厂和生成子类实例,获取了更大的灵活性。比如客户端可以在得到工厂后,进一步根据工厂类型向用户请求输入。但是对于一个新类就需要额外需要一个工厂,稍微麻烦些。通过同时使用两种方式,可以达到最大的灵活性。
## 抽象工厂Abstract Factory
抽象工厂进一步的将工厂本身也作为一种产品。它定义了一个“工厂族”的接口,规定了工厂需要生产哪些种类的产品。客户端从抽象工厂获取一个具体的工厂,再从这个工厂中制造一组相互关联的具体产品。
这可以用于创建一系列相关或相互依赖的对象,而不指定它们的具体类。也就是说,它解决了产品族一致性的问题。这在组件风格切换、平台适配、多模块协作等方面很有用。
## 生成器模式Builder
假定有一种复杂对象(比如电脑,需要有 CPU内存主板等如何构建这种复杂对象呢朴素的做法是有一个接受很多参数的构造函数这显然不好因为参数不总是都用上的。另一种做法是扩展电脑基类但是这意味着给电脑增加新的外设会很复杂因为需要添加子类。
解决方案是有一种生成器类专门负责构建复杂对象。对于每一种参数,生成器类定义了接受各个参数的函数,从而做到分步创建对象。参数输入结束后,生成器类可以基于之前输入的参数创建这一复杂对象。
对于复杂场景,比如生成器需要输入十种参数,可以添加一个指导者封装这些输入这几种参数的过程。它提供了一种简化的构建流程,客户端不需要关心构建细节,代价是牺牲了一定的灵活性,因为客户无法定制每一步。
## 单例模式Singleton
有些时候,我们希望一个类只被实例化一次,且能在全局被访问,比如一个事件模拟器。朴素的想法是直接用一个全局变量。但这使得模块间强耦合,这些模块依赖隐式的、看不见的全局状态。且多个全局变量出现时依赖难以管理。
单例模式是一种解决方案,这个类只通过静态的 `get_instance()` 提供统一访问方式,并且不提供拷贝构造和赋值操作。这样多个依赖的全局变量可以在这个类里管理。此外测试时可以只改内部实现,对外接口不变,提高了可维护性与可测试性。
具体实现上有一些技巧。为了只创建一次,可以用 `get_instance()` 中使用 `static` 变量(懒汉式),也可以这个类有一个 `static` 类成员(饿汉式),还可以用 `static` 指针成员,对于指针为空时(第一次访问)额外做初始化(需要注意创建时的线程安全,比如双检查)。
## 原型模式Prototype Pattern
有时需要在项目中创建副本,那么如何创建重复对象?很容易想到使用复制或赋值操作符,而不是普通的构造函数。但是这些方法没有多态性,如果很多子类需要复制就会有点麻烦。
解决方案也很简单,父类提供一种 `clone` 方法。这样客户端只需要调用 `clone` 方法,不需要知道这个类具体是什么。这种方法常配合对象注册表使用,通过维护一个原型缓存容器(如`map<string, Prototype*>`),根据名字/类型复制对象。
## 适配器模式Adapter
在开发过程中,可能会遇到接口不兼容的情况。例如,类 `A` 有一个向服务器发送请求的接口,而类 `B` 也需要实现类似的功能,但它并不继承自 `A`,因此无法通过多态方式调用 `A` 的接口。
一种解决方法是让 `B` 继承 `A`,这是一个很推荐的做法,但是这样做并不总是可行的,特别是当 `B` 是由他人设计或无法修改时。此时,可以设计一个适配器类。
适配器类可以继承自 `A`,并将 `B` 作为构造参数。在构造过程中,适配器将 `B` 的接口转换为符合 `A` 的接口形式。这样,通过适配器,`B` 就可以像 `A` 一样调用接口,达到接口兼容的效果。考虑到适配性,比如希望这个适配器类还能适配其他的 `C``D` 等,可以让适配器类接受一个更普遍的共性作为构造参数。
需要注意的是,这种做法显然会使得代码复杂很多,条件允许的话,还是直接修改 `B` 类较好。
## 桥接模式Bridge
有时类有多个变化维度需要组合。以图形为例,它既有“形状”的维度,也有“纹理”的维度。若采用继承来组合这两个维度,将导致类数量呈乘法增长(`M * N`)。而且,一个维度的变更都可能影响到多个类。
桥接模式给出的解决方案是:“组合优于继承”。它通过将“形状”和“纹理”分别建模为独立的类层次结构,并在“形状”中组合一个“纹理”的引用,使两者的组合关系从原来的 `M * N` 转变为 `M + N`。这样可以自由扩展任意一侧,而无需影响另一侧。即使实现中可能仍需要 `M * N` 种行为(这是无法减少的),但是当一个纹理要改变时,也只需要改这个纹理类而不影响其他类。此外,这种组合关系是运行时决定的,因此可以动态地将不同的抽象与实现配对,增强了系统的灵活性。
在桥接模式中,抽象部分(也称为接口)定义的是高层的控制逻辑,而真正的工作则由被称为实现部分的组件完成。抽象层在调用时,会将具体任务委派给实现层,从而实现灵活的组合与运行时绑定。
## 组合模式Composite
在实际开发中,我们经常需要将多个对象组织成一个整体来进行处理。例如,在一个绘图应用中,一个画板可能包含多个图形元素(如圆形、矩形、线条等),我们希望能够将这些元素打包成一个更大的组件,便于统一地移动、缩放或删除。
但这就引出一个问题:组件和基本元素类型不同,无法使用统一的接口进行操作,需要额外判断和处理。
组合模式正是为了解决这个问题。它的核心思想是:将对象组合成树形结构来表示“整体-部分”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。也就是说,我们可以将一个组件(由多个元素组成)当作一个普通元素一样来处理,统一操作接口,简化客户端代码。
## 装饰器模式Decorator
有时,我们需要给动态的一个类添加一些组件。例如:对于制作一杯咖啡,可以添加牛奶,糖等。一种做法是使用继承,这显然会造成类爆炸。另一种做法是将这些配料抽象成一个类,咖啡类维护一个配料的列表。对于纯数据,如计算最终价格,这样是合理的。但是对于添加额外行为,这就会将逻辑集中到 Coffee 类中。例如希望添加牛奶时咖啡必须加热,就不得不修改 Coffee 类,在它的 make 函数中添加处理逻辑。
装饰器模式解决了这个问题。让所有的配料都继承自 Coffee构建时则额外需要一个被装饰对象的指针构造函数中还能插入额外的处理。这样可以让每一层都能主动拦截、加工、改写行为而不需要集中到一个地方。对于客户端代码来说装饰器和原对象是一样的。当需要对咖啡做某个动作时可以对装饰器做装饰器再调用被修饰对象指针的对应动作。由于包含关系装饰器还天然记录了顺序。
## 外观模式Facade
在一个复杂系统中,往往由多个子系统协同工作来完成一项功能。例如,一个编译器可能包含词法分析器、语法分析器、语义分析器、优化器和代码生成器等多个模块。如果每次都要让调用者直接与这些子系统交互,不仅复杂、容易出错,而且对用户也不友好。而实际上,许多操作是常用的、固定的组合,例如“编译整个程序”就总是涉及所有子模块的调用顺序。
很容易想到通过一个统一的入口,也就是新增一个“外观类”或“外观函数”,封装这些子系统的内部调用逻辑。这就是外观模式,对外提供一个简化接口,同时又保留了对细节的操控能力。
## 享元模式Flyweight
在创建同一类的大量对象实例时,往往会面临内存占用过高的问题。这种情况的一个常见原因是:对象实例中包含了大量冗余的、可以共享的数据。例如,在游戏开发中,多个实体可能共用相同的模型和贴图资源。如果每个实例都独立持有这些资源,就会造成不必要的内存浪费。
为了解决这一问题,享元模式提供了一种有效的解决方案。它的核心思想是将对象的数据划分为可共享的和不可共享的。在实现过程中,享元模式通常通过一个享元工厂来统一管理和缓存内部状态。当需要创建新对象时,工厂会首先检查是否已有对应的内部状态资源,若有则复用,从而避免重复实例化共享部分,显著降低内存开销。
## 代理模式Proxy
我们在使用一个类时,有时会觉得这个类的功能较简单,实际使用需要额外添加一些访问控制,缓存管理等功能。简单的想法是直接在这个类中添加这些功能,但这会使得这个类急剧膨胀,且核心功能变得不那么明确。
代理模式解决了这个问题。通过一个代理类实现目标类相同接口,客户端使用代理时就像在直接操作目标类。代理类内部有目标类的指针,在处理缓存、权限等逻辑后再去调用目标类的相关函数,从而做到外围与核心逻辑的分离。
## 责任链模式Chain of Responsibility
有时我们会遇到如何处理用户请求的框架。一种做法是通过条件判断,得到不同条件下请求对应的处理函数。这样的话耦合严重,各个不同的 handler 需要放在一个函数或类中,每次扩展都需要改变这个类。
责任链模式解决了这个问题。一个 handler 要么处理这个请求,要么交给下一个 handler 处理,增加处理逻辑时,只需要新增一个 handler并且正确设置相应的 next 即可。这样,每个 handler 只关心自己的职责,所有 handler 处理清晰。当我们能将所有的 handler 组成一个链式或者树式的结构时,这会非常有用。
但也正如前面提到的,这不自然支持“一个请求需多个处理者同时处理”的场景。此时可以通过责任链进一步推广成“责任网”,观察者模式,流程引擎式结构等,增强责任链模式的处理能力。
## 命令模式Command
一个系统通常分为前端和后端两部分,前端负责接收用户输入,后端处理核心业务逻辑。那么,前后端之间如何高效、清晰地交互呢?如果每个后端模块都暴露自己的接口,系统一旦扩展,接口之间容易混乱、难以维护。
命令模式通过引入统一的“命令对象”,将请求的发送者(前端)与执行者(后端)解耦。前端只需构造命令并发送,不关心命令的执行细节;而后端根据命令对象来执行相应逻辑。
更重要的是,所有操作都被抽象为统一的命令接口,这使得命令可以统一记录、排队、撤销、重做。多个前端只需绑定相同的命令对象,即可复用相同的后端逻辑,增强了代码复用性与系统扩展性。
## 迭代器模式Iterator
集合是一种常用的数据结构,用于维护一组对象。一个集合显然需要一种方式遍历其中的元素。如果针对每一种集合设计一种遍历的方式或者函数,使用者的代码与集合强相关,且使用者可能需要了解集合的实现细节,这显然很麻烦。
将集合的遍历行为抽象为通用的迭代器对象可以解决这些问题。集合的设计者只需要提供用于遍历操作的配套对象,使用者通过统一的接口得到下一个元素,直至结束,从而解耦了集合的实现与使用者的遍历逻辑。
在 C++ 中,标准库大量使用了这类思想,比如 vectormap 等集合,以及 sort 等算法库。