一、工厂方法模式

1.1 工厂方法模式介绍

工厂方法模式(Factory Method Pattern)是一种创建型设计模式,旨在定义一个用于创建对象的接口,但将具体的实例化工作放到子类中去完成。换句话说,工厂方法模式通过让子类决定实例化哪个具体类,从而实现了对象创建的灵活性和可扩展性。

它有下列优点:

  1. 解耦代码:客户端无需知道具体的产品类,只需要依赖抽象的工厂接口,降低了代码之间的耦合度。

  2. 提高可扩展性:如果以后需要增加新的产品类型,只需新增相应的工厂类,而无需修改现有代码,符合开闭原则。

  3. 集中管理:产品的创建逻辑集中在工厂中,便于维护和管理。

使用和不使用工厂方法模式的对比:

通过对比可以看出,不使用工厂方法模式时,我们需要在 Logger 类中通过条件判断来处理不同类型的日志记录,这导致了代码耦合度高、违反开闭原则、职责不单等问题。每当需要添加新的日志类型时,都需要修改现有代码,增加了维护成本。

而使用工厂方法模式后,我们将对象的创建与使用分离,降低了代码耦合度。添加新的日志类型时,只需创建新的工厂类,无需修改现有代码,符合开闭原则。同时,通过工厂接口封装了具体日志记录器的创建细节,使代码结构更加清晰,职责更加单一。

1.2 工厂方法模式应用场景

举一些开发中典型的应用场景:

  • 消息通知系统:系统支持短信、邮件、站内信等不同通知方式时,可通过工厂方法屏蔽创建细节,让调用方只负责发消息。

  • 支付系统:用户可能选择支付宝、微信、银行卡等支付方式,工厂方法可以根据用户选择生成对应的支付通道对象,方便扩展新方式。

  • 文件解析模块:上传的文件可能是 Excel、CSV、JSON、XML等不同格式,工厂方法可以根据文件类型创建对应的解析器实例,统一解析入口。

  • 业务规则引擎:当一个系统支持多个业务规则版本(如不同行业、不同客户),可以通过工厂方法创建对应的规则计算器,灵活支持定制化逻辑。

1.3 工厂方法模式基本结构

工厂方法模式具有的角色和职责:

  1. 抽象产品(Product):定义产品的公共接口,是所有具体产品的父类。

  2. 具体产品(ConcreteProduct):实现了抽象产品接口,表示某种具体的产品。

  3. 抽象工厂(Factory):定义了一个返回产品对象的方法(一般是一个抽象方法)

  4. 具体工厂(ConcreteFactory):实现了抽象工厂中的创建产品的方法,生成具体的产品实例。

1.4 工厂方法模式的代码实现

具体实现可访问我的仓库:

learn_design_mode/src/main/java/com/yuan/creational/factory at master · AlexavierSeville/learn_design_mode

1.5 工厂方法模式的优缺点

优点:

  • 解耦了对象创建和使用: 调用方不再关心具体是怎么 new 出来的对象,只负责使用,这样一来,如果后续对象的创建逻辑变了,也不会影响到业务逻辑,代码更稳定、更灵活。

  • 符合开闭原则,易于扩展: 当需要新增一个产品时,只需要新增一个对应的工厂类就行,原来的代码不用动。这对于后期产品种类多、变化快的场景非常友好。

  • 提高了代码的复用性和可维护性: 创建逻辑集中在工厂里,避免了重复造轮子。有什么调整,只需要改一处地方,维护成本低,也更容易统一管理。

缺点:

  • 类的数量会增多: 每新增一种产品,通常就要新增一个对应的工厂类。产品多了之后,工厂类也多,项目结构可能会显得臃肿。

  • 增加了系统复杂度: 相较于直接 new 一个工厂类、一个产品接口这样组合起来的结构,对于小项目或者逻辑比较简单的场景来说,可能会显得有点过于复杂。

  • 有时候可能出现过度设计: 如果产品结构本身很简单,用工厂方法模式反而会让代码变得啰嗦,不如直接用简单工厂来得高效直接。

二、单例模型

单例模式(Singleton Pattern)是一种创建型设计模式,它就是说:一个类在整个应用运行期间,只能有一个实例,而且这个实例对外提供一个全局访问点。换句话说,无论是在哪儿调用这个类拿到的永远都是同一个对象

