Skip to content

@author owenj https://gemini.google.com/app/088e74ca9f8e229f

简单工厂模式 (Simple Factory Pattern)

它的核心思想是:定义一个用于创建对象的工厂类,根据传入的参数来决定创建哪一种产品的实例。 简单来说,就是你想要一个东西,但你不用亲自去创建它(new),而是告诉一个“工厂”你想要什么,工厂就会把造好的东西给你。这实现了创建者和使用者之间的解耦

适用场景:

  • 需要创建的对象数量较少,且在未来不会频繁增加。
  • 客户端只关心产品的使用,不关心产品的创建细节。例如,只需要知道传入“打折”参数就能得到打折对象,而不需要知道这个对象具体是哪个类。
  • 当你想将对象的创建逻辑集中管理,以方便维护时。

优点:

  • 职责分离:将对象的创建和使用分离,客户端无需关心创建细节。
  • 降低耦合:客户端代码只依赖于抽象产品和工厂类,不依赖于具体产品类

缺点:

  • 违反开闭原则:当需要增加新的产品时(例如增加一种“秒杀价”收费方式),就必须修改工厂类的内部判断逻辑,这违反了“对扩展开放,对修改关闭”的原则。
  • 工厂类职责过重:所有产品的创建逻辑都集中在一个工厂类中,如果产品种类非常多,这个工厂类会变得异常臃肿和复杂。

策略模式 (Strategy Pattern)

在简单工厂上加了一层

核心思想: 定义一个算法族,将每个算法分别封装起来,让它们可以互相替换。该模式使得算法的变化独立于使用它的客户端。本质上是 “分离算法,选择实现”,将“做什么”(Context)和“怎么做”(Strategy)分离开。

适用场景:

  • 当一个功能有多种实现方式,且需要在运行时动态选择一种时。例如,文件压缩有 ZIP、RAR、GZ 多种格式,可以为每种格式创建一个策略。
  • 当系统中存在大量 if-elseswitch-case 语句来选择不同的行为时,可以使用策略模式来替代这些条件判断,使代码更清晰、更易于扩展。
  • 当你想对客户端隐藏算法的具体实现细节时。客户端只需要知道用哪个策略,而不需要关心策略内部的复杂逻辑。
  • 当多个类只有行为上的区别时。可以将这些行为提取出来,做成独立的策略类,让这些类共享这些策略。

优点:

  • 遵循开闭原则:增加一个新的策略非常容易,只需要添加一个新的具体策略类即可,无需修改原有的上下文代码。
  • 避免多重条件判断:优雅地消除了代码中的 if-else 分支,提高了代码的可维护性。
  • 高内聚:每个算法的实现细节都被封装在自己的类中,职责清晰。

缺点:

  • 类数量增多:每个策略都会增加一个类,如果策略过多,会导致系统中的类数量膨胀。
  • 客户端必须了解所有策略:客户端需要知道有哪些具体的策略,并自行决定在何时使用哪一个。当然,这个问题可以结合工厂模式来解决,由工厂来负责创建具体的策略对象。

备忘录模式 (Memento Pattern)

核心思想: 在不破坏对象封装性的前提下,捕获一个对象的内部状态,并在该对象之外进行保存。这样以后就可以将该对象恢复到原先保存的状态。本质上是“将状态的保存与对象本身分离”。

适用场景:

  • 需要实现**撤销(Undo)/重做(Redo)**功能的场景,如文本编辑器、绘图工具等。
  • 需要实现**“快照”**功能的系统,例如虚拟机快照、系统还原点。
  • 游戏开发中的存档和读档机制。
  • 需要对一个复杂对象的状态进行备份,以便在某个操作失败后能**回滚(Rollback)**到操作之前的状态,例如数据库事务。
  • 任何需要保存和恢复一个对象历史状态的场合,同时又不想暴露该对象的内部实现。

优点:

  • 良好的封装性:将对象的状态存储在外部,但不会暴露对象的内部实现细节。管理者(Caretaker)并不知道备忘录(Memento)里存了什么。
  • 职责单一:发起人(Originator)负责其自身的业务逻辑,状态的存储和管理则交给了备忘录和管理者,符合单一职责原则。
  • 简化发起人:发起人不需要管理自己的历史状态,代码结构更清晰。

缺点:

  • 资源消耗:如果发起人的状态非常复杂或者备忘录创建得非常频繁,可能会占用大量的内存资源。
  • 代码复杂度:如果系统的业务逻辑很简单,引入备忘录模式可能会增加不必要的类和复杂性。

代理模式 (Proxy Pattern)

核心思想: 为其他对象提供一种代理以控制对这个对象的访问。代理对象作为中介,可以在请求传递给真实对象之前或之后,执行一些额外的处理。

