概念
每一个设计模式描述了一个在我们周围不断重复发生的问题,以及该问题解决方案的核心,这样,就可以在遇到相同的问题时使用该解决方案进行解决,不必进行重复的工作,设计模式的核心在于提供了问题的解决方案,使人们可以更加简单的复用成功的设计与架构。
设计模式有 4 个基本要素:模式名称,问题(应该在何时使用模式,该模式解决了什么问题),解决方案(设计模式的内容),效果(设计模式应用的效果)
使用设计模式的主要目的是为了可重用代码、让代码更容易被他人理解、提高代码的可靠性。
设计模式主要被划分为三类:1.创建型模式 2.结构型模式 3.行为型模式
创建型设计模式
主要用于创建对象,提高了代码的复用性和灵活性
抽象工厂模式(Abstract Factory Pattern)
它提供了一种创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。这种模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态,主要用于产品族的构建。
优点
- 它可以保证由同一个工厂类创建的产品之间的兼容性,这有助于减少客户端在集成各种产品时的复杂性。
- 它遵循开放-封闭原则,这意味着可以在不修改现有代码的情况下添加新的功能,这使得系统的扩展性得到增强。
- 它将对象的创建与使用分离,使得客户端不需要知道要创建的对象的具体类型,只需要知道它们共同的接口,这提高了代码的可读性和可维护性。
缺点
- 如果需要添加新的产品类型,可能需要修改抽象工厂接口和所有实现该接口的具体工厂类,这会增加系统的复杂性。
- 客户端必须知道所有的工厂类,并根据需要选择使用哪一个工厂类来创建对象,这增加了客户端的复杂性
应用场景
抽象工厂模式的应用场景通常包括需要提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现的情况。例如,在组装电脑的场景中,用户可以选择不同的CPU、主板、硬盘等配件,而这些配件需要相互兼容。抽象工厂模式可以确保这些配件的兼容性,同时提供一个统一的接口供用户选择和使用。
总结
抽象工厂模式是一种强大的设计模式,适用于需要创建一系列相互依赖的对象,并且希望将这些对象的创建过程与使用过程分离的场景。通过合理使用抽象工厂模式,可以提高软件的可维护性、可扩展性和可重用性。
构建器模式(Builder Pattern)
它将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。该模式主要用于构造复杂对象,特别是当构造函数参数很多或有些参数是可选的时候。‘
通过使用构建器模式,可以将参数的设置逐步分解,使得客户端可以根据需要选择性地设置参数,而不需要记住参数的顺序和数量。
优点
- 简化复杂对象的创建过程:当对象具有多个属性和组件,且构建过程涉及复杂的算法或逻辑时,可以将构建过程分解为多个步骤,从而简化对象的创建过程。
- 提高代码的可读性和可维护性:通过将构建逻辑封装在构建器中,可以使代码更加清晰和易于理解。同时,由于构建过程被分解为多个步骤,因此可以更容易地对构建过程进行修改和扩展。
- 灵活性和可扩展性:构建器模式允许客户端根据需要选择性地设置参数,从而提供了更大的灵活性。此外,如果需要添加新的构建步骤或参数,只需在构建器类中进行相应的修改,而无需修改其他部分的代码,因此具有良好的可扩展性。
缺点
- 可能降低性能:由于构建器模式涉及到多个对象的创建和方法的调用,相比直接构造函数,可能会带来一些性能开销。
- 代码冗长:构建器模式通常需要编写更多的代码,因为你需要创建构建器类,并为其定义一系列的方法以设置对象的属性。这可能会使代码看起来更冗长,尤其是在属性数量不多或者对象结构相对简单的情况下。
应用场景
构建器模式常用于创建具有多个可选属性的对象,例如配置类、报表生成器等。通过使用构建器模式,可以更加灵活地创建和配置这些对象,同时避免过多的构造函数重载和参数管理问题。
工厂方法模式(factory method)
工厂方法模式(factory method)的核心思想是将对象的实例化延迟到子类中进行。在工厂方法模式中,抽象工厂类负责定义创建产品对象的公共接口,而具体的工厂子类则负责实现该接口,创建并返回具体的产品对象。
工厂方法模式是对简单工厂模式的改进。与简单工厂模式相比,制造产品的工厂类不再只有一个,而是每种具体产品类都对应一个生产它的具体工厂类。客户端通常只与抽象工厂类进行交互,通过调用其工厂方法来获取所需的产品对象,而无需关心具体的实现细节。
工厂方法模式主要由以下几个角色组成:
- 抽象产品(Product):定义了产品的接口或抽象类,用于具体产规范品的行为。
- 具体产品(ConcreteProduct):实现了抽象产品接口或继承了抽象类的具体产品类,通常包含与特定应用相关的业务逻辑。
- 抽象工厂(Creator):提供了创建产品的接口,它声明了用于创建产品对象的操作,但不指定具体的产品类。
- 具体工厂(ConcreteCreator):实现了抽象工厂接口的具体工厂类,负责创建具体的产品对象。
优点
- 降低耦合度:工厂方法模式使得客户端与具体产品解耦,客户端不需要关心产品是如何创建以及如何实现的,只需要通过抽象工厂和产品接口进行操作即可。这降低了客户端与具体产品之间的耦合度,提高了系统的灵活性和可扩展性。
- 符合开闭原则:当需要添加新的产品时,只需要添加新的具体产品类和对应的工厂类,而不需要修改原有的代码。这符合开闭原则,即软件实体应当对扩展开放,对修改封闭
- 提高代码的可重用性和可维护性:通过工厂方法模式,可以将对象的创建逻辑封装在工厂类中,避免了在客户端代码中直接创建对象,提高了代码的可重用性和可维护性。
缺点
- 增加系统复杂度:随着具体产品数量的增加,工厂类的数量也会增加,导致类的数量增多,系统的复杂度提高。这可能会增加系统的理解和维护成本。
- 增加代码量:工厂方法模式需要定义抽象工厂类和抽象产品类,并且每个具体产品都需要一个对应的具体工厂类和具体产品类。这会导致代码量的增加,特别是在产品种类较多的情况下。
- 可能增加运行效率开销:由于每次创建产品都需要通过具体的工厂类进行实例化,这可能会增加一些运行效率的开销,尤其是在频繁创建对象的情况下
应用场景
- 当创建对象需要使用大量重复的代码时,工厂方法模式可以通过定义一个单独的创建实例对象的方法,解决上述问题。
- 当客户端不关心创建过程,不依赖产品类,不关心实例如何被创建、实现等细节时,工厂方法模式非常适用。
- 当一个类通过其子类来指定创建哪个对象时,工厂方法模式可以派上用场。客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由对应的工厂创建。
原型模式(prototype)
允许通过一个已经创建的实例作为原型,来创建新的对象。这个原型对象本身会创建目标对象,目标对象是原型对象的一个克隆。也就是说,通过原型模式创建的对象,不仅仅与原型对象具有相同的结构,还与原型对象具有相同的值。根据对象克隆深度层次的不同,原型模式可以分为浅度克隆与深度克隆。
浅度克隆:克隆的是对象的引用,因此新对象中的引用仍指向原对象的存储空间
深度克隆:克隆的是对象的值,它会递归地复制引用对象,使得新对象和原对象完全独立,互不影响。
优点
- 提高代码复用性:通过复制现有对象来创建新的对象,避免了重复编写相同的代码,提高了代码的复用性。
- 简化对象创建过程:当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例来提高新实例的创建效率。
- 隐藏具体实现细节:使用原型模式可以隐藏具体实现细节,使得客户端只关注如何使用这些对象而不需要知道其具体实现方式。
- 动态添加或删除属性:通过修改原型对象,可以动态地添加或删除属性,从而影响所有基于该原型创建的新对象。
缺点
- 需要为每一个类配备一个克隆方法:而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
- 实现深克隆时较为复杂:在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
- 可能导致性能开销:由于每次创建对象都需要通过克隆原型对象来实现,因此在某些情况下可能会引入额外的性能开销。
应用场景
- 当系统需要创建大量相似对象时,使用原型模式可以避免重复的初始化,提高对象的创建效率。
- 当创建对象的过程比较复杂,且需要多次初始化不同的属性时,使用原型模式可以简化对象的创建过程。
- 当需要动态地改变对象的属性时,使用原型模式可以通过克隆来创建新的对象,避免了修改原有对象的属性。
单例模式(Singleton)
用于确保一个类仅有一个实例,并提供一个全局访问点,在Java、C++、C# 等编程语言中,这种模式很常见,主要用于那些只需要一个对象的场景,如配置文件的读取、线程池、缓存等。
单例模式的核心思想是将类的实例数量限制为一个,并通过静态方法来访问这个唯一的实例。这有助于节省系统资源,提高性能,并避免多个实例可能导致的数据不一致或其他问题。
优点
-
节省资源:由于单例模式限制了类的实例数量只能有一个,因此可以节省系统资源,避免不必要的内存占用。特别是在需要频繁创建和销毁对象的情况下,使用单例模式可以显著提高资源利用率。
-
提高性能:由于单例模式避免了重复创建对象的过程,因此在需要频繁使用某个对象时,可以显著提高系统的性能。
-
简化管理:单例模式提供了全局唯一的访问点,这使得对共享资源的访问和管理变得简单。在需要控制对某个资源的访问时,单例模式非常有用。
-
易于扩展:在某些情况下,单例模式可以方便地添加新的功能或行为,而无需修改现有的代码。
缺点
- 单例模式将类的创建和使用紧密结合在一起,这可能导致类的职责不够单一。在某些情况下,这可能会增加代码的复杂性和维护成本。
-
测试困难:由于单例模式的全局唯一性,测试时可能难以模拟和替换实例。这可能导致测试的难度增加,尤其是在进行单元测试时。
-
线程安全问题:虽然可以通过一些技巧(如双重检查锁定、静态内部类等)来实现线程安全的单例模式,但这些实现方式可能会增加代码的复杂性和出错的可能性。
应用场景
单例模式是一种常见的设计模式,主要用于那些只需要一个实例的类,如配置文件管理器、线程池、日志记录器等。
结构型设计模式
主要用于处理类和对象之间的组合关系,实现更灵活,更可维护的代码结构
适配器模式(Adapter)
它允许将一个类的接口转换成客户端所期望的另一种接口,从而使得原本由于接口不兼容而无法协同工作的类能够一起工作。适配器模式通常用于解决两个已有接口之间不兼容的问题,它充当一个中间件或桥梁,将不兼容的接口转换为兼容的接口。
优点
- 灵活性:适配器模式允许将一个类的接口转换成客户端所期望的另一种接口,从而增强了系统的灵活性。它使得原本不兼容的类能够协同工作,提高了代码的复用性。
- 扩展性:适配器模式具有很好的扩展性。在实现适配器功能时,可以扩展源类的行为(增加方法),从而自然地扩展系统的功能。这意味着,当需要添加新的功能或接口时,可以通过编写新的适配器来实现,而无需修改原有的代码。
- 降低耦合度:通过引入适配器,可以将原本紧密耦合的类进行解耦,降低系统各部件之间的依赖关系,使得系统更加易于维护和扩展。
缺点
- 可能增加系统复杂性:如果滥用适配器模式,可能会导致系统结构变得复杂。过多的适配器会使得系统变得凌乱,不易于整体把握。此外,适配器编写过程需要结合业务场景全面考虑,这也可能增加系统的复杂性。
- 降低代码可读性:适配器模式可能会导致代码阅读难度增加,降低代码的可读性。例如,当看到调用的是A接口,而内部实际被适配成了B接口的实现时,如果系统中存在大量的这种情况,无疑会增加理解和维护的难度。
- 性能考虑:如果适配器没有实现好,可能会拖慢整个系统的性能。因此,在设计适配器时需要充分考虑性能因素,确保适配器不会对系统性能产生负面影响。
应用场景
适配器模式的应用场景广泛,包括系统扩展、接口转换、兼容老版本以及遗留系统升级等。当系统需要扩展时,可能会引入新的类和接口,而原有的客户端可能无法直接使用这些新的类和接口。通过适配器模式,可以将新的接口转换成客户端所期望的接口,使得客户端可以正常使用这些扩展的功能。
桥接模式(Bridge)
它的核心思想是将抽象部分与它的实现部分分离,使它们都可以独立地变化。这种分离使得抽象部分和实现部分可以沿着各自的维度变化,从而提供了系统的灵活性和扩展性。
优点
- 实现了抽象和实现部分的分离,有助于系统进行分层设计,从而产生更好的结构化系统。
- 对于系统的高层部分,只需要知道抽象部分和实现部分的接口,其它的部分由具体业务来完成。
- 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
缺点
- 增加了系统的理解和设计难度,因为关联关系建立在抽象层,要求开发者一开始就针对抽象层进行编程
- 要求正确识别出系统中两个独立变化的维度,因此其使用范围有一定的局限性。
应用场景
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。
组合模式(Composite)
是一种对象结构型设计模式,主要用于表示具有“整体—部分”关系的层次结构。它将对象组合成树形结构以表示“部分-整体”的层次关系,使得客户端能够以统一的方式处理单个对象和组合对象。
在组合模式中,对象被组织成树形结构,树的每个节点都是一个对象。这些对象可以是叶子节点(表示树的基本组成单元,没有子节点),也可以是组合节点(表示包含其他节点的容器)。无论是叶子节点还是组合节点,它们都实现了相同的接口,因此客户端可以对它们进行一致的操作,而不需要关心它们的具体类型。
优点
- 简化客户端代码:组合模式使得客户端代码可以一致地处理单个对象和组合对象,无需关心它们之间的具体差异。这大大简化了客户端代码,提高了代码的可读性和可维护性。
- 灵活性和扩展性:组合模式提供了对树形结构的灵活操作,可以方便地添加、删除或修改节点。同时,由于它遵循开闭原则,可以在不修改现有代码的情况下扩展新的功能。
- 统一的接口:无论是叶子节点还是组合节点,它们都实现了相同的接口,这使得客户端可以统一处理它们,无需区分它们的类型。
缺点
- 设计复杂度:组合模式的设计相对复杂,需要正确地定义抽象构件和具体构件的接口,以及处理它们之间的组合关系。这可能需要更多的时间和努力来理解和实现。
- 性能考虑:在处理大型树形结构时,由于每个节点都需要维护其子节点的列表,可能会导致内存占用较高。此外,对树形结构进行深度遍历或广度遍历等操作也可能导致性能问题。
- 不易限制容器中的构件类型:在组合模式中,容器(组合节点)可以包含任意类型的子构件,这使得在运行时对容器中的构件类型进行限制变得较为困难。这可能导致一些类型错误或不符合预期的行为。-
应用场景
组合模式常用于表示如树形菜单、文件系统、公司组织架构等具有整体和部分关系的系统对象层次。
装饰模式(Decorator)
它允许在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)这种模式属于对象结构型模式,通过创建一个包装对象(即装饰)来包裹真实的对象,以提供额外的功能。
优点
-
动态扩展功能:装饰模式可以在不修改原有类的基础上,动态地给对象增加新的职责和功能。这使得代码的扩展性非常好,可以根据需要随时增加新的装饰器来扩展功能。
-
开闭原则:装饰模式符合开闭原则,即对扩展开放,对修改封闭。这意味着当需要添加新功能时,不需要修改原有的类,而是通过添加新的装饰器类来实现。
-
灵活的组合:装饰模式可以组合多个装饰器,以创建具有多种功能的对象。这种组合是动态的,可以根据需要随时改变。
-
复用性:装饰模式中的装饰器类和被装饰类通常可以独立设计,具有较高的复用性。
缺点
-
设计复杂性:随着装饰器数量的增加,系统的复杂性可能会提高。因为每个装饰器都会增加一些新的行为,当装饰器数量较多时,理解整个系统的行为可能会变得复杂。
-
性能开销:由于装饰器模式的实现通常涉及多个对象之间的嵌套调用,这可能会带来一定的性能开销。尤其是在处理大量数据时,这种开销可能会更加明显。
-
装饰器过多可能导致理解困难:当存在过多的装饰器时,可能会使代码阅读者难以理解系统的整体功能和装饰流程。
应用场景
例如,在软件开发过程中,有时想使用一些现存的组件,但这些组件可能只完成了一些核心功能。在不改变其结构的情况下,可以动态地扩展其功能,这时就可以采用装饰器模式。
外观模式(facade)
它主要为子系统中的一组接口提供一个统一的高层次接口,使得子系统更容易使用。外观模式的核心思想是降低系统的复杂性,并提供一个简单的接口给调用者。这有助于减少系统调用者与子系统之间的耦合度,使得调用者可以更加便捷地使用子系统。
优点
- 简化接口:外观模式通过封装一组复杂的子系统功能,对外提供一个简化的接口,使得调用者可以更容易地调用子系统的功能。这有助于降低系统的复杂性,使得调用者无需了解子系统的内部实现细节,从而简化了调用过程。
- 解耦合:外观模式将子系统与外部系统进行解耦合,使得子系统的变化不会影响到使用它的外部系统。这样,外部系统只需要通过外观接口来访问子系统,无需关注子系统内部的实现细节,从而提高了系统的灵活性和可扩展性。
- 提高可维护性:由于外观模式将系统的复杂性推向了内部,使得外部看起来更加简单明了,这种封装的方式有助于代码更加易于维护。当系统复杂度高、子系统之间的联系复杂时,使用外观模式可以显著提高代码的可维护性。
- 提高安全性:外观类可以控制客户端对系统的访问,只暴露必要的接口给客户端,从而提高了系统的安全性。
缺点
- 不能很好地支持细粒度的控制:外观模式提供了一组简单的接口,用于访问系统的各个部分。如果需要更细粒度的控制,可能需要修改外观类或者直接访问系统的各个部分。
- 可能增加系统的复杂性:外观模式需要引入一个额外的外观类,这可能会增加系统的复杂性。特别是当子系统数量众多或变化频繁时,维护外观类可能会变得相对复杂。
- 可能影响性能:如果外观类的实现不够高效,可能会影响系统的性能。特别是在处理大量数据或进行复杂计算时,外观类可能成为性能瓶颈。
应用场景
- 当系统的复杂度较高,某一子系统变得过于复杂,不容易使用时,可以使用外观模式来简化系统的使用,对外提供一个简单的接口。
- 当系统中存在多个接口之间的依赖关系比较复杂时,外观模式可以封装这些接口,将复杂性进行内部化,从而简化其使用和维护。
- 当系统需要对外封闭,外界只能通过一个统一的接口来访问系统时,外观模式可以确保系统的安全性。
- 当系统需要进行重构,需要对原有的代码进行优化和改进时,外观模式可以使得代码更加易于理解和维护,提高系统的灵活性和可扩展性。
享元模式(Flyweight)
主要用于减少创建对象的数量,进而减少内存占用来提高系统性能。它通过共享对象来尽可能减少内存使用量以及分享信息给尽可能多的相似对象。享元模式特别适合用于只是因重复而导致使用无法令人接受的大量内存的大量对象。
在享元模式中,通常对象的部分状态是可以分享的。常见的做法是将这些可以共享的状态放在外部数据结构中,当需要使用时再将它们传递给享元对象。这种模式的主要目标是重用现有的同类对象,通过减少对象创建来减少内存开销并提高系统性能。
优点
- 减少大量对象的创建,降低了系统开销,提高了系统的性能。
- 可以提高程序的执行速度,因为需要处理的对象数量减少了。
缺点
- 一定程度上提高了系统的复杂度,因为需要分离外部状态和内部状态。
- 可能会增加程序的维护成本,因为需要在客户端代码中管理外部状态,并在使用享元对象时将外部状态传递给它。
应用场景
- 当系统中有大量对象并且会占用大量内存时,可以使用享元模式来减少内存使用。
- 当对象的状态可以外部化,且系统并不依赖对象的身份时,享元模式也是一个很好的选择。
- 在需要用到缓冲池的场景中,享元模式也可以发挥作用。
代理模式(Proxy)
核心思想是为其他对象提供一种代理,以控制对这个对象的访问。
优点
-
控制访问,当一个对象需要保护,不希望客户端直接访问时,代理模式可以发挥作用。代理对象在客户端和目标对象之间起到中介的作用,可以阻止客户端直接访问目标对象,而是通过代理对象进行间接访问。
-
增强功能,如果需要给某个对象增强某些功能,代理模式同样适用。代理对象可以在访问目标对象时,添加、删除或修改目标对象的行为,从而实现功能的增强。
-
解决交互问题,当两个对象之间无法直接交互时,代理模式也可以提供解决方案。通过代理对象作为中介,可以实现两个对象之间的间接交互。
-
起到很好的保护作用,由于客户端并不直接访问目标对象,而是通过代理对象进行访问,因此可以在代理对象中加入访问控制,从而限制对目标对象的访问。
缺点
- 增加系统的复杂度,引入代理模式意味着需要创建额外的代理对象,这可能会增加系统的复杂性和开发成本。
- 可能会造成请求处理速度变慢,由于每次请求都需要先经过代理对象,再转发给目标对象,因此可能会增加一些额外的开销,导致请求处理速度变慢。
应用场景
- 远程代理,当对象位于远程服务器上时,可以使用代理模式进行远程访问。客户端通过代理对象与远程对象交互,无需了解远程实现的细节。
- 安全代理:用于控制对对象的访问权限。代理对象可以根据一定的规则或策略,决定是否允许客户端访问目标对象。
行为型设计模式
主要关注对象之间的通信和协作,以实现更高级别的功能
责任链模式(Chain of Responsibility)
其主要目的是避免请求发送者与多个请求处理者耦合在一起。通过将请求沿着一个对象链进行传递,直到有一个对象处理它为止。
在这种模式中,每个对象都扮演着处理者或传递者的角色。当一个请求被发出时,它首先被传递给链中的第一个对象。如果该对象不能处理该请求,那么它会将请求传递给链中的下一个对象,以此类推,直到请求被某个对象处理为止。
优点
- 降低耦合度:发送者和接收者都不需要了解彼此的具体信息,降低了它们之间的耦合度。
- 增强可扩展性:新的处理者可以动态地添加到链中,使得系统更加灵活和可扩展。
- 提高灵活性:责任链模式允许在运行时动态地决定由哪个对象处理请求,提高了系统的灵活性。
缺点
- 无法保证请求一定被处理:如果链中的对象都不能处理请求,那么请求可能无法得到处理。
- 性能问题:对于较长的职责链,请求的处理可能涉及多个对象,这可能会影响系统的性能。
- 复杂性增加:职责链的合理性需要客户端来保证,这增加了客户端的复杂性,并可能由于错误设置而导致系统出错,如循环调用等。
应用场景
- 多个对象处理同一请求:当系统中存在多个对象都可以处理同一请求,但具体由哪个对象处理在运行时才能确定时,责任链模式非常适用。它允许将请求沿着一个对象链传递,直到找到能够处理该请求的对象。
- 日志记录:日志系统中,可以使用责任链模式将日志记录器组成一条链,实现多种日志记录方式的灵活组合。每个日志记录器都可以选择是否记录日志,以及记录日志的方式和内容。
命令模式(Command)
它将一个请求封装为一个对象,使得请求可以被传递、排队、记录或撤销,并且可以方便地实现命令的调用者和请求接收者之间的解耦。
命令模式的主要核心角色包括:
- 命令(Command):定义了执行操作的接口,通常包含一个execute方法,用于调用具体的操作。
- 调用者(Invoker):请求的调用者,内部持有具体请求的引用。
- 接收者(Receiver):请求接受者,根据请求对象的指挥进行不同的反应。
优点
- 解耦:通过命令模式,调用者和接收者之间不再直接交互,而是通过命令对象进行中介,从而实现解耦。
- 支持撤销和重做:命令对象可以存储执行状态,从而支持对操作的撤销和重做。
- 日志记录:可以方便地记录命令的执行历史,有助于系统的监控和调试。
缺点
- 可能导致系统有过多的具体命令类:随着系统中命令的增加,会相应地产生大量的具体命令类。这可能会增加系统的复杂性,导致代码量增大,维护和管理变得困难。
- 可能导致某些系统操作变得复杂:由于引入了命令对象,一些原本简单的系统操作可能会变得相对复杂。例如,每次执行一个操作都需要创建相应的命令对象,这可能会增加额外的开销。
应用场景
- 当请求调用者需要与请求接收者解耦时,命令模式可以使调用者和接收者不直接交互。
- 当系统随机请求命令或经常增加、删除命令时,命令模式可以方便地实现这些功能。
解释器模式(interpreter)
它给定一门语言,定义该语言文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。这种模式的本质在于分离实现与解释执行,通过一个解释器对象处理语法规则,把复杂的功能分离开,并选择需要被执行的功能,然后按照抽象语法树来解释执行,实现相应的功能。
优点
- 易于实现语法:在解释器模式中,每一条语法规则用一个解释器对象来解释执行,使得功能的实现变得相对简单。
- 易于改变和扩展文法:由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
- 增加新的解释表达式较为方便:只需对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合“开闭原则”。
缺点
- 对于复杂文法难以维护:如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
- 执行效率较低:解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时速度较慢,且代码的调试过程也比较麻烦。
应用场景
解释器模式的应用场景广泛,包括但不限于编程语言解析、数学表达式计算、数据查询语言解析、配置文件解析、自然语言处理以及机器人控制等。在这些场景中,解释器模式能够方便地解析和执行复杂的语言或表达式。
迭代器模式(Iterpreter)
提供了一种方法来顺序访问一个聚合对象中的各个元素,同时又不暴露该对象的内部表示。
迭代器模式主要由四个部分构成:
- Iterator(迭代器):定义访问和遍历元素的接口。
- ConcreteIterator(具体迭代器):实现迭代器接口,并对该聚合对象遍历时跟踪当前位置。
- Aggregate(聚合):定义创建相应迭代器对象的接口。
- ConcreteAggregate(具体聚合):实现聚合接口,返回具体迭代器。
优点
- 简化集合对象的接口,迭代器模式将遍历集合对象的责任封装到迭代器中,使得集合对象本身的接口更加简洁。
- 同一个聚合上可以有多个遍历。
- 支持以不同的方式遍历一个聚合对象。
- 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点
迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
应用场景
- 需要遍历一个聚合对象,而又不暴露其内部表示。
- 需要对聚合对象提供多种遍历方式。
- 需要提供一个统一的遍历接口,以便客户端代码能够以统一的方式处理不同类型的集合对象。
中介者模式(Mediator)
定义了一个中介对象来封装一系列对象之间的交互,使得这些对象不必直接相互作用,而是通过中介者对象来间接地进行交互。这样可以使得原有对象之间的耦合变得松散,且可以独立地改变它们之间的交互方式。
优点
- 类之间各司其职,符合迪米特法则,即一个对象应该对其他对象保持最少的了解。
- 降低了对象之间的耦合性,使得对象易于独立地被复用。
- 将对象间的一对多关联转变为一对一的关联,提高了系统的灵活性,使得系统易于维护和扩展。
缺点
- 当需要进行交互的对象越多时,中介者就会变得越臃肿,导致代码复杂且难以维护。
应用场景
在 GUI 界面开发中,中介者模式可以解决基于事件驱动的计算机系统中控件对象之间的复杂交互问题。通过引入中介者对象作为所有控件对象的唯一通信中心和事件处理中心,可以降低系统的耦合性,提高可维护性和可扩展性。
备忘录模式(Memento)
又叫快照模式(Snapshot Pattern)或Token模式,主要目的是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
备忘录模式的主要角色包括:
- 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
优点
- 提供了一种可以恢复状态的机制,就像“后悔药”一样,可以在需要的时候撤销操作或恢复状态。
- 实现了内部状态的封装,保护了对象状态的完整性和内部实现的隐蔽性。
- 简化了发起人 类,将状态的保存和恢复功能从发起人 类中分离出来,使得代码更加清晰和易于维护。
缺点
-
资源消耗大:当需要保存的发起人(Originator)类的成员变量过多时,备忘录对象会占用大量的存储空间。每次保存对象的状态都会消耗一定的系统资源,如果频繁进行状态保存,可能会对系统性能造成一定影响。
-
状态管理复杂性:备忘录模式需要管理备忘录对象的创建、存储和恢复,这增加了代码的复杂性。如果管理不当,可能会导致状态混乱或数据不一致的问题。
应用场景
备忘录模式适用于需要保存对象在某一时刻的状态或部分状态,并且不希望直接暴露对象内部状态的场景(不想破坏对象的封装性)。例如,游戏中的存档和读档功能、编辑工具中的“撤销”操作、浏览器中的后退功能等都可以采用备忘录模式来实现。
观察者模式(Observer)
观察者模式(有时又被称为模型(Model)-视图(View)模式、源-收听者(Listener)模式或从属者模式)在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。在观察者模式中,主体是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅并接收通知。观察者模式将观察者和被观察的对象分离开,定义了对象间的一种一对多的组合关系。
优点
-
松耦合:观察者和被观察的对象之间是抽象耦合的。这意味着观察者和被观察者可以各自独立地改变和复用,而一个类的变化不会影响到其他的类。
-
支持广播通信:当被观察者的状态发生改变时,它会自动通知所有注册的观察者。这种机制简化了对多个对象的监听,使得在大型项目中管理和协调组件间的通信变得更加容易。
-
开闭原则:观察者模式对扩展开放,对修改封闭。这意味着可以容易地添加新的观察者,而无需修改被观察者的代码。
-
灵活性和可扩展性:由于观察者可以独立存在和改变,因此可以动态地添加或删除观察者,这使得系统更加灵活和可扩展。
缺点
-
过度使用可能导致效率问题:当观察者非常多的时候,每次被观察对象的状态改变都需要通知所有的观察者,这可能会带来一些不必要的计算或操作,从而降低系统性能。
-
依赖关系不明确:在代码中,如果不仔细管理观察者和被观察者的关系,可能会导致依赖关系变得复杂和混乱,增加了代码的维护难度。
-
循环依赖:在某些情况下,观察者和被观察者之间可能存在循环依赖,这可能导致内存泄漏或其他问题。
-
通知顺序问题:在观察者模式中,通知的顺序通常是随机的或者由内部实现决定的。如果顺序对应用来说很重要,那么可能需要额外的逻辑来处理顺序问题。
状态模式(State)
也叫作状态机模式(StateMachine Pattern),它允许对象在内部状态发生改变时改变它的行为,使得对象看起来好像修改了它的类。其核心思想是将状态与行为绑定,不同的状态对应不同的行为。
状态模式主要包含三个角色:
环境类角色(Context),它定义客户端需要的接口,内部维护一个当前状态实例,并负责具体状态的切换;
抽象状态角色(IState),它定义该状态下的行为,可以有一个或多个行为;
具体状态角色,即实现了抽象状态角色所声明的接口的具体类。
优点
- 封装了转换规则,使得状态转换更加清晰和易于管理
- 枚举可能的状态,并在枚举状态之前确定状态种类,使得系统更加有序。
- 将所有与某个状态有关的行为放到一个类中,可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 可以让多个环境对象共享一个状态对象,减少系统中对象的个数。
缺点
- 使用状态模式会增加系统类和对象的个数,可能导致系统变得复杂。
- 状态模式的结构与实现都较为复杂,如果使用不当可能导致程序结构和代码的混乱。
- 状态模式对“开闭原则”的支持并不太好,增加新的状态类可能需要修改负责状态转换的源代码。
应用场景
状态模式的应用场景广泛,特别是在那些行为随状态改变而改变的场景中。例如,交通灯可以根据红灯、黄灯、绿灯的状态来改变其行为;游戏中的角色可以根据攻击、防御、逃跑等状态来改变其行为;电梯则可以根据停止、上行、下行等状态来调整其行为。
策略模式(strategy)
定义了一系列的算法,并将每一个算法封装起来,使它们可以互相替换。策略模式使得算法可以独立于使用它的客户变化,这意味着算法的实现和使用是分离的,从而提供了行为的灵活性。
策略模式涉及到三个主要角色:
- 抽象策略(Strategy)角色:这是一个抽象角色,通常是一个接口或者抽象类,它定义了所有支持的算法的公共接口。
- 具体策略(ConcreteStrategy)角色:实现了抽象策略角色所定义的接口,具体到每一个算法或行为。
- 环境(Context)角色:持有一个策略对象的引用,负责执行策略对象。
优点
- 算法与使用分离:这使得算法内容的调整不会影响使用,两者可以独立变化。
- 易于扩展:新的算法只需要实现新的类,不需要对原有的框架进行修改,符合开闭原则
- 避免多重条件选择:策略模式使得在运行时根据条件动态地改变对象的行为成为可能。
- 算法可复用:策略模式使得算法可以被不同的Context对象复用。
缺点
- 客户端必须知道所有的策略,并且自行选择使用哪一种策略。
- 代码中可能会产生非常多的策略类,这增加了维护的难度。
应用场景
策略模式在处理复杂的业务场景,实现系统的灵活性和可重复性方面发挥着重要作用。例如,在准备系统路由时,可以利用策略模式来实现灵活性和可重用性;在解决复杂的三方链接场景时,策略模式也能帮助我们实现灵活的数据、流程以及结构处理。
模板方法模式(Template Method)
它定义了一个算法的骨架,并允许子类为一个或多个步骤提供具体的实现。这使得子类可以在不改变算法结构的前提下,重新定义算法的某些步骤。在软件工程中,模板方法模式提供了一种将不变部分与可变部分分离的机制,从而提高了代码复用性和扩展性。
模板方法模式主要由以下角色组成:
- 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它定义了一个或多个抽象操作,这些抽象操作是算法中的可变部分,由子类来实现。同时,它还定义并实现了一个模板方法,这个模板方法给出了算法的顶级逻辑骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。
- 具体子类(Concrete Subclass):实现父类所定义的一个或多个抽象方法,它们是算法中可变部分的具体实现。每一个具体子类都可以给出这些抽象方法(也就是算法中的可变步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
优点
- 封装了不变部分,扩展了可变部分。它将算法的不变部分封装到父类中实现,而将可变部分留给子类来实现,便于子类继续扩展。
- 提高了代码复用性。将相同部分的代码放在抽象的父类中,避免了子类中的重复代码
- 提高了扩展性。子类可以通过实现不同的抽象方法来扩展或具体实现固定算法的某个特定步骤,符合开闭原则。
缺点
- 对每个不同的实现都需要定义一个子类,这可能导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这可能导致一种反向的控制结构,提高了代码阅读的难度。
应用场景
模板方法模式适用于多种场景,特别是在需要一次性实现算法的不变部分,并将可变部分留给子类实现的情境中。这种设计模式在以下场景中特别有用
- 算法框架定义:当需要定义一个算法框架,其中部分步骤是固定的,而其他步骤则可以根据具体需求进行扩展或修改时,,模板方法模式非常适用。通过定义抽象的父类和具体的子类,可以灵活地实现算法的不同变体。
- 避免代码重复:当多个子类中存在公共的算法步骤时可以将这些公共步骤提取出来,放到一个公共的父类中,避免代码重复。子类只需关注它们特有的部分,通过继承父类来实现完整的算法。
- 扩展现有功能:在需要扩展现有功能而不修改原有代码的情况下,模板方法模式非常有用。通过创建新的子类并实现特定的方法,可以在不改变原有算法结构的前提下添加新功能。
访问者模式(Visitor)
主要用于在不修改已有对象结构的情况下,定义新的操作方式。它允许你在不改变数据结构的前提下,通过在数据结构中加入一个新的角色——访问者,来执行不同的操作。
在访问者模式中,通常包含以下几个角色:
- 抽象访问者(Visitor):定义了对自身数据结构中各个元素的操作。为每个具体元素类对应一个访问操作,该操作中的参数标识了被访问的具体元素。
- 具体访问者(Concrete Visitor):实现了访问者接口中定义的具体操作,确定访问者访问一个元素时该做什么。
- 抽象元素(Element):定义了接受访问者的接口,通常是一个接口或抽象类,其中定义了一个接受访问者的方法,被访问者对象作为方法的参数。每个具体元素都有自己的业务逻辑,并在接收访问者时将具体的操作委托给访问者进行处理。
优点
- 扩展性好:方便添加新的访问者,只需实现新的访问者接口即可。
- 复用性好:通过访问者可以定义整个对象结构通用的功能,从而提高系统的复用程度。
- 灵活性好:访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可以相对自由地演化而不影响系统的数据结构。
- 符合单一职责原则:访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
缺点
- 破坏封装:访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
- 违反依赖倒置原则:具体元素类依赖于访问者类,这违反了依赖倒置原则,即高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
应用场景
访问者模式常用于解决一些需要对对象进行遍历或者操作的问题,例如门诊部对不同类型的病人对象进行不同的操作、电商网站对不同类型的商品对象进行不同的处理方法、汽车修理厂对不同类型的汽车对象进行不同的修理方法等。