2.1 为什么要使用单例模式

它有下列优点:

  1. 节省资源: 控制实例数量,避免在高频访问场景中反复创建销毁。

  2. 统一管理: 全局只有一个入口,方便统一初始化和清理,比如统一配置、日志级别控制

  3. 线程安全: 好的单例实现可以天然保证多线程环境下只有一次创建,避免竞态条件。

  4. 与其他模式结合:单例常常作为工厂、抽象工厂等模式的基础组件,为更复杂的结构型、行为型模式提供支持。

为了让大家更好地感受到单例模式的作用,以数据库连接管理器为例,我们需要确保整个应用程序中只有一个数据库连接实例,以避免资源浪费和连接冲突。让我们来看看使用和不使用单例模式的区别:

通过对比可以看出,不使用单例模式时,每次创建 DBconnectionManager 实例都会建立新的数据库连接,这导致了资源浪费、连接对象数过多、管理困难等问题。多个连接实例的存在还可能引发数据一致性问题,增加了系统维护的复杂度。

而使用单例模式后,我们确保了整个应用程序中只有一个数据库连接实例,有效避免了资源浪费。通过私有构造函数和静态获取方法,我们实现了对实例创建的严格控制,保证了连接的唯一性。这种实现方式不仅节省了系统资源,还简化了连接管理,提高了系统的可维护性。

2.2 单例模式的基本结构

在这个类中:

Singleton() 是私有构造方法,防止外部实例化

instance 是私有的静态实例变量

getInstance 是对外暴露的获取实例的静态方法

2.3 单例设计模式的基本要求

单例方法模式的基本特点:

  1. 私有化构造器: 禁止外部直接 new ,这样才能确保外部拿不到新对象。

  2. 持有唯一实例的静态变量: 通常写成 private static singleton instance; 程序启动或首次访问时再创建。

  3. 全局访问点: 提供一个 public static getInstance() 方法,外部就通过这个方法拿到唯一实例。

  4. 线程安全: 在多线程场景下,还得保证并发时也只有一份,常见做法有加锁、双检锁、静态内部类、或者直接用枚举。

2.4 单例模式的应用场景

举一些开发中典型的应用场景:

  • 全局唯一ID生成器: 在电商、内容平台等高并发系统中,需要全局唯一的订单号、用户ID、消息ID等,通常会实现一个雪花算法(Snowflake)或时间戳+随机数生成器,作为单例使用,保证线程安全和唯一性。

  • 系统配置管理类: 很多应用在启动时会加载一份配置(如系统参数、限流配置、第三方服务地址等),通过单例模式保证在内存中只加载一次,全局共享,避免重复读取和资源浪费。

  • 应用级缓存组件: 例如一些基础数据(地区列表、标签分类、活动配置等)缓存到内存中,需要多个模块共享,通常封装成单例类,提供统一的读写接口,保证数据一致性。

2.5 单例模式的几种实现

具体代码实现请参考:

learn_design_mode/src/main/java/com/yuan/creational/singleton at master · AlexavierSeville/learn_design_mode

  1. 饿汉式(Eager Initialization)
    饿汉式是在类加载阶段就完成实例化,保证从第一次访问该类到程序结束,全局只有这一个实例。它依赖 JVM 的类加载机制来确保线程安全。

  2. 懒汉式(Synchronized Lazy)
    懒汉式在第一次调用 getInstance() 时才创建实例,通过对该方法加锁来保证线程安全,适合对启动性能有要求且实例不一定马上需要的场景。

  3. 双重检查锁定(Double-Checked Locking)
    双重检查锁定结合了懒汉式的延迟加载和饿汉式的高性能,首次创建时加锁,后续访问则跳过同步块,从而减少锁开销。

  4. 静态内部类(Initialization-on-demand Holder)
    利用 JVM 在加载外部类时并不立即加载内部类的特性,将实例的创建延迟到真正访问内部类时。既能延迟加载,又能借助类加载的线程安全特性

  5. 枚举式(Enum Singleton)
    利用 Java 枚举类型的特性,枚举值在类加载时就创建,JVM 保证枚举实例的线程安全和唯一性。同时,枚举对序列化和反射攻击具有天然防护能力。

2.6 单例模式的优缺点