适用场景: 代理模式的应用场景非常广泛,根据其目的可以分为几种类型:

  • 虚拟代理 (Virtual Proxy): 当一个对象创建成本很高时(例如,加载一张高清大图),可以使用一个轻量级的代理来代表它。只有当真正需要这个对象时,代理才会去创建和加载它。这是一种**延迟加载(Lazy Loading)**技术。
  • 保护代理 (Protection Proxy): 用于控制对真实对象的访问权限。代理会在转发请求前,检查调用者是否具有足够权限。例如,不同用户等级拥有不同的操作权限。
  • 远程代理 (Remote Proxy): 为一个位于不同地址空间(例如,另一台服务器上)的对象提供一个本地的代表。客户端调用本地代理的方法,代理负责网络通信,将请求发送给远程的真实对象。
  • 智能引用 (Smart Reference): 在访问对象时执行一些额外的操作,例如:
    • 记录对象被引用的次数(引用计数)。
    • 在第一次访问时,将对象从数据库加载到内存。
    • 在访问前后进行日志记录(AOP面向切面编程)。
    • 实现线程安全的访问控制。 优点:
  • 职责清晰:真实主题只需关注核心业务逻辑,代理可以处理如权限控制、日志、缓存等非核心功能。
  • 开闭原则:可以在不修改真实主题的情况下,通过增加代理来扩展功能。
  • 高灵活性:代理可以实现多种控制策略,如延迟加载可以优化性能,保护代理可以增强安全性。

缺点:

  • 增加间接层:引入代理会增加系统中的类和对象的数量,可能导致请求处理速度变慢(因为增加了一层转发)。
  • 实现复杂:某些代理模式的实现可能比较复杂,需要额外的代码。

单例模式 (Singleton Pattern)

核心思想: 保证一个类仅有一个实例,并提供一个全局访问点来获取该实例。它通过将构造函数私有化、自行管理其唯一实例以及提供一个静态方法供外界访问,来控制实例的创建和分发。 有懒汉单例、饿汉单例

适用场景:

  • 当一个类需要且只需要一个实例时。这通常适用于那些代表着“全局唯一”的组件。
    • 无状态的工具类:例如一个日志记录器(Logger),整个应用共享一个日志实例即可。
    • 全局配置类:系统的配置信息在运行时应该是统一的,适合用单例模式管理。
    • 数据库连接池或线程池:这些是重量级资源,创建多个实例会极大地消耗系统资源,必须保证其唯一性。
    • 操作系统的窗口管理器或任务管理器:在整个系统中,这些显然都只需要一个。
    • 网站的计数器,或者Web应用中的Application对象。

优点:

  • 保证实例唯一:从根本上控制了实例的数量,防止多个实例造成逻辑错误或资源浪费。
  • 全局访问点:提供了一个方便的全局访问入口,可以随时获取到该实例。
  • 延迟加载:懒汉式实现可以在第一次使用时才创建实例,节省了内存空间,特别是在实例创建开销较大时。

缺点:

  • 违反单一职责原则:单例类既要负责自身的业务逻辑,又要负责保证自己是单例的,职责不够单一。
  • 扩展困难:由于没有抽象层,单例类的扩展有很大的困难(例如,无法继承)。
  • 对测试不友好:如果代码依赖于一个全局的单例,会使得单元测试变得困难,因为无法用模拟对象(Mock)来替换这个单例。
  • 可能隐藏依赖关系:代码中的任何地方都可以通过 getInstance() 获取实例,这可能导致模块间的依赖关系变得不明确。

迭代器模式 (Iterator Pattern)

核心思想: 将遍历集合的责任从集合本身分离出来,转移到一个独立的“迭代器”对象中。这样做的好处是,客户端在遍历集合时,无需关心集合内部的存储结构,从而实现了遍历操作与集合实现的解耦。

适用场景:

  • 当你需要为一个聚合对象提供多种不同的遍历方式时(例如,正序遍历、倒序遍历)。你可以提供多种不同的迭代器实现。
  • 当你想为不同结构(但概念上相似)的聚合对象(如 List, Set, Tree)提供一个统一的遍历接口时。
  • 当你希望对客户端隐藏聚合对象的内部结构时。迭代器提供了一个受控的、简化的访问接口,客户端不能随意修改集合内容(除非迭代器本身提供了 remove 等方法)。
  • 当遍历逻辑比较复杂时,将其从聚合类中分离出来,可以使聚合类的职责更单一,代码更清晰。

优点:

  • 封装性:很好地隐藏了聚合对象的内部实现细节。
  • 单一职责原则:将数据存储(聚合的职责)和数据遍历(迭代器的职责)分离开来。
  • 扩展性好:可以很方便地增加新的聚合类和迭代器类,而无需修改原有代码。
  • 支持多种遍历:可以为同一个聚合提供不同的迭代器来支持不同的遍历策略。

