文章目录
- 一、设计模式
-
- [1.1 设计模式的起源](#1.1 设计模式的起源)
- [1.2 设计模式的定义](#1.2 设计模式的定义)
- [1.3 记录要素](#1.3 记录要素)
- [1.4 合理使用模式](#1.4 合理使用模式)
- 二、设计模式的六大原则
-
- [2.1 开闭原则(Open-Closed Principle, OCP)](#2.1 开闭原则(Open-Closed Principle, OCP))
-
- [2.1.1 定义](#2.1.1 定义)
- [2.1.2 原则分析](#2.1.2 原则分析)
- [2.1.3 开闭原则的意义所在](#2.1.3 开闭原则的意义所在)
- [2.2 单一职责原则(Single Responsibility Principle, SRP)](#2.2 单一职责原则(Single Responsibility Principle, SRP))
-
- [2.4.1 定义](#2.4.1 定义)
- [2.4.2 原则分析](#2.4.2 原则分析)
- [2.3 里氏代换原则(Liskov Substitution Principle, LSP)](#2.3 里氏代换原则(Liskov Substitution Principle, LSP))
-
- [2.3.1 定义](#2.3.1 定义)
- [2.3.2 原则分析](#2.3.2 原则分析)
- [2.4 依赖倒转原则(Dependency Inversion Principle, DIP)](#2.4 依赖倒转原则(Dependency Inversion Principle, DIP))
-
- [2.4.1 定义](#2.4.1 定义)
- [2.4.2 原则分析](#2.4.2 原则分析)
- [2.5 合成复用原则(Composite Reuse Principle, CRP)](#2.5 合成复用原则(Composite Reuse Principle, CRP))
-
- [2.5.1 定义](#2.5.1 定义)
- [2.5.2 原则分析](#2.5.2 原则分析)
- [2.6 迪米特法则(Law of Demeter, LoD)](#2.6 迪米特法则(Law of Demeter, LoD))
-
- [2.6.1 定义](#2.6.1 定义)
- [2.6.2 原则分析](#2.6.2 原则分析)
- 三、设计模式的分类
-
- [3.1 创建型模式](#3.1 创建型模式)
- [3.2 结构型模式](#3.2 结构型模式)
- [3.3 行为型模式](#3.3 行为型模式)
- 参考资料
一、设计模式
1.1 设计模式的起源
软件领域的设计模式起源主要是受到1977年建筑大师Alexander出版的《A Pattern Language:Towns, Building, Construction》一书。Alexander在其著作中将其建筑行业中的许多问题的最佳解决方案记录为200多种模式,其思想不仅在建筑行业影响深远,而且很快影响到了软件设计领域。
1987年,Kent Beck和Ward Cunningham将Alexander在建筑学上的模式观点应用于软件设计,开发了一系列模式,并用Smalltalk语言实现了雅致的用户界面。Kent Beck和Ward Cunningham在1987年举行的一次面向对象的会议上发表了论文:《在面向对象编程中使用模式》,该论文发表后,有关软件的设计模式论文以及著作相继出版。
目前,被公认在设计模式领域最具影响力的著作是Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides在1994年合作出版的著作:《DesignPatterns: Elements of Reusable Object-OrientedSoftware》 (中译本《设计模式:可复用的面向对象软件的基础》或《设计模式》),该书提出了23种基本设计模式,被广大喜爱者昵称为GOF (Gang of Four)之书,被认为是学习设计模式的必读著作。GOF之书已经被公认为是设计模式领域的奠基之作。
时至今日,在可复用面向对象软件发展过程中,新的设计模式仍然在涌现,丰富着软件设计领域的知识和实践。
1.2 设计模式的定义
设计模式(pattern)是从许多优秀的软件系统中总结出的成功的可复用的设计方案。
软件模式:
•对通用设计问题的重复解决方案
•对真实世界问题的实践的/具体的解决方案
•面向特定的问题环境
•权衡利弊之后得到的"最佳"解决方案
•领域专家和设计老手的"杀手锏"
•用文档的方式记录的最佳实践
•在讨论问题的解决方案时,一种可交流的词汇
•在使用(重用)、共享、构造软件系统中,一种有效地使用已有的智慧/经验/专家技术的方式
1.3 记录要素
记录一个设计模式需有四个基本要素:
1.名称 一个模式的名称高度概括该模式的本质,有利于该行业统一术语、便于交流使用。
2.问题 描述应该在何时使用模式,解释设计问题和问题存在的前因后果,描述在怎样的环境下使用该模式。
3.方案 描述设计的组成部分,它们之间的相互关系及各自的职责和协作方式。
4.效果 描述模式的应用效果及使用模式应当权衡的问题。主要效果包括使用模式对系统的灵活性、扩充性和复用性的影响。
例如,GOF之书如下记录中介者模式:
名称 中介者
问题 用一个中介者来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
方案 中介者(Mediator)接口、具体中介者(ConcreteMediator)、同事(Colleague)、具体同事(ConcreteColleague)。
效果 减少了子类的生成、将各个同事解耦、简化了对象协议、控制集中化。
事实上,关于如何描述设计模式,《设计模式:可复用的面向对象软件的基础》有更多的细节,
1.4 合理使用模式
不是软件的任何部分都需要套用模式来设计的,必须针对具体问题合理的使用模式。
1. 正确使用
当你设计某个系统,并确认所遇到的问题刚好适合使用某个模式,就可以考虑使用该模式到你的系统设计中,毕竟该模式已经被公认是解决该问题的成功方案,能使设计的系统易维护、可扩展性强、复用性好,而且这些经典的模式也容易让其他开发人员了解你的系统和设计思想。
2. 避免教条
模式不是数学公式、也不是物理定律、更不是软件设计中的"法律"条文,一个模式只是成功解决某个特定问题的设计方案,你完全可以修改模式中的部分结构以符合你的设计要求。
3.模式挖掘
模式不是用理论推导出来的,而是从真实世界的软件系统中被发现、按着一定规范总结出来的可以被复用的方案。许多文献或书籍里阐述的众多模式实际上都是GOF书中经典模式的变形,这些变形模式都经过所谓的"三次规则",即该模式已经在真实世界的三个方案中被成功的采用。可以从某个系统中洞察出某种新模式,只要经过"三次规则"就会被行业认可。
4.避免乱用
不是所有的设计中都需要使用模式,因为模式不是发明出来的,而是总结出来的,事实上,真实世界中的许多设计实例都没有使用过GOF之书中的经典模式。在进行设计时,尽可能用最简单的方式满足系统的要求,而不是费尽心机地琢磨如何在这个问题中使用模式,一个设计中,可能并不需要使用模式就可以很好地满足系统的要求,如果牵强地使用某个模式可能会在系统中增加许多额外的类和对象,影响系统的性能,因为大部分设计模式往往会在系统中加入更多的层,这不但增加复杂性,而且系统的效率也会下降。
5.了解反模式
所谓反模式就是从某些软件系统中总结出的不好的设计方案,反模式就是告诉你如何采用一个不好的方案解决一个问题。既然是一个不好的方案,为何还有可能被重复使用呢?这是因为,这些不好的方案表面上往往有很强的吸引力,人们很难一眼就发现它的弊端,因此,发现一个反模式也是非常有意义的工作。在有了一定的设计模式的基础之后,你可以用搜索引擎查找有关反模式的信息,这对于学习好设计模式也是非常有帮助的。
二、设计模式的六大原则
2.1 开闭原则(Open-Closed Principle, OCP)
2.1.1 定义
开闭原则定义: 一个软件实体应当对扩展开放,对修改关闭。即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即实现在不修改源代码的情况下改变这个模块的行为。
开闭原则由Bertrand Meyer于1988年提出,它是面向对象设计中最重要的原则之一。在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。
2.1.2 原则分析
事实上完全封闭的系统是不存在的 。无论模块怎样实现封闭,到最后,总还是有一些无法封闭的变化。而对应的思路就是: 既然不能做到绝对封闭,那我们就应该选择对哪些变化进行封闭,哪些变化进行隔离。然后将那些无法封闭的变化抽象出来,进行隔离,并且允许扩展,尽可能地减少系统的开发。当系统变化出现时要能够及时地作出反应。
开放-封闭原则提供了一个使系统在面对需求变更时,可以保持系统相对稳定的解决方案。其思想简单来说就是面对需求的变化,通过添加新的类或者模块等就可以应对,而无需对原有的代码进行修改。
2.1.3 开闭原则的意义所在
只依赖于抽象,实现开放-封闭原则的核心思想就是 面向抽象编程,而不是面向具体编程。
因为抽象相对来说是稳定的。让类去依赖于固定的抽象,所以对于修改来说就是封闭的;而通过面向对象的继承以及多态机制,可以去实现对抽象体的继承,通过重写其方法来改变固有行为,从而实现新的扩展方法,所以对于来说扩展就是开放的。这是实施开放-封闭原则的基本思路。
同时这种机制也是建立在两个基本的设计原则基础上,这就是里氏代换原则和合成复用原则。可以简单地理解为,开闭原则是面向对象设计的目标,里氏代换原则和依赖倒转原则就是面向对象设计的主要手段。
2.2 单一职责原则(Single Responsibility Principle, SRP)
2.4.1 定义
单一职责原则主要有以下两种定义:
- 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
- 就一个类而言,应该仅有一个引起它变化的原因。
2.4.2 原则分析
一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
2.3 里氏代换原则(Liskov Substitution Principle, LSP)
2.3.1 定义
里氏代换原则主要有以下两种定义:
- 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型S是类型T的子类型。
- 所有引用基类(父类)的地方必须能透明地使用其子类的对象
2.3.2 原则分析
里氏代换原则可以通俗表述为: 在软件中如果能够使用基类对象,那么一定能够使用其子类对象。把基类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
2.4 依赖倒转原则(Dependency Inversion Principle, DIP)
2.4.1 定义
依赖倒转原则定义:
- 高层模块不应该依赖低层模块,但两者都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
可以简单地理解为,要针对接口编程,不要针对实现编程。
2.4.2 原则分析
面向对象设计最重要的原则就是创建抽象化 ,同时从抽象化导出具体化,具体化给出不同的实现。继承的关系就是一种从抽象化到具体化的实现。抽象层所包含的应该是应用系统的商务逻辑和宏观的、对整个系统来说具有重要的战略性的决定 ,也是必然性的体现。具体的层次含有的是一些次要的与实现有关的算法以及逻辑 ,还有战术性的决定,这些都带有相当大的偶然性选择。具体层次的代码都是经常变动的,也就无法避免出现错误。
从复用的角度来说,高层次的模块是应当被复用的,而且会是被复用的重点,因为它含有一个应用系统最重要的宏观逻辑,并且较为稳定。而在传统的过程性设计当中,复用则侧重于具体层次模块的复用。依赖倒转原则正是对传统的过程性设计方法的"倒转",也是高层次模块复用及其可维护性的最有效的规范。
2.5 合成复用原则(Composite Reuse Principle, CRP)
2.5.1 定义
合成复用原则定义:尽量使用对象组合,而不是继承来达到复用的目的 。
合成复用原则又称为组合/聚合复用原则(Composition/ Aggregate Reuse Principle, CARP)
合成与聚合都是特殊的关联种类。聚合表示比较"弱"的拥有关系,具体表现是甲对象中可以包括乙对象,但乙对象不是甲对象的一部分;合成则是一种比较"强"的拥有关系,体现的是严格的整体与部分之间的关系,并且整体与部分有相同的生命周期。比如鱼和鱼群是聚合关系,手臂与人体之间则是部分与整体的关系。
优先使用对象的合成/聚合会有助于保持系统的每个类都会被封装,并且类被集中在单个任务上。这样类和类之间的继承层次就可以保持比较小的规模,并且不太可能增长为不可控制的巨大单位。
2.5.2 原则分析
如果语义上存在着明确的"Is-A"关系,并且这种关系是稳定的且不变的,则我们考虑使用继承; 如果没有"Is-A"的关系,或者这种关系是可变的时,我们使用合成。
当两个类都能符合里氏代换原则时,这两个类才可以是"Is-A"的关系。也就是说,如果两个类之间的关系是"Has-A",但是这两个类被设计为继承,则这两个类肯定会违背里氏代换原则。
2.6 迪米特法则(Law of Demeter, LoD)
2.6.1 定义
迪米特法则(Law of Demeter, LoD)常见定义如下:
- 不要和"陌生人"说话
- 只与你的直接朋友通信
- 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位
- 如果两个类之间不必直接通信,则这两个类不应该发生直接的相互作用。如果其中的一个类需要调用另一个类的某个方法,可以通过第三方来转发这个调用。
迪米特法则的根本思想,是强调类之间需要尽量多实现松散耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及,反之则会导致很多麻烦。 信息的隐藏会促进软件的复用。
2.6.2 原则分析
迪米特法则强调的前提是在类的结构设计上,每一个类都要尽量降低成员的访问权限,类自己包装好自己的private状态,不需要让别的类知道的字段或者行为就不要公开。
三、设计模式的分类
根据目的准则,模式依据其目的可分为创建型(Creational)、结构型(Structural)、或行为型(Behavioral)三种。创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象怎样交互和怎样分配职责进行描述。
3.1 创建型模式
创建型模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
创建型模式主要包括:
- 工厂方法模式(Factory Method Pattern): 工厂方法模式定义一个用于创建对象的接口,但将实际的对象创建延迟到子类中。这样,不同的子类可以创建具体不同类型的对象,而客户端代码仅与抽象工厂接口进行交互,实现了解耦。它适用于需要根据不同条件创建不同类型对象的情况。
- 抽象工厂模式(Abstract Factory Pattern): 抽象工厂模式提供一个接口,用于创建一系列相关或相互依赖的对象家族,而不需要指定具体的类。客户端通过使用抽象工厂接口,可以创建和使用这些对象,而无需关心具体的实现类。它适用于需要创建一组相关对象的场景。
- 单例模式(Singleton Pattern): 单例模式确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这在需要全局唯一实例的场景下很有用,比如配置信息、日志记录器等。
- 建造者模式(Builder Pattern): 建造者模式将一个复杂对象的构建过程和表示分离,使得同样的构建过程可以创建不同的表示。通过使用指挥者来统一组织构建过程,客户端可以创建出不同配置的对象。
- 原型模式(Prototype Pattern): 原型模式通过复制现有的对象来创建新的对象实例,而不是通过实例化类来创建。它允许通过克隆的方式来创建新的对象,减少了对象创建的开销。
3.2 结构型模式
结构型模式涉及到如何组合类和对象以获得更大的结构,以解决对象之间的组合和接口问题 。结构型类模式 采用继承机制来组合接口或实现。一个简单的例子是采用多重继承方法将两个以上的类组合成一个类,结果这个类包含了所有父类的性质。这一模式尤其有助于多个独立开发的类库协同工作。结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从而实现新功能的一些方法。因为可以在运行时刻改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
结构性模式主要包括:
- 适配器模式(Adapter Pattern): 适配器模式允许将一个类的接口转换为另一个类的接口,以便两者可以一起工作。它在不改变原有类接口的情况下,使得原本不兼容的类可以一起协作。
- 装饰器模式(Decorator Pattern): 装饰器模式允许动态地给对象添加额外的功能,而不影响其结构。通过将对象放入装饰器中,可以以透明的方式扩展对象的功能。
- 代理模式(Proxy Pattern): 代理模式用于控制对另一个对象的访问。它在客户端和目标对象之间引入了一个代理对象,可以用于增加额外的控制或管理目标对象的访问。
- 外观模式(Facade Pattern): 外观模式提供了一个简化的接口,用于访问复杂系统的一组接口。通过外观模式,客户端可以更方便地使用系统,而不需要了解底层复杂的实现。
- 桥接模式(Bridge Pattern): 桥接模式将抽象部分与实现部分分离,使得它们可以独立地变化。通过桥接模式,可以避免在类之间使用多重继承,提高了系统的灵活性。
- 组合模式(Composite Pattern): 组合模式用于将对象组合成树状结构,以表示部分-整体的层次关系。通过组合模式,可以使得客户端统一地处理单个对象和组合对象。
- 享元模式(Flyweight Pattern): 享元模式用于减少对象的创建,通过共享相同的对象来降低内存的消耗。它适用于需要创建大量相似对象的场景。
3.3 行为型模式
行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻划了在运行时难以跟踪的复杂的控制流。它们将你的注意力从控制流转移到对象间的联系方式上来。
行为型模式主要包括:
- 观察者模式(Observer Pattern): 观察者模式定义了对象之间的一对多依赖关系,当一个对象的状态发生变化时,其所有依赖者都会收到通知并自动更新。这种模式使得对象之间的关联变得松散,增加了系统的可维护性和扩展性。
- 策略模式(Strategy Pattern): 策略模式定义了一系列的算法,并使它们可以互相替换,使得算法可以独立于客户端而变化。客户端可以根据需要选择使用不同的策略,从而实现灵活的算法组织和切换。
- 模板方法模式(Template Method Pattern): 模板方法模式定义了一个算法的骨架,将一些步骤的实现延迟到子类中。这样,不同的子类可以实现算法的具体细节,而不改变算法的整体结构。
- 命令模式(Command Pattern): 命令模式将请求封装为一个对象,从而使得客户端可以参数化不同的请求,并支持对请求进行排队、记录和撤销。这样,可以实现请求的发起者和接收者之间的解耦。
- 责任链模式(Chain of Responsibility Pattern): 责任链模式通过将多个对象组成一条责任链,依次处理请求,直到请求被处理。这样,可以动态地改变请求的处理顺序,实现请求发送者与接收者之间的解耦。
- 迭代器模式(Iterator Pattern): 迭代器模式提供一种顺序访问聚合对象的方法,而无需暴露其内部表示。这样,可以在不影响聚合对象的情况下遍历聚合对象的元素。
- 备忘录模式(Memento Pattern): 备忘录模式用于在不破坏封装性的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态。这样,在后续需要时可以恢复对象到之前的状态。
参考资料
《设计模式:可复用的面向对象软件的基础》(设计模式奠基之作)
《设计模式其实很简单》(设计模式的设计原则讲得很清楚)