优点:

  • 全局唯一,资源可控: 单例模式可以保证某个类在系统中始终只有一个实例,这对一些需要全局共享资源的场景,比如配置管理器、线程池、缓存等,非常实用,既节省资源又避免冲突

  • 提供统一访问点: 通过统一的 getlnstance0 方法获取实例,不用每次都去 new,代码更简洁,管理也更集中,有时候还能结合懒加载,提升性能。

  • 便于扩展为多线程安全版本: 只要设计得当,比如加锁、使用双重检查或静态内部类方式,都能很好地保证线程安全,让单例在多线程环境下也能稳定运行。

缺点:

  • 不利于测试和扩展: 单例一旦写死了,很难在单元测试时替换成 mock 对象,也不容易在运行时切换成别的实现,不利于灵活扩展和测试隔离。

  • 可能引发隐藏的状态问题: 由于单例在多个地方被共享使用容易导致不小心在一个地方修改了它的状态,影响到其他地方的行为,尤其在多线程环境下更容易出问题。

  • 生命周期不可控: 单例通常在 JVM 生命周期内都存在,不容易销毁,容易导致资源无法及时释放。如果单例中持有了重资源,比如数据库连接或者文件句柄,可能造成内存泄漏风险。

三、抽象工厂模式

抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,说白了,它就是提供一个“超级工厂”的接口,专门用来创建整组互相关联的产品对象,而且不用我们关心这些产品具体是怎么实现的。

3.1 为什么要使用抽象工厂模式

抽象工厂模式的主要目的是为了解决对象创建的复杂性和耦合性问题。当我们有多个系列的产品,每个系列包含多个相互关联的对象时,如果直接在客户端代码中创建这些对象,会导致代码变得难以维护和扩展。抽象工厂模式通过将对象的创建过程抽象化,让客户端无需关心具体的产品实现,只需要通过工厂接口来获取相应的产品对象。这样,客户端代码和产品的具体实现解耦,便于产品的扩展和修改,同时也方便在不改变客户端代码的前提下,轻松切换不同系列的产品,从而提高了系统的灵活性和可维护性。

为了让大家更好地感受到抽象工厂模式的作用,以跨平台U组件库为例,我们需要支持不同操作系统(Windows、Mac)下的按钮、文本框等U组件。让我们来看看使用和不使用抽象工厂模式的区别:

通过对比可以看出,不使用抽象工厂模式时,我们需要在 UIcomponent 类中通过条件判断来创建不同平台的UI组件,这导致了代码耦合度高、违反开闭原则、难以扩展等问题。每当需要添加新的平台或组件类型时,都需要修改现有代码,增加了维护成本。

而使用抽象工厂模式后,我们将产品族的创建与使用分离,降低了代码耦合度。通过抽象工厂接口和具体工厂类,我们可以轻松添加新的平台支持,无需修改现有代码。同时,这种实现方式确保了同一平台下的组件风格一致性,提高了代码的可维护性和可扩展性。

3.2 抽象工厂模式的应用场景

举一些开发中典型的应用场景:

  • 多平台 UI 渲染引擎: 在开发一个支持 Web、Android、小程序等多端渲染的内容管理系统时,可以用抽象工厂为不同平台提供一整套组件(按钮、表单图片组件等),工厂屏蔽平台差异,保持调用方式一致。

  • 多数据库适配场景(逻辑与物理结构差异大): 当一个系统需要支持 MySQL、Oracle、SQL Server 等数据库,且它们不仅语法不同,连分页、存储过程、数据结构等都不统一时,可用抽象工厂提供一整套数据访问组件(分页查询器、SQL拼接器、字段映射器等)。

  • 多种导出格式支持系统: 比如报表系统或试卷系统中,一套业务数据可能要导出为 PDF、Word、Excel、HTML等格式,每种格式对应一组导出组件(布局器、样式渲染器、文件写入器),通过抽象工厂生成完整的导出工具链。

  • 多语言/多地区内容适配: 跨境系统中,不同地区展示的内容、日期格式、币种符号等都不相同,可以通过抽象工厂为每个地区创建一整套"本地化服务组件”,如文本翻译器、格式转换器、金额展示器等。

3.3 抽象工厂模式的基本结构