缺点:

  • 增加了类的数量:对于一个简单的聚合,引入迭代器模式会增加一些额外的类,可能会使系统设计变得稍微复杂一些。

访问者模式 (Visitor Pattern)

核心思想: 将处理逻辑(操作)从数据结构中分离出来。它允许在不修改数据结构中各个元素类的前提下,定义作用于这些元素的新操作。

适用场景:

  • 当一个对象结构中包含许多类型的对象,并且你希望对这些对象实施一些依赖于其具体类型的操作时。
  • 当数据结构相对稳定,但需要频繁地定义新的操作时。这是使用访问者模式的关键前提。如果数据结构经常变动(比如增加新的 ConcreteElement 类),那么使用此模式会非常痛苦。
  • 当需要对一个复杂的对象结构(如组合模式构成的树形结构)中的所有元素执行某项操作时。
  • 典型案例:编译器的抽象语法树(AST)。AST的节点类型(Element)是固定的,但需要对它进行多种操作,如类型检查、代码生成、优化等(这些就是不同的Visitor)。

优点:

  • 符合开闭原则:可以非常方便地增加新的操作(增加新的ConcreteVisitor),而无需修改已有的元素类和访问者类。
  • 高内聚:将相关的操作集中到一个访问者类中,而不是分散在各个元素类里。
  • 职责分离:数据结构负责维护自身数据,而操作则由访问者负责,职责清晰。

缺点:

  • 增加新元素困难:这是访问者模式最大的缺点。每当在数据结构中增加一个新的 ConcreteElement 类型,就需要修改 Visitor 接口,并对所有具体的 ConcreteVisitor 类进行修改,违反了开闭原则。
  • 破坏封装:为了让访问者能够执行操作,元素类可能需要暴露一些其内部状态的接口,这在一定程度上破坏了元素的封装性。
  • 复杂性高:该模式理解和实现起来都比较复杂,特别是双重分派的机制。

观察者模式 (Observer Pattern)

核心思想: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知并被自动更新。其本质是触发联动,实现了发布者和订阅者之间的松散耦合。

适用场景:

  • 当一个对象的改变需要同时改变其他对象,但你又不想让这些对象紧密耦合时。
  • 当一个抽象模型有两个方面,其中一方面依赖于另一方面,你希望将这两者封装在独立的对象中以复用它们。这是经典的**模型-视图(Model-View)**分离场景(MVC框架的核心)。Model是Subject,View是Observer。
  • 事件处理机制:在图形用户界面(GUI)中,按钮(Subject)的点击事件会通知所有注册的监听器(Observer)。
  • 消息队列/事件总线:在分布式或模块化系统中,一个模块发布一个事件(消息),其他感兴趣的模块(订阅者)接收并处理该事件。
  • 实时数据更新:例如,股票价格(Subject)变动,所有展示该股票的图表和面板(Observer)都需要立即更新。

优点:

  • 松散耦合:目标(Subject)和观察者(Observer)之间是松散耦合的。目标只知道它有一系列观察者,但不需要知道它们的具体类别。
  • 广播机制:支持一对多的广播通信,一个通知可以被多个观察者接收。
  • 易于扩展:可以很方便地增加新的具体观察者,而无需修改目标的任何代码。

缺点:

  • 通知顺序问题:如果观察者之间存在依赖关系,那么通知的顺序可能很重要,但模式本身不保证通知的顺序。
  • 可能导致性能问题:如果一个目标有大量的观察者,或者目标状态频繁改变,通知所有观察者可能会花费大量时间,影响性能。
  • “循环引用”风险:如果目标和观察者之间存在复杂的相互依赖,可能会导致意外的、连锁的更新,甚至造成死循环。
  • 内存泄漏:如果观察者在销毁前没有从目标那里注销(remove),目标会一直持有对它的引用,导致内存无法回收。

解释器模式 (Interpreter Pattern)

核心思想: 给定一个语言,定义它的文法的一种面向对象的表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。其本质是将一个语言的文法解析成一个树形结构,并通过递归遍历来解释执行

适用场景: 这个模式适用性较窄,通常在以下情况可以考虑使用:

  • 当有一个简单的语言需要被重复解释执行时。例如,机器人指令、查询语言、数学公式解析。
  • 当你可以将一个问题或一个句子表示为抽象语法树时。
  • 当语言的语法规则相对稳定,不需要频繁变更时。
  • 经典案例:正则表达式的解析、SQL语句的解析、各种脚本语言的解释器。

优点:

  • 易于扩展语法:由于语法规则被表示为类,所以添加新的语法规则(即增加新的 Expression 子类)相对容易,符合开閉原則。
  • 灵活性高:通过类的组合,可以轻松地表示复杂的语法结构。
  • 实现清晰:将语法的表示和解释分离开来,结构清晰。

