SOLID原则
S:单一原则
修改一个类的原因只能有一个
尽量让每个类只负责软件中的一个功能,并将该功能完全封装在该类中。 目的:减少复杂度
O:开闭原则
对于扩展,类应该是“开放”的;对于修改,类则应该是“封闭”的。
本原则的主要理念是在实现新功能的时候保持已有代码不变。
- 开放:可以对一个类进行扩展,可以创建它的子类并对其做任何事情。
- 封闭:如果每个类已经做好了充分的准备并可提供其它类使用(即其接口已明确定义且以后不会修改),那么该类就是封闭的,例如
String
L:里氏替换原则
当你扩展一个类时,记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递。
这意味着必须要保持与父类行为的兼容。在重写一个方法时,你要对其基类行为进行扩展,而不是将其完全替代。
- 子类方法的参数必须与其超类的参数类型相同或更加抽象;
- 子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配;
- 子类中的方法不应抛出基础方法预期之外的异常类型:在绝大多数现代编程语言中,这些规则内置其中,如果违反了,将无法对程序进行编译;
- 子类不应该加强其前置条件:例如,基类的方法有一个
int
类型的入参。如果子类重写时,要求传递给该方法的参数值必须为证书(如果该值为负则抛出异常),这就是加强了前置条件; - 子类不能削弱其后置条件:例如,某个类中有个方法对
IO
进行操作,该方法操作完成后会自动关闭IO流。而继承的子类对该方法进行了修改,为了对连接进行复用,操作完IO之后并没有关闭,然而这对使用该方法的客户端是透明的,客户端在使用该子类时就会导致内存泄漏,文件句柄超出限制的问题; - 子类不能修改超类中私有成员变量的值。
I:接口隔离原则
客户端不应该被迫依赖于其不使用的方法。
D:依赖倒置原则
高层次的类不应该依赖于低层次的类。两者都应该依赖于抽象接口。抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。
-
低层次的类实现基础操作(例如磁盘操作、传输网络数据和连接数据库等),这些类是稳定的;
-
高层次的类包含复杂的业务逻辑以指导低层次类执行特定操作。
修改前:高层次类依赖于低层次的类
修改后:低层次的类依赖于高层次的抽象
UML类关系
依赖(Dependency)
依赖关系时五种关系中耦合最轻的一种关系。类A要完成某个功能引用了B,则类A依赖类B。
关联(Association)
关联类之间的关系比依赖要强。学生与老师是关联的,学生可以不用电脑,但是学生不能没有老师。
依赖和关联的区别: 发生依赖的两个类都不会增加属性。其中一个类作为另一个类的方法的返回值或者是某个方法的变量而已。而发生关联的两个类,类A成为类B的属性,而属性是一种更为紧密的耦合,更为长久的持有关系。
聚合(Aggregation)
聚合用来表示集体与个体之间的关联关系。例如班级与学生之间存在聚合关系。
组合(复合,Composition)
组合用来表示个体与组成部分之间的关联关系。例如学生与心脏之间存在组合关系。
聚合与组合的区别: 聚合的成员可独立,组合的成员必须依赖于整体才有意义。
泛化
创建型模式
创建型模式提供了创建对象的机制,能够提升已有代码的灵活性和复用性。
- 工厂方法:在父类中提供一个创建对象的接口以允许子类决定实例化对象的类型。
- 抽象工厂:能创建一系列相关的对象,而无需指定其具体类。
- 生成器:能分步骤创建复杂对象。该模式允许你使用相同的创建代码生成不同类型和形式的对象。
- 原型:能够复制已有对象,而又无需使代码依赖它们所属的类。
- 单例:能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
工厂方法
工厂方法在父类提供一个创建对象的方法,允许子类决定实例化对象的类型。
类图
适用场景:
- 当你在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时;
- 如果你希望用户能扩展你软件库或框架的内部组件;
- 如果你希望复用现有对象来节省系统资源,而不是每次都重新创建对象;
优点:
- 可以避免创建者和具体产品之间的紧密耦合。
- 单一职责原则。可以将产品创建代码放在程序的单一位置,从而使得代码更容易维护。
- 开闭原则。无需更改现有客户端代码,就可以在程序中引用新的产品类型。
缺点:应用工厂方法模式需要引入许多新的子类,代码可能会因此变得更复杂。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法,随后演化为抽象工厂、原型或生成器(更灵活单更加复杂)。
- 抽象工厂模式通常基于一组工厂方法,但也可以使用原型模式来生成这些类的方法。
- 可以同时使用工厂方法和迭代器来让子类集合返回不同类型的迭代器,并使得迭代器与集合相匹配。
- 原型并不基于继承,因此没有集成的缺点。另一方面,原型需要对被复制对象进行复杂的初始化。工厂方法基于继承,但是它不需要初始化步骤。
- 工厂方法是模板方法的一种特殊形式。同时,工厂方法可以作为一个大型模板方法中的一个步骤。
抽象工厂
抽象工厂能创建一系列相关的对象,而无需指定其具体类。
类图
适合场景
- 如果代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,不希望代码基于产品的具体类进行构建。
- 如果有一个基于一组抽象方法的类,且其主要功能因此变得不明。
优点
- 可以确保同一工厂生成的产品相互匹配。
- 可以避免客户端和具体产品代码耦合。
- 单一职责原则。可以将产品生成代码抽取到同一位置,使得代码易于维护。
- 开闭原则。向应用程序中引入新产品变体时,无需修改客户端代码。
缺点
- 由于采用该模式需要向应用中引入众多接口和类,代码可能会比之前更加复杂。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法(较为简单, 而且 可以更方便地通过子类进行定制), 随后演化为使用抽象工 厂、 原型或生成器(更灵活但更加复杂)。
- 生成器重点关注如何分步生成复杂对象。抽象工厂专门用户生产一系列相关对象。抽象工厂会马上返回产品,生成器则允许在获取产品前执行一些额外构造步骤。
- 抽象工厂模式通常基于一组工厂方法,但也可以使用原型模式来生成这些类的方法。
- 当只需要对客户端代码隐藏子系统创建对象的方式时,可以使用抽象工厂来代替外观。
- 课程将抽象工厂和桥接搭配使用。如果有桥接定义的抽象只能与特定实现合作,这一模式搭配就非常有用。在这种情况下,抽象工厂可以对这些关系进行封装,并且对客户端代码隐藏其复杂性。
- 抽象工厂、生成器和原型都可以用单例来实现。
生成器
生成器也叫建造者模式、Builder,能够分步骤创建复杂对象。可以使用相同的创建代码生成不同类型和形式的对象。
类图
适合场景
- 使用生成器模式可避免“重叠构造函数”的出现。
- 使用代码创建不同形式的产品。
- 使用生成器构造组合树或其他复杂对象。
优点
- 可以分步创建对象,暂缓创建步骤或递归运行创建步骤。
- 生成不同形式的产品时,你可以复用相同的制造代码。
- 单一职责原则。可以将复杂构造代码从产品的业务逻辑中分离出来。
缺点
- 由于该模式需要新增多个类,因此代码整体复杂程度有所增加。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法(较为简单, 而且 可以更方便地通过子类进行定制), 随后演化为使用抽象工 厂、 原型或生成器(更灵活但更加复杂)。
- 生成器重点关注如何分步生成复杂对象。抽象工厂专门用户生产一系列相关对象。抽象工厂会马上返回产品,生成器则允许在获取产品前执行一些额外构造步骤。
- 可以在创建复杂组合树时使用生成器,因为这可使其构造步骤以递归的方式运行。
- 可以结合使用生成器和桥接模式:Director类负责抽象工作,各种不同的生成器负责实现工作。
- 抽象工厂、生成器和原型都可以用单例来实现。
原型
能够复制已有对象,而又无需使代码依赖它们所属的类。
类图
适用场景
- 如果需要复制一些对象,同时又希望代码独立于这些对象所属的具体类
- 如果子类的区别仅在于对象初始化方式,那么可以使用该模式来减少子类的数量。别人创建这些子类的目的可能是为了创建特定类型的对象。
优点
- 可以克隆对象,而无需与它们所属的具体类相耦合。
- 可以克隆预生成原型,避免反复运行初始化代码。
- 可以更方便地生成复杂对象。
- 可以用继承以外的方式来处理复杂对象的不同配置。
缺点
- 克隆包含循环引用的复杂对象可能会非常麻烦。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法(较为简单, 而且 可以更方便地通过子类进行定制), 随后演化为使用抽象工 厂、 原型或生成器(更灵活但更加复杂)。
- 抽象工厂模式通常基于一组工厂方法,但也可以使用原型模式来生成这些类的方法。
- 原型可用于保存命令的历史记录
- 大量使用组合和装饰的设计通常可从对于原型的使用中获益。可以通过该模式来复制复杂结构,而非从零开始重新构造。
- 原型并不基于继承,因此没有继承的缺点。另一方面,原型需要对被复制对象进行复杂的初始化。工厂方法基于继承,但是它不需要初始化步骤。
- 有时候原型可以作为备忘录的一个简化版本,其条件是需要在历史记录中存储的对象的状态比较简单,不需要链接其它外部资源,或者链接可以方便地重建。
- 抽象工厂、生成器和原型都可以用单例来实现。
单例
能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。单例模式同时解决了两个问题,所以违反了单一职责原则。
类图
适合场景
- 程序中的某个类对于所有客户端只有一个可用的实例。
- 需要更加严格地控制全局变量。
优点
- 可以保证一个类只有一个实例。
- 获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
缺点
- 违反了单一职责原则。该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
- 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。由于单例类的构造函数是私有的,而且绝大部分语言无法重写静态方法,所以需要想出仔细考虑模拟单例的方法。要么干脆不编写测试代码,或者不使用单例模式。
与其他模式的关系
- 外观类通常可以转换为单例类,因为在大部分情况下一个外观对象就足够了。
- 如果能将对象的所有共享状态简化为一个享元对象,那么享元就和单例类似了。单这两个模式有两个根本性的不同。
- 只会有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同。
- 单例对象可以是可变的。享元对象是不可变的
- 抽象工厂、生成器和原型都可以用单例来实现。
结构型模式
结构型模式介绍如何将对象和类组成较大的结构,并同时保持结构的灵活和搞笑。
- 适配器:让接口不兼容的对象能够相互合作。
- 桥接:可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而能在开发时分别使用。
- 组合:可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。
- 装饰:允许通过将对象放入包含行为的特殊封装对象中来为原来对象绑定新的行为。
- 外观:能为程序库、框架或其他复杂类提供一个简单的接口。
- 享元:摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,能在有限的内存容量中载入更多对象。
- 代理:能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。
适配器
适配器能够使接口不兼容的对象能够相互合作
类图
适用场景
- 当使用某个类,但是其接口与其它代码不兼容时。
- 如果需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法,但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
优点
- 单一职责原则。可以将接口或数据转换代码从程序主要业务逻辑中分离。
- 开闭原则。只要客户端代码通过客户端接口与适配器进行交互,就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
缺点
- 代码整体复杂度增加,因为需要新增一系列接口和类。有时直接更改服务类使其与其它代码兼容会更简单。
与其他模式的关系
- 桥接通常会于开发前期进行设计,能够将程序的各个部分独立开来。另一方面,适配器通常在已有程序中使用,让相互不兼容的类能很好地合作。
- 适配器可以对已有对象的接口进行修改,装饰则能在不改变对象接口的前提下强化对象功能。此外,装饰还支持递归组合,适配器则无法实现。
- 适配器能为被封装对象提供不同的接口,代理能为对象提供相同的接口,装饰则能为对象提供加强的接口。
- 外观为现有对象定义了一个新接口,适配器则会视图运用已有的接口。适配器通常只封装一个对象,外观通常会作用于整个子系统上。
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。
桥接
桥接可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而能在开发时分别使用。
类图
适用场景
- 拆分或重组一个具有多重功能的庞杂类。
- 在几个独立维度上扩展一个类。
- 在运行时切换不同实现方式。
优点
- 可以创建与平台无关的类和程序。
- 客户端代码仅与高层抽象部分进行互动,不会接触到平台的详细信息。
- 开闭原则。可以新增抽象部分和实现部分,且它们之前不会相互影响。
- 单一职责原则。抽象部分专注于处理高层逻辑,实现部分处理平台细节。
缺点
- 对高内聚的类使用该模式可能会让代码更加复杂。
与其他模式的关系
- 桥接通常会于开发前期进行设计,能够将程序的各个部分独立开来。另一方面,适配器通常在已有程序中使用,让相互不兼容的类能很好地合作。
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。
- 可以将抽象工厂和桥接搭配使用。如果由桥接定义的抽象只能与特定实现合作,这一模式搭配就非常有用。在这种情况下,抽象工厂可以对这些关系进行封装,并且对客户端代码隐藏其复杂性。
- 可以结合使用生成器和桥接模式:Director类负责抽象工作,各种不同的生成器负责实现工作。
组合
可以使用组合将对象组合成树状结构,并且能像使用独立对象一样使用它们。
类图
适用场景
- 实现树状对象结构。
- 客户端代码以相同方式处理简单和复杂元素。
优点
- 可以利用多态和递归机制更方便地使用复杂树结构。
- 开闭原则。无需修改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分。
缺点
- 对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,需要过度一般化组件接口,使其变得令人难以理解。
与其他模式的关系
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。
- 可以在创建复杂组合树使用生成器,因为这可使其构造步骤以递归的方式运行。
- 责任链通常和组合模式结合使用。在这种情况下,叶组件接收到请求后,可以将请求沿包含全体父组件的链一直传递到对象树的地步。
- 可以使用迭代器来遍历组合树。
- 可以使用访问者对整个组合树执行操作。
- 可以使用享元实现组合树的共享节点以节省内存。
- 组合和装饰的结构图很相似,因为两者都依赖递归组合来组织无限数量的对象。
- 装饰类似于组合,但只有一个子组件。
- 装饰为被封装对象添加了额外的职责,组合仅对其子节点的结果进行了“求和”。
- 大量使用组合和装饰的设计通常可从对于原型的使用中获益。可以通过该模式来复制复杂结构,而非从零开始重新构造。
装饰
装饰通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
类图
适用场景
- 在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外行为。
- 如果用继承来扩展对象行为的方案难以实现或者根本不行。
优点
- 无需创建新子类即可扩展对象的行为。
- 可以在运行时添加或删除对象的功能。
- 可以用多个装饰封装对象来组合几种行为。
- 单一职责原则。可以将实现了许多不同行为的一个大类拆分为多个较小的类。
缺点
- 在封装器栈中删除特定封装器比较困难。
- 实现行为不受装饰栈顺序影响的装饰比较困难。
- 各层的初始化配置代码看上去可能会很糟糕。
与其他模式的关系
- 适配器可以对已有对象的接口进行修改,装饰则能在不改变对象接口的前提下强化对象功能。此外装饰还支持递归组合,适配器则无法实现。
- 适配器能为被封装对象提供不同的接口,代理能为对象提供相同的接口,装饰则能为对象提供加强的接口。
- 责任链和装饰模式的类结构非常相似。两者都依赖递归组合将需要执行的操作传递给一系列对象。但是,两者有几点重要的不同之处。
- 责任链的管理者可以相互独立地执行一切操作,还可以随时停止传递请求。
- 各种装饰可以在遵循基本接口的情况下扩展对象的行为。
- 装饰无法终端请求的传递。
- 组合和装饰的结构图很相似,因为两者都依赖递归组合来组织无限数量的对象。
- 装饰类似于组合,但只有一个子组件。
- 装饰为被封装对象添加了额外的职责,组合仅对其子节点的结果进行了“求和”。
- 大量使用组合和装饰的设计通常可从对于原型的使用中获益。可以通过该模式来复制复杂结构,而非从零开始重新构造。
- 装饰和代理有着相似的结构,但是其意图却非常不同。这两个模式的构建都基于组合原则,也就是说一个对象应该将部分工作委派给另一个对象。两者之间的不同之处在于代理通常自行管理其服务对象的生命周期,而装饰的生成则总是由客户端进行控制。
外观
外观能为程序库、框架或其它复杂类提供一个简单的接口。
类图
适用场景
- 需要一个指向复杂子系统的直接接口,且该接口的功能有限。
- 将子系统组织为多层结构。
优点
- 可以让自己的代码独立于复杂子系统。
缺点
- 外观可能成为程序中所有类都耦合的上帝对象
与其他模式的关系
- 外观为现有对象定义了一个新接口,适配器则会试图运用已有的接口。适配器通常只封装一个对象,外观通常会作用于整个子系统上。
- 当只需要对客户端代码隐藏子系统创建对象的方式时,可以使用抽象工厂来代替外观。
- 外观和中介者的职责类似:都尝试在大量紧密耦合的类中组织起合作。
- 外观为子系统中的所有对象定义了一个简单的接口,但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。
- 中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象,无法直接相互交流。
- 外观类通常可以转换为单例类,因为在大部分情况下一个外观对象就足够了。
- 外观和代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。代理与其服务对象遵循同一接口,使得自己和服务对象可以互换,在这一点上与外观不同。
享元
享元摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象。
类图
享元模式只是一种优化。在应用该模式之前,要确定程序中存在大量类似对象同时占用内存相关的内存消耗问题,并且保证该问题无法使用其它更好的方法来解决。
适用场景
- 仅在程序必须支持大量对象且没有足够内存容量时使用。
优点
- 如果程序中有很多相似对象,那么就可以节省大量内存。
缺点
- 你可能需要牺牲执行速度来缓存内存,因为他人每次调用享用方法时都需要重新计算部分情景数据。
- 代码会变得更加复杂。
与其他模式的关系
- 可以使用享元实现组合树的共享叶节点以节省内存。
- 享元展示了如何生成大量的小型对象,外观则展示了如何用一个对象来代表整个子系统。
- 如果能将对象的所有共享状态简化为一个享元对象,那么享元就和单例类似了。但这两个模式有两个根本性的不同。
- 1、只会有一个单例实体,但是享元可以有多个实体,各实体的内在状态也可以不同。
- 2、单例对象可以是可变的。享元对象是不可变的。
代理
代理能够提供对象的替代品或其占位符。代理控制着对于原有对象的访问,并允许在将请求提交给对象前后进行一些处理。
类图
适合场景
- 延迟初始化(虚拟代理)。如果有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用代理模式。
- 访问控制(保护代理)。只希望特定客户端使用服务对象。
- 本地执行远程服务(远程代理)。适用于服务对象位于远程服务器上的情形。
- 记录日志请求(日志记录代理)。适用于需要保存对于服务对象的请求历史记录时。代理可以在向服务传递请求前进行记录。
- 智能引用。可在没有客户端使用某个重量级对象时立即销毁该对象。
优点
- 可以在客户端毫无察觉的情况下控制服务对象。
- 如果客户端对服务对象的生命周期没有特殊要求,可以对生命周期进行管理。
- 即使服务对象还没准备好或不存在,代理也可以正常工作。
- 开闭原则。可以在不对服务或客户端做出修改的情况下创建新代理。
缺点
- 代码可能变得更复杂,因为需要新建许多类。
- 服务响应可能会延迟。
与其他模式的关系
- 适配器能为被封装对象提供不同的接口,代理能为对象提供相同的接口,装饰则能为对象提供加强的接口。
- 外观和代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。代理与其服务对象遵循同一接口,使得自己和服务对象可以互换,在这一点上与外观不同。
- 装饰和代理有着相似的结构,但是其意图却非常不同。这两个模式的构建都基于组合原则,也就是说一个对象应该将部分工作委派给另一个对象。两者之间的不同之处在于代理通常自行管理其服务对象的生命周期,而装饰的生成则总是由客户端进行控制。
行为模式
行为模式负责对象间的高效沟通和职责委派。
- 责任链:允许将请求沿着矗立着链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下个处理者。
- 命令:可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。
- 迭代器:能在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。
- 中介者:能减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。
- 备忘录:允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
- 观察者:允许定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其它对象。
- 状态:能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
- 策略:能定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能不相互替换。
- 模板方法:在超类中定义一个算法的框架,允许子类再不修改结构的情况下重写算法的特定步骤。
- 访问者:将算法与其所作用的对象隔离开来。
责任链
允许将请求沿着矗立着链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下个处理者。
类图
适用场景
- 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时。
- 当必须按顺序执行多个处理者时。
- 所需处理者及其顺序必须在运行时进行改变。
优点
- 可以控制请求处理的顺序。
- 单一职责则。 可对发起操作和执行操作的类进行解耦。
- 开闭原则。可以在不更改现有代码的情况下在程序新增处理者。
缺点
- 部分请求可能未被处理。
与其他模式的关系
- 责任链、命令、 中介者和观察者用于处理请求发送者和接收者之间的不同连接方式:
- 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
- 责任链通常和组合模式结合使用。 在这种情况下, 叶组件接 收到请求后,可以将请求沿包含全体父组件的链一直传递至对象树的底部。
- 责任链的管理者可使用命令模式实现。在这种情况下,可以对由请求代表的同一个上下文对象执行许多不同的操作。 还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 可以对由一系列不同上下文连接而成的链 执行相同的操作。
- 责任链和装饰模式的类结构非常相似。两者都依赖递归组合将需要执行的操作传递给一系列对象。但是,两者有几点重要的不同之处。
- 责任链的管理者可以相互独立地执行一切操作,还可以随时停止传递请求。
- 各种装饰可以在遵循基本接口的情况下扩展对象的行为。
- 装饰无法终端请求的传递。
命令
可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。
类图
适用场景
- 需要通过操作来参数化对象。
- 如果想要将操作放入队列中、操作的执行或者远程执行操作。
- 如果想要实现操作回滚功能。
优点
- 单一职责原则。可以解耦触发和执行操作的类。
- 开闭原则。可以在不修改已有客户端代码的情况下在程序中创建新的命令。
- 可以实现撤销和恢复功能。
- 可以实现操作的延迟执行。
- 可以将一组简单命令组合成一个复杂命令。
缺点
- 代码可能会变得更加复杂,因为在调用者和接收者之间增加了个全心的层次。
与其他模式的关系
- 责任链、命令、 中介者和观察者用于处理请求发送者和接收者之间的不同连接方式:
- 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
- 责任链的管理者可使用命令模式实现。在这种情况下,可以对由请求代表的同一个上下文对象执行许多不同的操作。 还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 可以对由一系列不同上下文连接而成的链 执行相同的操作。
- 命令和策略看上去很想,因为两者都能通过某些行为来参数化对象。但是,它们的意图有非常大的不同。
- 你可以使用命令来讲任何操作转换成对象。操作的参数将成为对象成员变量。你可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者想远程服务发送命令等。
- 另一方面,策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法。
- 原型可用于保存命令的历史记录。
- 可以将访问者视为命令模式的加强版本,其对象可对不同类的多种对象执行操作。
迭代器
能在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。
类图
适用场景
- 当集合背后为复杂的数据结构,且希望对客户端隐藏其复杂性时(出于使用便利和安全性考虑)。
- 使用该模式可以减少程序中重复的遍历代码。
- 需要遍历不同的甚至是无法预知的数据结构。
优点
- 单一职责原则。通过将体积庞大的遍历算法代码抽取为独立的类,可以对客户端代码和集合进行整理。
- 开闭原则。可实现新型的集合和迭代器并将其传递给现有代码,无需修改现有代码。
- 可以并行遍历同一集合,因为每个迭代器对象都包含其自身的遍历状态。
- 相似的,可以暂停遍历并在需要时继续。
缺点
- 对于某些特殊集合,使用迭代器可能比直接遍历的效率低。
- 如果只是简单的集合进行交互,应用该模式可能会矫枉过正。
与其他模式的关系
- 可以使用迭代器来遍历组合树。
- 可以同时使用工厂方法和迭代器来让子类集合返回不同类型的迭代器,并使得迭代器与集合相匹配。
- 可以同时使用备忘录和迭代器来获取当前迭代器的状态,并且在需要的时候进行回滚。
- 可以同时使用访问者和迭代器来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。
中介者
能减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。
类图
适用场景
- 当一些对象和其它对象紧密耦合以致难以对其进行修改时。
- 当组件因过于依赖其他组件而无法在不同应用中复用时。
- 如果为了能在不同情景下复用一些基本行为,导致需要被迫创建大量组件子类时。
优点
- 单一职责原则。可以将多个组件间的交流抽取到同一位置,使其更易于理解和维护。
- 开闭原则。无需修改实际组件就能增加新的中介者。
- 可以减轻应用中多个组件间的耦合情况。
- 可以更方便地复用各个组件。
缺点
- 一段时间后,中介者可能会演变成为上帝对象。
与其他模式的关系
- 责任链、命令、 中介者和观察者用于处理请求发送者和接收者之间的不同连接方式:
- 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
- 外观和中介者的职责类似:都尝试在大量紧密耦合的类中组织起合作。
- 外观为子系统中的所有对象定义了一个简单的接口,但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。
- 中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象,无法直接相互交流。
备忘录
允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
类图
在该实现方法中,备忘录类将被嵌套在原发器中。这样原发器就可访问备忘录的成员变量和方法。另外一种实现方法适用于不支持嵌套类的编程语言(PHP),如下图所示:
如果不想让其它类有任何机会通过备忘录来访问原发器的状态,那么还有另外一种可用的实现方式如下所示:
适用场景
- 当需要创建对象状态快照来恢复之前的状态时。
- 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时。
优点
- 可以在不破坏对象封装情况下创建对象的状态快照。
- 可以通过让负责人维护原发器状态历史记录来简化原发器代码。
缺点
- 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
- 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的贝网来说。
- 绝大部分动态程序语言(如PHP、Python和JavaScript)不能确保备忘录中的状态不被修改。
与其他模式的关系
- 可以使用命令和备忘录来实现“撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
- 可以同时使用备忘录和迭代器来获取当前迭代器的状态,并且在需要的时候进行回滚。
- 有时候原型可以作为备忘录的一个简化版本,其条件是你需要在历史记录中存储的对象的状态比较简单,不需要链接其他外部资源,或者链接可以方便地重建。
观察者
允许定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其它对象。
类图
适用场景
- 当一个对象状态的改变需要改变其它对象,或实际对象是事先未知的或动态变化时。
- 当应用中的一些对象必须观察其它对象时。
优点
- 开闭原则。无需修改发布者代码就能引入新的订阅者类。
- 可以在运行时建立对象之间的联系。
缺点
- 订阅者的通知顺序是随机的。
与其他模式的关系
- 责任链、命令、 中介者和观察者用于处理请求发送者和接收者之间的不同连接方式:
- 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
状态
能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
类图
适用场景
- 对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更。
- 某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句。
- 当相似状态和基于条件的状态机转换中存在许多重复代码时。
优点
- 单一职责原则。将与特定状态相关的代码放在单独的类中。
- 开闭原则。无需修改已有状态和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
缺点
- 如果状态机只有很少的几个状态,或者很少发生改变,那么应用该模式有点小题大做。
与其他模式的关系
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。
- 状态可被视为策略的扩展。两者都基于组合机制:他们都通过将部分工作委派给“帮手”对象来改变其在不同情景下的行为。策略是的这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的以来,且允许它们自行改变在不同情境下的状态。
策略
能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
类图
适用场景
- 当想要使用对象中各种不同的算法变体,并希望在运行时切换算法时。
- 当有许多仅在执行某些行为时略有不同的相似类时。
- 如果算法在上下文的逻辑中不是特别重要,使用该模式能将类的业务逻辑与其算法实现细节隔离开来。
- 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时。
优点
- 可以在运行时切换对象内的算法。
- 可以将算法的实现和使用算法的代码隔离开来。
- 可以使用组合代替集成。
- 开闭原则。无需对上下文进行修改就能够引入新的策略。
缺点
- 如果算法极少发生改变,那么没有任何理由引入新的类和接口。使用该模式只会让程序过于复杂。
- 客户端必须知晓策略间的不同——需要选择合适的策略。
- 许多现代变成语言支持函数类型功能,允许在一组匿名函数中实现不同版本的算法。这样,使用这些函数的方法就和使用策略对象时完全相同,无需借助额外的类和接口来保持代码简洁。
与其他模式的关系
- 桥接、状态和策略(在某种程度上包括适配器)模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象,不过也各自解决了不同的问题。
- 命令和策略看上去很想,因为两者都能通过某些行为来参数化对象。但是,它们的意图有非常大的不同。
- 你可以使用命令来讲任何操作转换成对象。操作的参数将成为对象成员变量。你可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者想远程服务发送命令等。
- 另一方面,策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法。
- 装饰可更改对象的外表,策略则能够改变其本质。
- 模板方法基于继承机制:它允许通过扩展子类中的部分内容来改变部分算法。策略基于组合机制:可以通过对相应行为提供不同的策略来改变对象的部分行为。模板方法在类层次上运作,因此是静态的。策略在对象层次上运作,因此允许在运行时切换行为。
- 状态可被视为策略的扩展。两者都基于组合机制:他们都通过将部分工作委派给“帮手”对象来改变其在不同情景下的行为。策略是的这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的以来,且允许它们自行改变在不同情境下的状态。
模板方法
在超类中定义一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。
类图
适用场景
- 当只希望客户端扩展某个特定算法步骤,而不是整个算法或其结构时。
- 当多个类的算法除一些细微不同之外几乎完全一样时。但其后果是,只要算法发生变化,就可能需要修改所有的类。
优点
- 可仅允许客户端重写一个大型算法中的特定部分,使得算法其他部分修改对其所造成的影响减小。
- 可将重复代码提取到一个超类中。
缺点
- 部分客户端可能会收到算法框架的限制。
- 通过子类抑制默认步骤实现可能会导致违反里氏替换原则。
- 模板方法中的步骤越多,其维护工作就可能越困难。
与其他模式的关系
- 工厂方法是模板方法的一种特殊形式。同时,工厂方法可以作为一个大型模板方法中的一个步骤。
- 模板方法基于继承机制:它允许通过扩展子类中的部分内容来改变部分算法。策略基于组合机制:可以通过对相应行为提供不同的策略来改变对象的部分行为。模板方法在类层次上运作,因此是静态的。策略在对象层次上运作,因此允许在运行时切换行为。
访问者
将算法与其所作用的对象隔离开来。
类图
适用场景
- 对一个复杂结构(例如对象树)中的所有元素执行某些操作。
- 可以使用访问者模式来清理辅助行为的业务逻辑。
- 当某个行为仅在类层次结构中的一些类中有意义,而在其它类中没有意义时,可使用该模式。
优点
- 开闭原则。可以引入在不同类对象上执行的新行为,且无需对这些类做出修改。
- 单一职责原则。可将统一行为的不同版本移动同一个类中。
- 访问者对象可以在与各种对象交互时收集一些有用的信息。当要遍历一些复杂的对象结构,并在结构中的每个对象上应用访问者,这些信息可能会有所帮助。
缺点
- 每次在元素层结构中添加或移除一个类时,都要更新所有的访问者。
- 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限。
与其他模式的关系
- 可以将访问者视为命令模式的加强版本,其对象可对不同类的多种对象执行操作。
- 可以使用访问者对整个组合树执行操作。
- 可以同时使用访问者和迭代器来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。
参考
《深入设计模式》