抽象工厂模式具有的角色和职责:

  1. 抽象工厂(AbstractFactory): 声明一组创建产品的方法。

  2. 具体工厂(ConcreteFactory): 实现创建具体产品对象的方法。

  3. 抽象产品(AbstractProduct): 定义每个产品的公共接口。

  4. 具体产品(ConcreteProduct): 实现具体的产品对象。

  5. 客户端(Client): 只依赖抽象工厂和抽象产品,负责调用工厂去生产对象。

3.4 抽象工厂模式代码实现

以“多平台 UI 渲染”为例,用抽象工厂模式实现一个简单的组件系统。

具体实现请看:

learn_design_mode/src/main/java/com/yuan/creational/abstractFactory at master · AlexavierSeville/learn_design_mode

3.5 简单工厂、工厂方法、抽象工厂的区别

  1. 简单工厂: 通常由一个工厂类负责创建所有产品的实例,缺点是当产品种类增加时,工厂类会变得庞大,且难以扩展。

  2. 工厂方法: 每个具体工厂类负责创建某一类产品,通过继承和多态机制解决了简单工厂类的问题,使得每个工厂只负责一种产品的创建。

  3. 抽象工厂: 进一步扩展了工厂方法模式,不仅提供了创建单一产品的工厂方法,还提供了创建一系列相关产品的工厂方法,确保了不同产品之间的协调性。

3.6 抽象工厂模式的优缺点

优点:

  • 产品族的一致性: 抽象工厂模式可以保证同一个产品族中的组件搭配使用时不会出错,比如 UI 组件的样式风格统一,使用体验更协调,避免了"东拼西凑”的情况。

  • 便于切换产品系列: 如果系统支持多种产品系列,比如要支持不同品牌或不同平台的实现,只需要替换一个工厂类,就能完成整套产品的切换,代码改动量小,扩展性好。

  • 封装了对象的创建逻辑: 对象的创建过程都被工厂封装起来了,客户端不用关心具体怎么创建,只要调用对应的工厂方法就行,降低了使用门槛,也让代码更整洁。

缺点:

  • 扩展产品族比较困难: 一旦产品族的结构确定下来,比如工厂里要创建哪些产品是固定的,后期要再加一个新产品(比如再加一个新控件),就得改所有的工厂实现类,违反了开闭原则。

  • 类的数量会变多: 每个产品系列都要对应一个具体工厂类,加上每个产品本身也要有接口和实现,项目结构会变得比较庞大,对于简单项目来说可能有些“杀鸡用牛刀”。

  • 增加了系统的抽象层级: 虽然提高了灵活性,但也让代码结构变得更抽象,理解成本提升,对于新手或者不熟悉该模式的人来说,可能会觉得不太直观。

四、建造者模式

建造者模式(Builder Pattern)是一种创建型设计模式,它的核心思想是:把一个复杂对象的创建过程拆解成多个小步骤,然后一步步构建出来。这个过程中,客户端只需要告诉系统“我想要什么”,而不用关心”怎么一步步造出来的”。

用更通俗点的话说,这模式就像组装一台电脑:你告诉装机师傅“我要高性能的游戏主机”,然后他一步步帮你装好电源、主板、显卡...最后你只管坐下来打游戏,不用操心每个零件怎么配、线怎么插。

4.1 为什么要使用建造者模式?

  1. 对象构建过程复杂: 如果需要创建的对象有很多步骤或者很多参数,直接在构造函数或者其他方法中传递这些信息会变得复杂难懂,而建造者模式能把这些步分开处理,使得对象的构建过程更加清晰。

  2. 需要灵活的对象构建方式: 当我们需要根据不同的需求构建相似的对象时,建造者模式允许我们通过改变建造过程中的某些步骤来创建不同的对象,而不需要重新编写整个对象的构造代码。

  3. 构建过程和表示分离: 建造者模式可以把构建过程和对象的表示(即结果)分离开来,使得同一个构建过程能够创建出不同表示的对象。

为了让大家更好地感受到建造者模式的作用,以电脑组装为例,我们需要创建不同配置的电脑(游戏电脑、办公电脑等)。让我们来看看使用和不使用建造者模式的区别:

通过对比可以看出,不使用建造者模式时,我们需要在创建computer 对象时需要手动一个一个 set ,这导致了代码可读性差、不够直观。当需要创建不同配置的电脑时,代码会变得冗长且难以维护。