缺点:

  • 维护成本高:当语法规则变得非常复杂时,需要定义的 Expression 类的数量会急剧增加,导致整个系统难以维护。
  • 性能问题:解释器模式通常会导致执行效率较低,因为它涉及到大量的递归调用和动态的对象创建。对于性能要求高的场景通常不适用。
  • 适用范围窄:只适用于能够被表示成清晰语法树的“简单”语言。对于复杂的语言,通常会使用更专业的工具,如解析器生成器(例如 ANTLR, Yacc)。

命令模式 (Command Pattern)

核心思想: 将一个请求封装成一个对象,从而允许你使用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作。其本质是将请求的发送者与接收者解耦

最关键的一点是,它将**请求的发送者(调用者)请求的接收者(真正执行任务的对象)**彻底解耦。 命令模式中“调用者”、“命令”和“接收者”三者解耦的关系

适用场景:

  • 当你需要将操作的发起者与操作的执行者解耦时。
  • 图形界面(GUI)中的按钮和菜单项:每个菜单项或按钮都可以是一个Invoker,它持有一个Command对象。点击时执行command.execute()。这样可以轻松实现多个按钮触发同一个命令。
  • 实现撤销(Undo)和重做(Redo)功能:可以为命令接口增加一个unexecute()方法。系统维护一个已执行命令的栈,撤销时就从栈顶弹出一个命令并执行其unexecute()方法。
  • 任务队列和线程池:当需要将任务(请求)异步执行时,可以将请求封装成命令对象,放入一个队列中。后台的工作线程不断从队列中取出命令对象并执行。
  • 宏录制与执行:将用户的一系列操作封装成一系列命令对象,然后可以一次性地按顺序执行这些命令,实现宏功能。
  • 事务性操作:将一系列操作封装成命令,如果中间某个操作失败,可以依次调用前面已成功命令的撤销方法,实现事务的回滚。

优点:

  • 降低耦合度:调用者和接收者之间没有任何直接的依赖关系。
  • 易于扩展:增加新的命令非常容易,只需要创建新的具体命令类即可,符合开闭原则。
  • 命令是头等对象:命令作为对象,可以被存储、传递和管理(例如放入队列)。
  • 易于实现撤销和重做

缺点:

  • 类的数量膨胀:如果系统中的行为非常多,可能会导致需要创建大量的具体命令类,增加系统的复杂性。

模板方法模式 (Template Method Pattern)

核心思想: 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构,即可重新定义该算法的某些特定步骤。这是继承和多态的经典应用。 模板方法模式是一种行为型设计模式,它的核心思想是:在一个方法中定义一个算法的骨架(或模板),并将算法中一些可变的步骤延迟到子类中去实现。

适用场景:

  • 算法结构固定,具体步骤易变:当多个类有相同的一套算法流程,但其中个别步骤的实现细节不同时。
  • 复用和封装:当需要复用一些公共代码,同时又想把可变的行为局部化,以便于扩展和维护时。
  • 控制子类扩展:可以通过模板方法来控制子类的行为。模板方法定义了核心流程,子类只能在指定的“扩展点”(抽象方法)上进行修改,而不能改变整个流程。
  • 框架设计:这是模板方法模式最主要的应用场景。框架定义了主要的架构和流程(模板方法),开发者则通过继承和实现抽象方法来填充框架,完成具体的业务逻辑。例如,Java Servlets中的doGet/doPost方法就是模板方法模式的应用。 优点:
  • 代码复用:将公共的不变部分代码提取到父类中,提高了代码复用性。
  • 结构清晰,易于维护:将算法的骨架和具体实现分离开来,使得结构非常清晰。
  • 扩展性好:通过增加新的子类,可以很容易地增加新的功能,符合开闭原则。

缺点:

  • 继承的限制:由于该模式强依赖于继承,导致子类与父类之间存在较强的耦合。一个子类只能继承一个父类。
  • 可能导致类数量增多:每一种不同的实现都需要一个新的子类,如果变体过多,会导致类的数量膨胀。

桥接模式 (Bridge Pattern)

核心思想: 将抽象(Abstraction)与实现(Implementation)解耦,使它们可以沿着各自的维度独立变化。其本质是用组合关系代替继承关系,从而避免因多维度变化而导致的类爆炸问题。

适用场景:

  • 当一个类存在两个或多个独立变化的维度,且你希望避免使用多层继承导致类数量剧增时。
  • 当你希望一个抽象的接口和它的具体实现可以独立扩展时。
  • 当你希望在运行时动态地切换或选择一个对象的具体实现时。
  • 经典案例
    • JDBC数据库驱动:Java的java.sql包提供了一套统一的数据库操作API(抽象部分),而各个数据库厂商(MySQL, Oracle等)则提供了这套API的具体实现(实现部分)。应用程序通过这套API操作数据库,而无需关心底层用的是哪种数据库。
    • 跨平台的GUI框架:抽象的窗口、按钮等控件(抽象部分)与不同操作系统下的具体绘制实现(实现部分)分离。

