实际应用中的设计模式
您需要了解的最常见设计模式及其在C#中的示例。
下载本书的PDF版本。
什么是设计模式?
软件工程中设计模式的概念在20世纪90年代初由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides合著的著名书籍**《设计模式:可复用面向对象软件的基础》**而广为人知,这四位作者被统称为"四人帮"(GoF)。然而,设计模式的根源可以追溯得更远,其灵感来源于建筑学领域。
建筑师Christopher Alexander在1977年的著作**《模式语言》**中首次提出了建筑中的模式概念,他描述了解决城市规划和建筑设计中常见问题的方案。Alexander的工作强调每个模式都解决了一个特定问题,并且是更大设计系统的一部分。这种方法引起了面临类似软件构建挑战的软件开发人员的共鸣。
四人帮看到了Alexander概念在软件开发中的潜力,于是将这些想法调整并扩展到面向对象编程中。他们的书介绍了23种设计模式,分为创建型、结构型和行为型模式,为解决常见软件设计问题提供了标准化方法。
在软件开发中,设计模式起着类似的作用——它们为反复出现的问题提供模板化解决方案,确保你在遇到熟悉的问题时不必重新发明轮子。
软件设计模式 - 是对实践中观察到的常见问题的通用解决方案。
点个星!:star:
如果你喜欢或正在使用这个项目来学习或开始你的解决方案,请给它一个星星。谢谢!
设计模式类型
设计模式可以分为三大类:
-
创建型模式 - 对象创建
-
单例模式:确保一个类只有一个实例,并提供一个全局访问点。这对管理数据库连接等资源特别有用。
-
工厂方法模式:定义一个用于创建对象的接口,但让子类决定实例化哪个类。当你有一个超类和多个子类,并且需要根据某些初始化参数创建其中一个子类的实例时,这种模式非常理想。
-
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。当你必须确保创建的对象可以协同工作,而不需要知道它们的确切类型时,这种模式非常有用。
-
建造者模式:将复杂对象的构建与其表示分离,允许同样的构建过程创建不同的表示。当你需要创建一个具有许多可选或必需组件的对象时,这种模式非常出色。
-
原型模式:通过复制现有对象(称为原型)来创建新对象。当创建对象的成本比复制现有对象更高时,这种模式特别有用。
-
-
结构型模式 - 对象组装
-
适配器模式:允许不兼容的接口一起工作。它充当两个不兼容接口之间的桥梁,使它们能够在不改变现有代码的情况下进行通信。当集成新功能或库而不破坏现有代码时,这种模式非常完美。
-
组合模式:使你能够统一对待单个对象和对象组合。它非常适合表示部分-整体层次结构,你希望忽略单个对象和对象组合之间的差异。
-
代理模式:为另一个对象提供一个替身或占位符以控制对它的访问。这对于延迟加载、控制访问或日志记录很有用,作为客户端和实际对象之间的中介来添加处理层。
-
享元模式:通过尽可能多地与相似对象共享数据来最小化内存使用;当处理许多具有一些共享状态的对象时,这对效率有很大帮助。
-
外观模式:为复杂的类系统、库或框架提供一个简化的接口。提供一个更高级的接口使子系统更容易使用,减少复杂性并促进解耦。
-
桥接模式:将抽象部分与其实现部分分离,使它们可以独立变化。当需要在几个正交(独立)维度上扩展一个类时,这种模式特别有用。
-
装饰器模式:允许动态地给一个对象添加一些新的行为,而不影响同一类其他对象的行为。这种模式为扩展功能提供了一种灵活的替代子类化方法。
-
-
行为型模式 - 对象交互
-
策略模式:允许你定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法可以独立于使用它的客户而变化。当你有多种方法来完成一个任务,并希望在运行时选择方法时,这种模式非常完美。
-
观察者模式:定义对象之间的一种依赖关系,当一个对象改变状态时,所有依赖于它的对象都会得到通知并自动更新。它非常适合实现分布式事件处理系统,其中一个对象状态的改变需要反映在另一个对象中。
-
命令模式:将一个请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化。这种转换允许你用不同的请求参数化方法,延迟或队列执行请求,并支持可撤销操作。
-
迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。这种模式对于集合对象来说很有用,可以提供一种标准的方式来遍历它们,并可能访问一些元素而不暴露内部结构。
-
状态模式:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。当一个对象的行为取决于它的状态,并且必须能够在运行时根据该状态改变其行为时,这种模式非常有益。
-
备忘录模式:在不违反封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这种模式对于实现撤销机制或保存和恢复对象状态很有用。
-
中介者模式:通过封装一组对象如何交互来减少交互类之间的混乱。这样做有助于防止"意大利面条式代码"的情况,即多个类以复杂的方式直接通信。
-
责任链模式:为了避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求。将这些对象连接成一条链,并沿着这条链传递请求直到有对象处理它。这对于以分散方式处理多个请求特别有用。
-
访问者模式:让你能够在不改变元素类的前提下定义作用于这些元素的新操作。这种模式非常适合需要对具有不同类的一组对象执行操作的场景。
-
解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。这对于开发新的编程或脚本语言的工具和编译器很有用。
-
模板方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。当一个多步骤过程需要灵活性但同时保持整体结构时,这种模式非常有益。
-
这些设计模式并不是我们拥有的唯一类型的设计模式。要了解更多其他类型的设计模式,请查看这里。
不要陷入设计模式陷阱
你应该警惕,当你第一次学习设计模式时,你可能会陷入设计模式陷阱。这意味着你会试图在每个解决方案中硬塞一个模式,很快你的代码库就会变得过度工程化且难以使用。
但我们希望尽可能使我们的代码库简单,所以设计模式并不是解决所有问题的灵丹妙药。我们不应该试图将它们应用于我们遇到的每个问题,因为它们是问题的解决方案,而不是应该到处使用的工具。如果你可以不使用设计模式就实现一个简单的解决方案,那就这样做!
图片来源:flavio
在最新的O'Reilly 2024年技术趋势中,我们发现了更令人担忧的问题。管理者在询问开发人员使用了多少种模式。这为过度工程化打开了大门。
但这还不是全部,我们还看到了按设计模式文件夹名称组织代码的代码库示例。这简直错得不能再错了!
然而,理解设计模式可以防止你重新发明轮子。
在本文的剩余部分,我们将介绍所有重要的设计模式。在多年使用这些模式的过程中,我注意到有些经常使用,有些很少使用,有些根本不使用。在这里,我将只介绍那些你日常需要的模式。
每个模式都有一个**C#语言实现**。该解决方案可以使用.NET 8
运行。
创建型设计模式
单例模式
用途:当需要一个类的单一实例时使用。一些例子包括日志记录和数据库连接。
现实世界的例子:只有一位CEO领导公司,做出决策并代表整个组织,就像单例提供全局访问和控制一样。
关于用途的备注:单例模式被认为是一种反模式;因此,建议谨慎使用。为什么?使用它时,我们倾向于用全局变量编写过程式代码。
单例模式的UML图:
关于单例模式的一个重要注意事项是,它在多线程环境中存在显著问题,特别是在多个线程可能同时尝试创建单例类实例的情况下。如果没有适当的同步机制,这可能导致创建多个实例,从而违反单例模式只应存在一个类实例的核心原则。解决这个问题有多种方法,如急切初始化、双重检查锁定或延迟初始化。
工厂方法模式
用途:将对象创建与使用分离。例如,根据配置创建不同类型的数据库连接。
现实世界的例子:想象一家披萨店有一个"披萨工厂"而不是厨师。顾客点"奶酪"或"意大利香肠"披萨,不需要知道制作过程。根据订单,这个工厂告诉专门的"奶酪披萨"或"意大利香肠披萨"生成器开始制作。每个生成器添加特有的配料,将创建逻辑分开但保持订购过程顺畅。
关于用途的备注:它可能导致类的数量增加,潜在地使代码库更加复杂。
工厂方法模式的UML图:
建造者模式
用途:逐步构建复杂对象。例如,需要创建复杂的领域对象时。
现实世界的例子:如果我们雇佣一位建筑师设计我们的梦想家园,我们不需要知道每个建筑细节。我们只需告诉建筑师我们的偏好(房间数量、风格、材料),他们就会根据这些规格创建蓝图。建筑师充当"建造者模式",通过明确的步骤(地基、墙壁、屋顶)引导我们完成创建过程,确保正确的顺序,并处理复杂的细节。你做出选择(要不要壁炉?),建造者将其纳入,一步步构建房子,直到完工。
关于用途的备注:由于引入多个新类,可能导致复杂性增加。
建造者模式的UML图:
这个组中其他有趣的设计模式包括:
结构型设计模式
适配器模式
用途:使不兼容的接口兼容。例如,将新的日志库集成到期望不同接口的现有系统中。
现实世界的例子:允许你在不同国家使用设备,通过适配当地的电源插座(适配器调解不兼容系统间的通信)。
关于用途的备注:可能导致适配器数量增加,使系统变得更复杂。有时,修改服务类以与代码库的其余部分保持一致可能更简单。
适配器模式的UML图:
组合模式
用途:表示部分-整体层次结构。例如,绘图应用中的图形对象可以分组并统一处理。
现实世界的例子:在图书馆中,书籍被组织在书架上,但每个书架还可以包含类别(小说、历史)。这些类别甚至可能包含子类别(爱情、悬疑)。每个书架作为一个组合,同时包含单本书(叶节点)和其他类别(组合节点)。
关于用途的备注:限制层次结构中某些组件或叶子的操作可能具有挑战性。
组合模式的UML图:
代理模式
用途:控制对对象的访问。例如,在网络应用中延迟加载高分辨率图像。
现实世界的例子:假设你是一位CEO,有一位个人助理充当"代理",处理请求并保护你免受不必要的干扰。助理评估每个请求,优先处理重要的事项,过滤垃圾信息,并准备相关信息。只有经过筛选的重要事项才会传达给CEO,让CEO专注于重大决策,而助理处理其余事务。
关于用途的备注:过度使用代理可能会增加不必要的复杂性并影响性能。
代理模式的UML图:
装饰器模式
用途:动态添加/移除行为。例如,在文件流之上实现压缩或加密。
现实世界的例子:如果我们想制作咖啡,我们会从普通咖啡(核心对象)开始。然后,用奶油(增加浓郁度)、糖(增加甜度)和肉桂(额外风味)"装饰"它,每一种都是一个"装饰器",增强基础咖啡而不改变它。你甚至可以组合它们(多个装饰器)创造独特的产品,比如奶油甜肉桂拿铁!
关于用途的备注:过度使用装饰器可能导致复杂的对象层次结构。
装饰器模式的UML图:
外观模式
用途:为复杂的子系统提供简化的接口。
现实世界的例子:假设我们在访问一家酒店。我们需要各种服务 - 点客房服务、预约水疗、请求客房清洁。不必单独联系每个部门,你只需打电话给前台。前台充当外观,隐藏了底层系统的复杂性。他们接受你的请求,与相关部门(厨房、水疗中心、客房清洁)沟通,并提供服务,使一切看似毫不费力。你不需要知道每个部门如何运作;外观提供了一个简化的接口来访问所有这些服务。
关于用途的备注:外观有可能演变成与应用程序每个类都相连的上帝对象。
这个组中另一个有趣的设计模式是桥接模式。它用于将抽象与实现分离。例如,将特定平台的代码与核心逻辑分开。查看C#中的实现。
行为型设计模式
策略模式
用途:定义一系列算法。例如,允许用户选择不同的排序或压缩算法。
现实世界的例子:让我们规划从A城市到B城市的旅行。你可以根据需求选择"交通策略":乘坐快速列车(注重速度),舒适的巴士(注重舒适),或经济实惠的拼车(注重低成本)。
关于用途的备注:在处理多种策略时,这可能导致类的数量增加和复杂性提高。如果只有几种很少变化的算法,就没有必要添加新的类和接口。
策略模式的UML图:
观察者模式
用法:通过接收变更通知并相应地通知订阅者来维持一致状态,例如在消息系统中通知事件订阅者。
现实世界的例子:我们可以想象一个突发新闻应用。用户订阅特定主题(体育、政治等),充当"观察者"。当订阅主题有突发新闻时,"观察者模式"会向所有相关用户发送个性化提醒。体育迷获得比分,政治爱好者收到选举更新,无需主动查看。
关于用法的说明:当观察者数量众多或更新逻辑复杂时,可能会导致性能问题。订阅者通知的顺序是不可预测的。
观察者模式的 UML 图:
命令模式
用法:将请求封装为一个对象。例如,在文本或图像编辑器中实现撤销/重做功能。
现实世界的例子:想象在餐厅点餐。你告诉服务员你的要求(披萨,多加奶酪),创建一个"订单命令"。服务员作为信使,将你的"命令"传递给厨房的厨师(接收者)。厨师收到"命令"后,按照指定要求制作披萨。这种将订餐(命令)与制作(执行)分离的方式使你能轻松更改或取消。
关于用法的说明:它可能会引入复杂性,因为需要为每个动作或请求创建额外的类,使简单操作的架构变得复杂。
命令模式的 UML 图:
状态模式
用法:封装特定状态的行为。例如,处理用户界面元素的不同状态(启用、禁用、选中等)。
现实世界的例子:智能手机根据你的操作轻松切换状态(开机、关机、静音、飞行模式)。每种状态("具体状态")都有独特的行为:开机允许通话和通知,静音模式关闭声音,飞行模式屏蔽信号。手机("上下文")不直接管理这些行为;它将任务委托给当前状态对象。当你按下按钮或切换设置时,手机过渡到新状态,无需复杂逻辑就能无缝改变其行为。
关于用法的说明:它可能导致类的激增,因为每个状态通常由一个单独的类表示。这不仅增加了系统的复杂性,还可能使其更难管理和理解。
状态模式的 UML 图:
模板方法模式
用法:在操作中定义算法的骨架,将某些步骤推迟到子类中实现,并实现一个基类用于单元测试,具有可定制的设置和拆卸步骤。
现实世界的例子:假设我们有一个工厂,每种汽车(轿车、SUV和卡车等子类)都遵循相同的基本步骤:焊接车架、添加发动机、安装电气组件和喷漆。这是由模板方法定义的整体结构。然而,每种汽车类型都有特定的变化:轿车有较小的车架和发动机,SUV有较高的离地间隙和不同的内饰,卡车有加固的车架和更大的发动机。
关于用法的说明:当需要多个算法变体时,可能会导致过于复杂的层次结构。
模板方法模式的 UML 图:
如何选择正确的设计模式
在软件工程中选择正确的设计模式对于有效解决问题至关重要。本指南简化了这个过程,帮助你根据特定需求在模式之间做出决策。它为每种模式提供简洁的描述和有价值的用例,使理解和在实际场景中应用它们变得更加容易。
要选择一种模式,我们必须首先进行问题识别,如下图所示。
下载 PDF 版本。
以下是模式选择的总结:
附赠:设计模式速查表
下载 PDF 版本。
深入学习的资源
如果你想深入了解设计模式的世界,可以查看以下资源:
-
深入浅出设计模式 书籍。
-
设计模式:可复用面向对象软件的基础 GoF 的书。
-
优秀的软件和架构设计模式,精选的软件和架构相关设计模式列表。
-
设计模式库 Pluralsight 课程。
-
Refactoring.Guru 网站。
-
Source Making 网站。
总结
如果你认为该仓库可以改进,请提交任何更新的 PR 并提交问题。此外,我将继续改进它,所以你也应该给这个仓库加星标。
贡献
- 提交改进的拉取请求
- 在 issues 中讨论想法
- 传播这个项目
许可证
作者
Milan Milanović 博士 - 3MD 的 CTO 和微软开发者技术 MVP。