而使用建造者模式后,我们可以通过链式调用逐步设置电脑的各个组件使代码更加清晰易读。建造者模式将对象的构建过程与表示分离,使得创建过程更加灵活,也可以轻松处理可选参数,并且能够确保对象的完整性。这种实现方式不仅提高了代码的可维护性,还使得创建不同配置的电脑变得更加简单和直观。

4.2 建造者模式的应用场景

举一些开发中典型的应用场景:

  • 复杂表单或请求对象的构建: 如创建一个包含多个可选字段的用户注册请求、订单提交请求、报表筛选条件等,可以用建造者模式灵活拼接参数,避免构造方法过长或参数顺序混乱。

  • 导出文件内容构建: 例如生成复杂的 PDF 报告、Word 文件或 Excel报表,往往需要一步步构建文档结构(页眉、表格、图片、段落等),使用建造者模式可以分步骤构造并保证内容完整性。业务流水记录。

  • 对象创建: 例如在支付系统或交易系统中,流水日志往往由多个字段拼成(请求来源、时间戳、交易码、响应内容、错误栈等),用建造者模式可以按需构造,避免创建臃肿的构造器或 set 方法杂乱调用。

  • 消息推送内容构建器: 推送系统中,不同渠道(短信、微信、邮件)消息格式各异,有标题、正文、按钮、跳转链接等字段,通过建造者统一构造内容体,保持代码清晰灵活。

4.3 建造者模式的基本结构

建造者模式具有的角色和职责:

  1. 产品类(Product): 要构建的复杂对象,包含多个组成部分。

  2. 抽象建造者类(Builder): 定义构建产品各个部分的抽象方法,以及返回最终产品的方法。

  3. 具体建造者类(ConcreteBuilder): 实现Builder接口,具体负责各个部分的构建细节,并最终组装出完整产品。

  4. 指挥者类(Director): 统一指挥建造者按照一定步骤来构建产品,屏蔽了构建过程的细节。

  5. 客户端类(Client): 发起建造请求,选择具体的建造者并使用指挥者来完成产品的创建。

4.4 建造者模式的实现

用建造者模式模拟一下“电脑组装”的过程,来看下这个模式在实际场景中是怎么运作的。

具体实现请看:

learn_design_mode/src/main/java/com/yuan/creational/builder at master · AlexavierSeville/learn_design_mode

4.5 建造者模式的优缺点

优点:

  • 结构清晰、过程可控: 建造者模式把复杂对象的构建过程拆成一个个明确的步骤,让整个构建流程变得清晰、稳定,而且每一步都可以单独控制,方便管理和调整。

  • 便于构建”不同版本”的对象: 通过不同的建造者,可以轻松地创建出结构类似但配置不同的对象,特别适合那种“定制化“很强的业务场景,比如生成不同类型的报表、构造不同风格的 UI 等。

  • 代码更易维护和扩展: 把构建逻辑从产品本身抽离出来,符合单一职责原则,既减少了耦合,也让代码更容易维护。如果后续要新增构建步骤或者替换某个细节,实现起来也很自然。

缺点:

  • 增加了类的数量:为了实现建造者模式,需要引入额外的建造者类和指挥者类,类的数量一下子就上来了。如果对象结构本身不复杂,这种拆分反而会让代码变得繁琐。

  • 不适合构建过程差异太大的对象:建造者模式适合用在“有共同构建步骤”的对象上。如果对象之间的构建逻辑完全不一样,硬套建造者反而会让代码变得臃肿、不灵活。

五、原型模式

原型模式(Prototype Pattern)是一种创建型设计模式,主要用于通过复制现有的对象来创建新对象,而不是通过“new“关键字来直接实例化。简而言之,原型模式让我们能够在已有对象的基础上创建新对象。它依赖于“克隆”已有对象的状态,从而减少了重复构建相同对象的成本。

5.1 为什么要使用原型模式

它有下列优点:

  1. 对象创建过程开销较大: 当对象创建的过程比较复杂或者需要消耗较多资源时直接复制现有的对象可能会比重新构建对象更高效。

  2. 需要大量相似的对象: 如果我们要创建大量结构相似、但又不完全相同的对象那么通过克隆一个模板对象来创建新对象,会比每次都手动设置每个属性要简单许多。

  3. 对象的状态是变化的: 当对象的状态不止一次构建就能完成,并且在多次使用后可能会有不同的变化时,使用原型模式可以帮助我们根据现有对象状态来快速创建新对象。