优点:

  • 分离和解耦:将抽象和实现彻底分离,极大地降低了它们之间的耦合度。
  • 出色的扩展性:抽象和实现两个维度可以独立地进行扩展,而不会相互影响,符合开闭原则。
  • 灵活性高:可以在运行时动态地组合不同的抽象和实现。

缺点:

  • 增加系统复杂性:引入了额外的类和接口,需要理解四个角色的协作关系,增加了系统的理解和设计难度。
  • 设计难度大:需要在一开始就识别出系统中独立变化的维度,对设计者的要求较高。

适配器模式 (Adapter Pattern)

核心思想: 将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。它在不修改原有代码的基础上,起到了“承上启下”的桥梁作用。

适配器模式主要有两种实现方式:

  1. 类适配器模式 (Class Adapter)通过继承来实现。这就是你图中所展示的。
    • Adapter 类同时继承 Adaptee 类(获得了specificRequest的能力)和实现 Target 接口(满足了客户端的要求)。
    • Adapter重写的 request() 方法中,它调用了从Adaptee继承来的 specificRequest() 方法。
    • 缺点:由于Java等语言不支持多重类继承,所以这种方式要求Target必须是接口,且Adapter只能适配一个具体的Adaptee类,不够灵活。
  2. 对象适配器模式 (Object Adapter)通过组合/聚合来实现。(这种方式更常用,也更灵活)
    • Adapter 类不再继承 Adaptee,而是在内部持有一个 Adaptee 类的实例
    • Adapter 同样实现 Target 接口。
    • Adapter重写的 request() 方法中,它调用内部持有的Adaptee实例的 specificRequest() 方法。
    • 优点:非常灵活,一个适配器可以适配Adaptee及其所有子类。

适用场景:

  • 系统集成与兼容:当你需要使用一个已经存在的类,但它的接口不符合你的需求时(最常见的场景)。
  • 遗留代码重用:想复用一些老的、接口不统一的遗留代码,可以通过适配器将其统一成新的接口规范。
  • 第三方库或框架集成:当你使用的第三方库的接口与你系统现有接口不一致时,可以写一个适配器来“包装”一下这个库。
  • 为未来做准备:在设计系统时,如果预见到未来可能会引入接口不同的新组件,可以预先设计好Target接口,后续引入新组件时只需为其编写适配器即可。
  • 经典案例:Java中的Arrays.asList()方法,它将一个数组(Adaptee)适配成一个List(Target),使其能够被集合框架的其他部分使用。

优点:

  • 提高类的复用性:可以很好地复用已有的功能类(Adaptee)。
  • 增强灵活性和扩展性:客户端代码与具体实现解耦,更换适配者或增加新的适配者对客户端是透明的。对象适配器尤为灵活。
  • 符合开闭原则:无需修改原有代码(Adaptee),通过增加适配器类即可扩展功能。

缺点:

  • 增加代码复杂性:过多地使用适配器,会让系统中的类数量增多,代码的可读性在一定程度上会下降。
  • 类适配器的局限性:类适配器有其固有的局限性(如单继承问题),因此在实际开发中,对象适配器的使用频率远高于类适配器。

外观模式 (Facade Pattern)

核心思想: 为子系统中的一组接口提供一个统一的、高层的接口,使得子系统更加容易使用。其本质是封装交互,简化调用

适用场景:

  • 简化复杂接口:当需要为一个复杂的子系统(例如,一个包含大量类的库或框架)提供一个简单的入口时。这是最主要的用途。
  • 降低耦合度:当需要将客户端代码与子系统的具体实现解耦时。未来如果子系统的内部实现发生变化,客户端代码不需要改动,只需要调整外观类即可。
  • 分层结构设计:在分层架构中,可以使用外观模式来定义每层之间的入口点。例如,表现层(UI)通过一个外观接口来调用业务逻辑层,业务逻辑层通过一个外观接口来调用数据访问层。这样可以减少层与层之间的依赖。
  • 包装遗留代码:当需要将一套设计混乱、难以使用的遗留代码封装起来,提供一套清晰的API时。

优点:

  • 简化使用:客户端无需了解子系统的复杂内部结构,使得子系统更易于使用。
  • 松散耦合:实现了客户端与子系统之间的解耦,提高了系统的灵活性和可维护性。
  • 更好的分层:有助于建立良好的系统分层结构。

缺点:

  • 可能不符合开闭原则:当子系统的功能需要增加时,可能需要修改外观类的代码。
  • 可能变成“上帝类”:如果一个外观类承担了过多的职责,关联了过多的子系统类,它可能会变成一个巨大的、难以维护的“上帝类”(God Object)。
  • 不限制直接访问:外观模式提供了简化的接口,但并没有限制客户端在需要时直接访问子系统类。这既是灵活性,也可能破坏封装。

