设计模式之(1)基础知识
Author:Onceday Date:2023年12月5日
漫漫长路,才刚刚开始...
全系列文章请查看专栏: 设计模式_Once_day的博客-CSDN博客。
参考文档:
- 设计模式: 可复用面向对象软件的基础, (美) 埃里克·伽马(Erich Gamma)等著,李英军等译,(北京)机械工业出版社,2019.3。
文章目录
-
-
- 设计模式之(1)基础知识
-
- [1. 概述](#1. 概述)
-
- [1.1 介绍](#1.1 介绍)
- [1.2 基本组成](#1.2 基本组成)
- [1.3 设计模式分类](#1.3 设计模式分类)
- [1.4 常见设计模式](#1.4 常见设计模式)
- [1.5 如何选择设计模式](#1.5 如何选择设计模式)
- [1.6 如何使用设计模式](#1.6 如何使用设计模式)
- 附录:
-
- [附录1. Smalltalk](#附录1. Smalltalk)
- [附录2. 面对对象编程系统](#附录2. 面对对象编程系统)
-
1. 概述
1.1 介绍
设计模式的概念最早源自于建筑学家克里斯托弗·亚历山大在1970年代的研究。他尝试找出建筑设计中的一些常见问题和解决方案,然后将这些解决方案记录为"模式"。这个概念后来被软件工程界接纳,尤其是在Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides(被称为"四人帮")的《设计模式:可复用面向对象软件的基础》一书中得到了深入的探讨。
设计模式是为了解决在软件设计中经常遇到的一些特定问题提供的一种模板性的解决方案。它们可以帮助设计者快速有效地解决这些问题,提高代码的可读性、可复用性和可维护性。设计模式可以被视为一种通用语言,让软件开发者能够有效地交流有关设计的问题和解决方案。
在实际应用中,设计模式的实践过程通常包括以下步骤:
- 识别问题:在软件设计中,识别出可能需要使用设计模式的问题或挑战。
- 选择合适的设计模式:基于问题的具体情况,选择一个或多个可能的设计模式。
- 应用设计模式:根据所选设计模式的定义和结构,重新设计或重构你的代码。
- 评估结果:评估应用设计模式后的结果,包括代码的可读性、可复用性、可维护性等。
- 反馈和改进:如果结果不理想,可能需要选择和应用不同的设计模式,然后再次评估结果。
设计模式是一个强大的工具,但也需要谨慎使用。不是所有的问题都需要使用设计模式来解决,而且过度使用设计模式可能会导致代码过于复杂。
1.2 基本组成
设计模式通常由以下四个基本元素组成:
-
模式名称(Pattern Name):每个设计模式都有一个唯一的名字,通常以其用途或行为来命名,如"单例"、"观察者"或"工厂方法"。
-
问题(Problem):问题部分描述了在何种场景下应该使用该模式。它可能包括设计中的特定问题和需求。
-
解决方案(Solution):解决方案部分描述了设计的组成部分,它们的关系以及各自的职责和协作方式。解决方案并不是具体的代码,而是以设计问题的形式,可以在许多不同的情况下实施。
-
效果(Consequences):描述了应用模式的效果和权衡之处。它们是对设计可重用性、灵活性等方面影响的系统性评价。
描述一个设计模式通常会涉及到以下方面:
-
模式名(Pattern Name):模式的名称应当简洁,并且能清楚地反映出模式的主要功能或概念。
-
分类(Classification):设计模式通常被分为创建型、结构型和行为型三种类型。创建型模式关注对象的创建机制,结构型模式关注类和对象的组合,而行为型模式关注对象之间的通信。
-
意图(Intent):这部分简单清晰地描述了模式的主要目的和主要作用。
-
动机(Motivation):动机部分提供一个实例,解释了模式如何在实际情况中解决问题。
-
适用性(Applicability):这部分描述了在何种情况下应该使用模式。
-
结构(Structure):这部分通常包括类图和交互图,描绘了模式的基本结构和交互。
-
参与者(Participants):这部分描述了模式中的各个角色(通常是类或者对象),以及他们在模式中的责任和行为。
-
协作(Collaborations):描述了参与者如何协作以完成他们的职责和实现模式的目的。这部分解释了参与者之间的交互和协作方式。
-
效果(Consequences):描述了模式的优点和缺点,以及使用模式可能会带来的影响。
-
实现(Implementation):这部分提供了一些关于如何在特定语言中实现模式的建议和注意事项。
1.3 设计模式分类
设计模式可以根据两个维度进行分类:目的(Purpose)和范围(Scope)。这两个维度提供了一种有用的方式来理解、学习和比较不同的设计模式。
**目的(Purpose)**划分主要用于描述模式主要用于完成什么任务,它分为三个子类别:
- 创建型模式:这类模式处理对象创建机制,尽量使得创建的对象满足特定的情况,而不仅仅是明确的类实例化。创建型模式包括工厂方法模式、抽象工厂模式、单例模式、建造者模式和原型模式等。
- 结构型模式:这类模式主要关注类和对象的组合,以获得更大的结构。他们解决了类和对象的组合或组装的问题,使得这些结构满足特定的复杂要求。包括了适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式和享元模式等。
- 行为型模式:这类模式专注于算法和对象间的通讯。他们为对象间的通信提供了更加灵活的方式。包括了策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、访问者模式、中介者模式、解释器模式和状态模式等。
**范围(Scope)**则指设计模式是主要用于类的哪一方面,它分为两个子类别:
- 类模式:这些模式主要用于处理类和子类之间的关系,基本上是基于继承的。他们用继承关系来组成接口或者实现。例如工厂模式、模板方法模式等。
- 对象模式:这些模式主要处理对象之间的关系,基本上是基于对象的实例来处理。他们用对象实例来组成接口或者实现。例如单例模式、原型模式、代理模式、装饰器模式等。
这两种维度组合之后,可以如下理解:
- 创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。
- 结构型类模式使用继承机制来组合类,而结构型对象模式则描述了对象的组装方式。
- 行为型类模式使用继承描述算法和控制流,而行为型对象模式则描述了一组对象怎么协作完成单个对象所无法完成的任务。
1.4 常见设计模式
以下是一些常见的设计模式,按照创建型、结构型和行为型三类来进行分类:
(1) 创建型设计模式:
-
工厂模式(Factory Pattern): 提供一个创建对象的接口,让其子类决定实例化哪一个类。工厂方法让类把实例化推迟到子类。
-
抽象工厂模式(Abstract Factory Pattern): 提供一个接口,让该接口负责创建一系列"相关或者相互依赖的对象",无需指定它们具体的类。
-
单例模式(Singleton Pattern): 确保一个类只有一个实例,并提供一个全局访问点。
-
建造者模式(Builder Pattern): 使用多个简单的对象一步一步构建成一个复杂的对象。
-
原型模式(Prototype Pattern): 通过复制现有的实例来创建新的实例。
(2) 结构型设计模式:
-
适配器模式(Adapter Pattern): 转换一个接口到另一个接口,使得原本由于接口不兼容无法一起工作的类可以一起工作。
-
装饰器模式(Decorator Pattern): 动态地给一个对象添加一些额外的职责,而不影响从这个类派生的其他对象。
-
代理模式(Proxy Pattern): 为其他对象提供一个代理,以控制对这个对象的访问。
-
外观模式(Facade Pattern): 提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用。
-
桥接模式(Bridge Pattern): 将抽象部分与实现部分分离,使它们都可以独立地变化。
-
组合模式(Composite Pattern): 允许你将对象组合成树形结构来表示"部分-整体"的层次结构。
-
享元模式(Flyweight Pattern): 通过共享技术有效地支持大量的细粒度对象。
(3) 行为型设计模式:
-
策略模式(Strategy Pattern): 定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
-
模板方法模式(Template Method Pattern): 定义一个操作中的算法的骨架,将一些步骤延迟到子类中。
-
观察者模式(Observer Pattern): 当一个对象的状态发生改变时,其相关依赖对象会被自动通知。
-
迭代器模式(Iterator Pattern): 提供一种方法访问一个容器对象中的各个元素,而又不需暴露该对象的内部细节。
-
责任链模式(Chain of Responsibility Pattern): 为请求创建了一个接收者对象的链。
-
命令模式(Command Pattern): 将一个请求封装为一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
-
备忘录模式(Memento Pattern): 在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以后就可将该对象恢复到原始状态。
-
访问者模式(Visitor Pattern): 在不改变类的前提下,定义作用于某种数据结构中的各元素的新操作。
-
中介者模式(Mediator Pattern): 用一个中介者对象封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
-
解释器模式(Interpreter Pattern): 给定一种语言,定义它的语法的表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
-
状态模式(State Pattern): 允许一个对象在其内部状态改变时改变它的行为。
1.5 如何选择设计模式
选择适合的设计模式通常依赖于几个关键因素,包括你面对的问题类型、你的应用程序或系统的特定需求以及你想要实现的扩展性和可维护性目标。以下是选择设计模式时可以遵循的几个步骤:
-
明确问题和需求:
- 分析你的软件项目或问题以确定你正在尝试解决的具体问题。
- 识别需求,如可维护性,可扩展性,松耦合等。
-
了解设计模式分类:
- 设计模式通常分为三类:创建型(Creational)、结构型(Structural)、行为型(Behavioral)。
- 选择适合的类别,这取决于你解决的是创建对象的问题、组合对象的问题还是对象之间的交互问题。
-
研究相关的设计模式:
- 在确定了可能适用的设计模式类别后,研究和理解该类别下的各种设计模式。
- 例如,如果你需要动态地创建对象并希望隐藏创建逻辑,可能会考虑使用工厂模式(Factory Pattern)或抽象工厂模式(Abstract Factory Pattern)。
-
评估案例适用性:
- 将你的具体情况与设计模式的意图和结构进行对比,确保选择的模式可以解决你的问题。
- 考虑模式是否可以在不引入不必要复杂性的情况下解决问题。
-
考虑长期维护:
- 考虑未来的需求和潜在的系统变化。
- 选择能够容纳这些变化的设计模式,以便你的系统可以在不需要重大重构的情况下发展。
-
原则指导:
- 遵循SOLID原则(单一职责,开闭原则,里氏替换,接口隔离和依赖倒置)来评估模式的合适性。
- 设计模式应该帮助你更好地遵守这些原则,而不是违背它们。
-
考虑已有的实践:
- 查看类似项目或行业中的标准做法。
- 如果一个特定的设计模式在类似的上下文中经常被使用,那么它可能是出于某些好的理由。
-
评估团队熟悉度:
- 考虑你的团队对某个设计模式的熟悉程度。
- 引入一个团队成员不熟悉的设计模式可能需要额外的时间来学习和理解。
-
实施和反馈:
- 实施所选的设计模式,并在代码审查和测试中评估其有效性。
- 如果模式不符合预期,不要害怕回溯并尝试其他方案。
1.6 如何使用设计模式
使用设计模式基本上是识别你的软件设计中出现的共同问题,并应用一种经过时间检验,得到广泛认可的标准解决方案。以下是如何在实践中使用设计模式的步骤:
-
识别问题,在你的设计中寻找可以通过已知设计模式解决的问题。这可能是关于如何创建对象、如何组织复杂的结构,或者如何协调不同对象之间的交互。
-
选择合适的设计模式,一旦你识别出问题,就可以从适合的设计模式中做出选择。选择时,需要考虑模式是否适应你的问题,以及它是否与你的项目需求和目标相符。
-
了解设计模式,在实施前,彻底理解所选模式的结构、参与者和协作方式。设计模式通常有类图和示例代码,这些都是理解模式的好资源。
-
应用设计模式,将设计模式的结构应用到你的代码中。这通常涉及创建接口和抽象类,以及定义类和方法,使其遵循模式的模板。
-
调整以适应你的具体情况,很少有设计模式可以不经修改直接应用。通常,你需要调整它们以适应你的应用程序的具体需求。
-
验证与迭代,实施设计模式后,验证它是否解决了初始问题并且没有引入任何新问题。基于反馈,可能需要进行微调。
下面以单例模式来作为示例,展示如何使用设计模式:
假设你有一个配置管理器,它在应用程序中多处被调用,但你希望在运行时始终只有一个实例。
-
识别问题:
- 需要一个全局可访问的实例,且实例只能有一个。
-
选择设计模式:
- 单例模式(Singleton Pattern),因为它确保一个类只有一个实例,并提供一个全局访问点。
-
了解单例模式:
- 研究单例模式的类图和典型实现。
-
应用设计模式:
- 实现一个私有的静态属性用于保存类的唯一实例。
- 提供一个公共的静态方法用于获取这个实例。
- 将构造函数设置为私有,以防止外部使用
new
关键字实例化。
-
调整以适应具体情况:
- 如果你的应用程序是多线程的,确保单例实现是线程安全的。
-
验证与迭代:
- 检查实现是否确实只创建了一个实例,并且该实例在应用程序中全局可用。
附录:
附录1. Smalltalk
Smalltalk 是一种高级、动态、纯面向对象的编程语言,它在 1970 年代由 Xerox PARC(帕洛阿尔托研究中心)开发。Smalltalk 对计算机科学和编程语言设计产生了深远影响,它是面向对象编程和开发环境的先驱。
以下是 Smalltalk 的一些主要特点:
-
纯面向对象:在 Smalltalk 中,几乎所有的东西都是对象,包括数字、函数和线程等。所有的操作都是通过向对象发送消息来完成的。
-
动态类型:Smalltalk 是动态类型的语言,这意味着你不需要(也不能)在编译时声明变量的类型。对象的类型在运行时决定。
-
图形开发环境:Smalltalk 提供了一个完整的图形开发环境,其中包括代码编辑器、调试器、对象浏览器和界面构建器等。这个环境是交互式的,你可以在运行程序的同时修改和调试代码。
-
垃圾收集:Smalltalk 自动管理内存,程序员不需要手动分配和释放内存。
-
反射:Smalltalk 支持高级的反射机制。你可以在运行时检查和修改对象的内部状态和行为。
-
图形用户界面:Smalltalk 提供了一套用于创建图形用户界面的工具和库。
以下是一个简单的 Smalltalk 程序,它计算并打印出前 10 个自然数的和:
smalltalk
| sum |
sum := 0.
1 to: 10 do: [:i | sum := sum + i].
Transcript show: sum.
Smalltalk 的语法非常简洁和一致,但是它与 C、Java 和 Python 等主流语言有很大的不同,可能需要一些时间来适应。尽管 Smalltalk 的使用并不广泛,但是学习 Smalltalk 可以帮助你更深入地理解面向对象编程和动态语言的概念。
附录2. 面对对象编程系统
这些都是一些重要的面向对象编程系统或语言,每个都有其独特的特性和优点。
-
CLOS (Common Lisp Object System),CLOS 是 Common Lisp 的对象系统,它为这种强大的动态编程语言提供了面向对象的特性。CLOS 的一个重要特性是多方法(multi-methods),这意味着方法不仅可以基于其接收者的类型,而且还可以基于其其他参数的类型进行分派。此外,CLOS 支持类和方法的动态修改,这为开发者提供了极大的灵活性。
-
Dylan,一种多范式编程语言,它结合了函数式编程和面向对象编程的特性。Dylan 的语法类似于 ALGOL 和 Pascal,但它的对象系统包含了许多高级特性,如多方法、类的多重继承和元编程。尽管 Dylan 的使用并不广泛,但它对编程语言设计的影响力不可忽视。
-
Self,一种基于原型的编程语言,它是 JavaScript 和 Lua 等语言的灵感来源。在 Self 中,所有的值都是对象,并且类的概念被完全消除。相反,新的对象是通过复制和修改现有的对象(称为原型)来创建的。Self 还引入了一种叫做 "just-in-time" (JIT) 的编译技术,这种技术现在被广泛用于许多编程语言的实现中。