为了让大家更好地感受到原型模式的作用,以文档编辑系统为例,我们需要创建文档的副本,并允许用户对副本进行修改而不影响原文档。让我们来看看使用和不使用原型模式的区别:

通过对比可以看出,不使用原型模式时,我们需要手动实现复制方法,为每个属性创建新的实例,这导致了代码冗长、容易出错、维护困难等问题。当类的属性发生变化时需要同步修改复制方法,增加了开发成本。

而使用原型模式后,我们通过实现Cloneable接口和实现自定义的cloneDocument 方法,可以轻松创建对象的副本。原型模式将对象的复制过程封装在类内部,使得复制操作更加简单和可靠。这种实现方式不仅减少了代码量,还提高了代码的可维护性,使得创建对象副本变得更加高效和优雅。

5.1 原型模式的应用场景

举一些开发中典型的应用场景:

  • 图形编辑软件中的形状复制:在图形编辑系统(如绘图软件、CAD 系统)中,用户常常需要复制图形(圆形、矩形、多边形等)。每种图形可能有许多相似的属性,使用原型模式可以通过克隆一个现有的对象来快速创建一个新的对象,而不是重新初始化对象。

  • 配置对象复制:例如系统的初始化配置、模板文件、配置信息等,这些通常是对象的集合(如数据库连接池配置、缓存配置等),如果某个配置在程序中有多个实例,原型模式可以通过克隆来快速生成新的配置对象,避免重复创建和设置。

  • 缓存对象克隆:在缓存系统中,尤其是当缓存对象需要被更新并生成新对象时,使用原型模式可以避免重复创建对象,直接从缓存中克隆出新对象并进行修改。

  • 文档复制功能:在内容管理系统、企业知识库、在线协作文档平台中,很多时候我们需要基于一份已有文档快速创建一个新文档。这个过程需要复制文档的标题、正文、作者信息、附件、目录结构、版本记录等内容。

5.2 原型模式的基本结构

原型模式具有的角色和职责:

  1. 原型接口(Prototype):声明一个克隆自身的接口,所有具体原型类都需要实现这个接口。

  2. 具体原型类(ConcretePrototype):实现原型接口,定义如何复制自身的对象。

  3. 客户端(client ):通过调用原型对象的克隆方法,来获取新的对象实例。

5.3 原型模式代码实现

下面就以“文档克隆”为例,我们用原型模式实现文档复制功能。

具体代码请看:

learn_design_mode/src/main/java/com/yuan/creational/prototype at master · AlexavierSeville/learn_design_mode

5.4 原型模式的优缺点

优点:

  • 对象创建效率高: 原型模式通过克隆已有的对象来创建新对象,省去了重新 new的过程,尤其适合那种初始化过程比较复杂或者耗资源的对象。复制一份现成的比从头构建快得多。

  • 简化对象创建逻辑: 有时候一个对象的创建逻辑非常复杂,比如需要从数据库加载、远程拉数据、组合多个子对象等等,用原型模式就可以把这些一次性处理好之后通过克降就能直接复用。

  • 便于动态创建新对象: 如果系统需要在运行时灵活生成对象,而这些对象的结构和内容不确定,原型模式提供了很好的解决方案。通过复制已有对象,可以更方便地动态生成所需的新实例。

缺点:

  • 深拷贝比较麻烦: 如果对象内部还有嵌套引用,比如有 List、Map 或其他复杂对象,浅拷贝就不够用了。这时候就得实现深拷贝,而深拷贝的代码写起来往往不太轻松,稍不注意就可能出问题。

  • 对克隆的依赖性强: 原型模式依赖对象实现 clone()方法或者自定义的复制逻辑,如果原型类设计得不合理,比如克隆出来的对象状态不一致,就可能带来隐性bug,尤其是在多人协作开发中更容易出问题。

  • 可能违反封装性: 为了实现克隆,有时候需要暴露一些本来不应该开放的字段或方法,这可能会破坏类本身的封装性,降低系统的健壮性。

本章结束,请看下一章:结构性模式