享元模式 (Flyweight Pattern)

核心思想: 运用共享技术来有效地支持大量细粒度的对象。其本质是分离并共享对象的内部状态(Intrinsic State),同时将外部状态(Extrinsic State)作为方法参数传递,从而减少了系统中对象的总数。 享元模式是一种结构型设计模式,它的核心目标是通过共享对象来最大限度地减少内存使用。当一个应用程序需要创建大量相似的对象时,这个模式能带来巨大的性能提升。

适用场景: 当你发现一个系统中存在大量相似或重复的对象,而这些对象造成了极大的内存开销时,就应该考虑使用享元模式。具体条件如下:

  • 一个应用程序使用了大量的对象。
  • 大部分对象的状态都可以被外部化,提取为外部状态。
  • 移除外部状态后,可以用相对较少的共享对象取代大量对象。
  • 经典案例
    • 数据库连接池/线程池:池中的每一个连接/线程都是可被共享的享元对象。
    • 文本编辑器:一个文档中可能会有成千上万个字符,但字符的种类(a, b, c...)是有限的。每个字符(如'A')可以设计成一个享元对象,其内部状态是字符编码,外部状态是它在文档中的位置、字体、颜色等。
    • 图形应用和游戏:在游戏中渲染成千上万棵树或粒子效果。树的模型和贴图是内部状态,可以共享;而每棵树的位置、大小、朝向是外部状态。

优点:

  • 大幅减少内存占用:通过共享对象,极大地减少了系统中对象的创建数量,从而降低了内存消耗。
  • 提高性能:减少了对象创建和垃圾回收的开销。

缺点:

  • 增加了系统的复杂性:需要分离内部和外部状态,并引入工厂类来管理,这使得系统的逻辑变得更加复杂。
  • 外部状态管理:将状态外部化,使得客户端需要自行维护外部状态,增加了客户端的负担。
  • 线程安全问题:享元对象是被共享的,因此必须确保其内部状态是不可变的(Immutable),否则在多线程环境下会产生安全问题。

原型模式 (Prototype Pattern)

核心思想: 用一个原型实例指定要创建的对象的种类,然后通过拷贝(克隆)这个原型来创建新的对象。其本质是用克隆的方式代替常规的构造函数来创建对象

原型模式是一种创建型设计模式,它的核心思想是**“通过复制一个现有的实例来创建新的实例”**,而不是通过new关键字来创建。这个被复制的实例就称为“原型”(Prototype)。

适用场景:

  • 创建成本高:当一个对象的创建过程非常耗时或消耗资源时(例如,初始化需要访问数据库、RPC调用或复杂的计算),使用原型模式可以显著提高性能。
  • 状态相似的对象:当需要创建大量相似的对象,它们之间只有少数属性不同时。可以先创建一个原型,然后克隆出多个副本,再对每个副本进行微调。
  • 解耦创建过程:当一个系统需要独立于其产品的创建、构成和表示时。客户端无需知道具体的类名,只需要通过一个原型管理器,根据一个“名字”来获取原型并克隆。
  • 动态加载:当需要实例化的类是在运行时动态指定时,可以方便地通过克隆一个注册过的原型来创建。

优点:

  • 性能优越:直接在内存中进行二进制流的拷贝,比执行new操作和构造函数要快得多,尤其是在创建复杂对象时。
  • 简化创建过程:客户端可以像使用普通对象一样,轻松地复制出新对象,而无需关心复杂的创建细节。
  • 灵活性高:可以动态地添加或删除原型,从而在运行时改变系统的行为。

缺点:

  • 必须实现克隆方法:每个需要被复制的类都必须配备一个clone方法,这对于已有的类进行改造时可能比较麻烦。
  • 深拷贝实现复杂:在实现深拷贝时需要特别小心,如果对象之间存在循环引用,可能会导致克隆失败或栈溢出。

责任链模式 (Chain of Responsibility Pattern)

责任链模式是一种行为型设计模式,它的核心思想是:为了避免请求的发送者和接收者之间的耦合,让多个对象都有机会处理这个请求。它将这些对象连接成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。核心思想: 为请求创建了一个接收者对象的链。请求的发送者和接收者被解耦,请求会沿着链传递,直到链上的某个接收者处理它为止。发送者无需知道是哪个对象最终处理了请求。

适用场景:

  • 当你有多个对象可以处理同一个请求,但具体由哪个对象处理是在运行时动态决定的。
  • 当你想在不明确指定接收者的情况下,向多个对象中的一个提交请求时。
  • 当你需要动态地组织和修改可以处理请求的对象集合时。
  • 经典案例
    • Web框架的中间件(Middleware)或过滤器(Filter):一个HTTP请求在到达最终处理程序之前,会经过一系列中间件,如日志记录、身份验证、数据压缩等。每个中间件就是一个处理者,处理完自己的任务后,将请求传递给下一个。
    • 审批和工作流系统:如请假、报销审批流程,请求根据金额或天数在不同级别的审批人之间传递。
    • GUI中的事件冒泡:在前端开发中,一个元素的点击事件如果没有被自己处理,会“冒泡”到它的父元素,父元素再传给父元素的父元素,形成一条事件处理链。
    • Java异常处理机制try-catch块实际上也类似责任链,一个try块抛出的异常会沿着catch块的链寻找能够处理它的catch块。

优点:

  • 降低耦合度:请求的发送者和接收者完全解耦,发送者不需要知道请求的处理细节和链的结构。
  • 增强了灵活性:可以随时通过改变链中节点的顺序,或者增加、删除节点来动态地修改责任链的结构和功能。
  • 符合单一职责原则:每个具体处理者只需要关注自己感兴趣的请求,职责清晰。

缺点:

  • 不能保证请求被处理:请求可能到达链的末端也未被任何一个处理者处理。
  • 调试不便:如果链条比较长,请求的流转路径可能比较复杂,给调试带来一定的困难。
  • 性能问题:在最坏的情况下,请求需要遍历整条链才能被处理,可能会对性能产生一定影响。

中介者模式 (Mediator Pattern)

中介者模式是一种行为型设计模式,它的核心思想是用一个中介对象来封装一系列对象之间的交互

想象一个场景:在一个房间里有多个人(对象),如果每个人都可以和其他任何人自由交谈,那么整个房间会变得非常嘈杂混乱,关系网也会像一张巨大的蜘蛛网。

中介者模式就像是为这个房间引入了一个“主持人”或“会议主席”(中介者)。规定如下:

  • 任何人(同事 Colleague)想发言,都必须先告诉主持人。
  • 由主持人(中介者 Mediator)来决定把这个信息转达给谁(一个或多个其他同事)。

这样一来,人与人之间不再有直接的联系,他们都只和主持人打交道。原来复杂的“网状”沟通结构,就变成了简单的“星状”结构,大大简化了沟通的管理。

核心思想: 通过引入一个中介者对象,将原来“多对多”的网状交互结构,转变为“一对多”的星状结构。所有相关对象都通过中介者来通信,而不是相互引用,从而降低对象之间的耦合度。

适用场景:

  • 当一组对象以复杂的方式相互交互,导致它们之间存在大量的相互依赖和引用时。
  • 当一个对象的行为依赖于其他多个对象,而你希望将这些复杂的依赖关系解耦,使其更容易复用和维护时。
  • 当你想自定义一个分布在多个类中的行为,但又不想生成大量的子类时。可以将这些行为抽取到中介者中。
  • 经典案例
    • 图形用户界面(GUI)开发:一个复杂的对话框中,各个控件(按钮、文本框、列表框等)就是同事类,而对话框窗口本身就可以作为中介者。当一个控件的状态改变时(例如,勾选一个复选框),它通知对话框(中介者),由对话框来更新其他相关控件的状态(例如,启用某个按钮)。
    • 聊天室程序:聊天室服务器是中介者,所有用户(同事)都将消息发送到服务器,再由服务器广播给其他用户。
    • MVC框架中的Controller有时也扮演中介者的角色,协调ModelView之间的交互。

优点:

  • 降低耦合度:将同事对象之间错综复杂的依赖关系解耦,使得每个同事对象都相对独立。
  • 集中控制交互:将复杂的交互逻辑集中封装在中介者对象中,使得交互行为更容易理解和维护。
  • 符合单一职责原则:各个同事类只负责自己的业务逻辑,交互的逻辑则交给中介者。

缺点:

  • 中介者膨胀问题:如果系统中同事间的交互逻辑非常复杂,所有的逻辑都集中到中介者中,可能会导致中介者类变得异常庞大和复杂,成为难以维护的“上帝类”(God Object)。

装饰器模式 (Decorator Pattern / Wrapper)

装饰器模式,也叫作包装器模式 (Wrapper),它的核心思想是在不改变原有对象结构的基础上,动态地给一个对象添加一些额外的功能核心思想: 动态地给一个对象添加一些额外的职责。就增加功能而言,装饰器模式相比生成子类的方式更为灵活。其本质是在不改变原有对象接口的前提下,通过“包装”一层来扩展功能

适用场景:

  • 当需要在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责时。
  • 当不能采用继承的方式来扩展功能时(例如,类被final修饰),或者因为继承会导致子类数量爆炸式增长时。
  • 经典案例
    • Java的I/O流java.io包是装饰器模式的完美体现。FileInputStream是原始的字节流(ConcreteComponent),而BufferedInputStreamDataInputStream等都是装饰器(ConcreteDecorator),它们为原始流增加了缓冲、读写基本数据类型等功能。你可以这样层层包裹:new DataInputStream(new BufferedInputStream(new FileInputStream("file.txt")))
    • 图形界面的组件:为一个窗口或文本框添加边框、滚动条等功能。

优点:

  • 高度灵活性:比继承更灵活,可以在运行时动态地添加或删除功能。
  • 符合开闭原则:可以很方便地增加新的装饰器来扩展功能,而无需修改原有的构件类。
  • 避免类爆炸:可以用少量装饰器的不同组合,实现大量的功能扩展,避免了为每种组合都创建一个子类。

缺点:

  • 增加系统复杂性:会产生许多细小的、功能相似的类,如果过度使用,会让系统变得复杂,排查问题也可能变得困难。
  • 多层装饰的性能:多层装饰会增加方法调用的次数,可能会对性能有轻微影响。
  • 对象身份问题:装饰后的对象和原始对象在equals()比较时是不相等的,如果程序依赖于对象身份(==),可能会出现问题。

状态模式 (State Pattern)

核心思想: 允许一个对象在其内部状态改变时改变它的行为。其本质是将与特定状态相关的行为封装到状态对象中,并通过委托来消除if-elseswitch等条件分支

状态模式是一种行为型设计模式,它的核心思想是**“允许一个对象在其内部状态改变时,改变它的行为,看起来就像是修改了它的类”**。

适用场景:

  • 当一个对象的行为依赖于其状态,并且必须在运行时根据状态改变其行为时。
  • 当代码中包含大量与对象状态有关的条件语句(if-elseswitch)时。状态模式可以将每个条件分支的逻辑放入一个独立的类中,使代码结构更清晰。
  • **有限状态机(Finite-State Machine, FSM)**的实现:状态模式是实现状态机的一种非常优雅的方式。
  • 经典案例
    • 订单处理:一个订单有“待支付”、“已支付”、“已发货”、“已完成”、“已取消”等状态,在不同状态下,能对订单进行的操作是不同的。
    • 网络连接:TCP连接有“建立连接中”、“已连接”、“已关闭”等状态。
    • 工作流引擎:流程中的一个任务在不同审批节点(状态)下,有不同的处理逻辑和流转规则。
    • 游戏角色的状态:游戏角色可能有“正常”、“中毒”、“眩晕”、“无敌”等状态,不同状态下角色的行为(如攻击、移动)会受到不同影响。

优点:

  • 结构清晰:将与特定状态相关的代码集中到各自的状态类中,使得代码的结构和职责非常清晰。
  • 符合开闭原则:添加新的状态非常容易,只需要增加一个新的具体状态类即可,无需修改现有代码。
  • 简化了Context类Context类无需处理复杂的条件判断,只需负责委托和状态切换,逻辑大大简化。

缺点:

  • 类和对象数量增多:如果状态非常多,会导致系统中类的数量增加。
  • 系统结构和实现都更为复杂:对于只有几个简单状态的场景,直接使用if-else可能更直观、更简单。

组合模式 (Composite Pattern)

组合模式是一种结构型设计模式,它的核心思想是**“将对象组合成树形结构,以表示‘部分-整体’的层次关系”。最关键的是,它使得客户端可以统一地**对待单个对象(叶子)和对象的组合(容器)。

我们用一个最常见的例子来理解:文件系统

核心思想: 将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象(叶子)和组合对象(树枝)的使用具有一致性。

适用场景: 当你发现你的代码中需要处理一个树形结构,并且希望能够统一地、递归地处理这个结构中的所有对象时,就应该考虑使用组合模式。

  • 表示对象的“部分-整体”层次结构
  • 希望客户端代码可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象
  • 经典案例
    • 文件系统:文件夹(Composite)可以包含文件(Leaf)和其他文件夹。
    • 图形用户界面(GUI):一个窗口(Composite)可以包含面板(Composite),面板又可以包含按钮、文本框等(Leaf)。draw()操作可以被统一调用。
    • 公司组织架构:一个公司(Composite)下有多个部门(Composite),部门下有多个员工(Leaf)。
    • 菜单系统:菜单(Composite)可以包含菜单项(Leaf)和子菜单(Composite)。

优点:

  • 简化客户端代码:客户端可以像处理单个对象一样处理对象组合,无需编写复杂的if-else来区分。
  • 易于扩展:可以很方便地增加新的叶子节点或树枝节点,而无需修改现有代码,符合开闭原则。
  • 结构清晰:可以清晰地定义包含基本对象和组合对象的层次结构。

缺点:

  • 设计变得更通用:在“透明方式”下,为叶子节点实现add/remove等方法是没有意义的,这可能会让设计变得不够安全。
  • 实现相对复杂:在某些情况下,可能需要对组合中的组件类型加以限制,这会增加实现的复杂度。