大家好,这里是编程Cookbook,关注公众号「编程Cookbook」,获取更多面试资料。本文是对设计模式中创建模式的详细讲解,共5种,分别是单例模式、工厂模式、抽象工厂模式、建造者模式和原型模式。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
常用创建型模式
单例模式
什么是单例模式?
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例
,并提供一个 全局访问点
来获取该实例。单例模式的核心思想是控制对象的创建过程,避免重复创建对象,从而节省资源并保证一致性。
单例模式的特点
- 唯一性:在程序的整个生命周期里,该类只会有一个实例存在。
- 全局可访问性:提供了一个能让所有代码都可以访问到唯一实例的全局访问点。
- 延迟加载特性:在有需求的时候才去创建实例(比如通过懒汉式来实现这一特性)。
单例模式的使用场景
单例模式适用于以下场景:
- 配置管理:系统中只需要一个全局配置对象,用于统一管理配置信息。
- 日志记录:日志对象通常只需要一个实例,用于记录系统的运行状态。
- 数据库连接池:数据库连接池通常只需要一个实例,用于管理数据库连接。
- 缓存系统:缓存对象只需要一个实例,用于存储和提供缓存数据。
- 线程池:线程池通常只需要一个实例,用于管理线程资源。
单例模式的实现方式
单例模式有多种实现方式,以下是常见的几种:
1. 懒汉式(Lazy Initialization)
- 特点:很懒,在第一次调用时创建实例。
- 优点:延迟加载,节省资源。
- 缺点:线程不安全,需要额外处理多线程问题。
- Go 实现:
type Singleton struct{} var instance *Singleton func GetInstance() *Singleton { if instance == nil { instance = &Singleton{} } return instance }
2. 饿汉式(Eager Initialization)
- 特点:很饿,在类加载时创建实例。
- 优点:线程安全,实现简单。
- 缺点:无论是否使用都会创建实例,可能浪费资源。
- Go 实现:
type Singleton struct{} var instance = &Singleton{} func GetInstance() *Singleton { return instance }
3. 双重检查锁(Double-Checked Locking)
- 特点:在懒汉式的基础上,通过双重检查锁机制保证线程安全。
- 解释:双重检查锁机制——第一次检查实例是否为空,第二次在加锁后再次检查实例是否为空。只在第一次检查实例为空时加锁,提高了性能。避免了每次获取实例都加锁导致的效率低下问题。
- 优点:延迟加载,线程安全,性能较好。
- 缺点:实现稍复杂。
- Go 实现:
import "sync" type Singleton struct{} var instance *Singleton var once sync.Once func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{} }) return instance }
- 第一次检查实例是否为空:在加锁之前,先检查实例是否已经创建。如果实例已经存在,就直接返回该实例;如果第一次检查发现实例为空,就需要创建实例。为了保证在多线程环境下只创建一个实例,需要对创建实例的代码块进行同步处理,即加锁。
- 第二次在加锁后再次检查实例是否为空:在加锁之后,再次检查实例是否为空。这是因为可能存在多个线程同时通过了第一次检查,然后排队等待锁。当第一个线程获得锁并创建了实例后,后续等待的线程获得锁时,如果不进行第二次检查,就会再次创建实例,破坏了单例模式的唯一性。
4. 静态内部类(Static Inner Class)
- 特点:利用类加载机制保证线程安全,同时实现延迟加载。
- 解释:推荐使用。内部类在外部类被加载时不会立即加载,只有在调用内部类时才会加载,实现了懒加载;同时,类加载过程由 JVM 保证线程安全。
- 优点:线程安全,延迟加载,实现简单。
- 缺点:不适用于所有编程语言,如Go没有类,也就没有这个。
- Java 实现:
public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
5. 枚举(Enum)
- 特点:利用枚举的特性实现单例。
- 解释:枚举类型在 Java 中天然是单例的,并且 Java 的枚举机制保证了在序列化和反序列化、反射调用等情况下都能保证单例的正确性。
- 优点:线程安全,防止反射攻击,实现简单。
- 缺点:不适用于所有场景。
- Java 实现:
public enum Singleton { INSTANCE; public void doSomething() { System.out.println("Singleton instance is working."); } }
如何保证线程安全?
- 使用双重检查锁: Go 语言通过
sync.Once
或sync.Mutex
确保实例只被创建一次。 - 使用饿汉式:在类加载时创建实例,天然线程安全。
- 使用枚举:枚举的特性保证了线程安全和实例唯一性。
- 使用静态内部类:利用类加载机制保证线程安全。
例子
- 场景:你需要一个全局的数据库连接池,确保整个应用程序中只存在一个连接池实例。
- 解决方案:使用单例模式,将数据库连接池类设计为单例,确保只有一个实例被创建和共享。
- 应用:适用于资源管理类(如线程池、缓存)、配置文件读取类等。
总结
单例模式的核心是确保一个类只有一个实例,并提供全局访问点。常见的实现方式包括懒汉式、饿汉式、双重检查锁、静态内部类和枚举。为了保证线程安全,可以使用双重检查锁、饿汉式或枚举等方式。单例模式适用于需要全局唯一对象的场景,如配置管理、日志记录、数据库连接池等。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
工厂模式和抽象工厂模式
工厂模式是一组创建型设计模式,用于解耦对象的创建与使用,使代码更具灵活性和可维护性。以下是工厂模式、简单工厂模式和抽象工厂模式的工作原理、特点、使用场景和实现方式,以及它们之间的区别。
1. 简单工厂模式(Simple Factory)
工作原理
- 定义:通过一个
工厂类
,根据传入的参数决定创建哪种产品对象。 - 核心思想:将对象的创建逻辑集中在一个工厂类中。
特点
- 优点:
- 简单易用,适合对象创建逻辑不复杂的场景。
- 客户端无需关心对象的创建细节。
- 缺点:
- 工厂类职责过重,违反单一职责原则。
- 新增产品时需要修改工厂类,违反开闭原则。
使用场景
- 对象创建逻辑简单,且产品种类较少。
- 客户端不需要关心对象的创建过程。
2. 工厂模式(Factory Method)
工作原理
- 定义:定义一个创建对象的
接口
,但让子类决定实例化哪个类。 - 核心思想:将对象的创建延迟到子类。
特点
- 优点:
- 符合开闭原则,新增产品时只需增加新的工厂类。
- 客户端与具体产品解耦。
- 缺点:
- 每增加一个产品,就需要增加一个工厂类,类的数量会增加。
使用场景
- 对象创建逻辑复杂,且需要扩展性。
- 客户端不需要关心具体的产品类。
3. 抽象工厂模式(Abstract Factory)
工作原理
- 定义:提供一个创建
一系列
相关或相互依赖对象的接口
,而无需指定具体类。 - 核心思想:将多个产品的创建集中在一个工厂类中。
特点
- 优点:
- 支持产品族的创建,保证产品之间的兼容性。
- 符合开闭原则,新增产品
族
时只需增加新的工厂类。
- 缺点:
- 类的数量较多,系统复杂度增加。
使用场景
- 需要创建一组相关或依赖的对象。
- 系统需要保证产品之间的兼容性。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
三种模式的区别
特性 | 简单工厂模式 | 工厂模式 | 抽象工厂模式 |
---|---|---|---|
定义 | 一个工厂类 负责创建所有 产品 | 有一个抽象工厂类(父类),定义创建产品的抽象方法;每个具体工厂子类 负责创建一种具体 产品对象 | 有一个抽象工厂类(父类),定义创建产品的抽象方法;每个具体工厂子类 负责创建一组相关 产品对象 |
扩展性 | 较差,新增产品通常需要修改工厂类的创建方法,添加新的判断逻辑 | 较好,新增产品只需增加一个具体工厂子类,实现抽象工厂类的创建方法,无需修改已有工厂类代码 | 较好,新增产品族 只需增加一个具体工厂子类,实现工厂类中创建相关产品的方法,不影响其他代码 |
适用场景 | 产品种类较少,创建逻辑简单,适用于简单场景 | 产品种类较多,创建逻辑复杂,适用于需要扩展性的场 | 需要创建一组相关或依赖的产品,适用于需要创建一组相关产品的场景 |
复杂度 | 简单,工厂类代码相对集中,逻辑较为直接 | 中等,存在抽象工厂类和多个具体工厂子类,代码结构相对复杂一些,但职责明确 | 较高,涉及多个抽象方法和多个相关产品的创建,代码结构和逻辑更复杂 |
是否符合开闭原则 | 不符合,因为新增产品可能需要修改工厂类的已有代码 | 符合,可在不修改已有代码的基础上,通过增加具体工厂子类来扩展新功能 | 符合,新增产品族时不影响已有的工厂类和产品类代码,只需新增具体工厂子类 |
工厂方法模式和抽象工厂模式的一个关键区别就在于创建对象的粒度,前者创建单个具体产品,后者创建一组相关产品
工厂模式例子
- 场景:你正在开发一个游戏,需要创建不同类型的角色(如战士、法师、弓箭手)。
- 解决方案:使用工厂模式,定义一个角色工厂接口,每个具体工厂类负责创建一种角色,每个角色(如战士、法师、弓箭手)都是一个独立的类。
- 应用:适用于对象创建过程复杂,或需要根据不同条件创建不同类型对象的场景。
抽象工厂模式例子
- 场景:你需要开发一个跨平台的 UI 库,支持不同操作系统(如 Windows、Mac)的按钮和文本框。
- 解决方案:使用抽象工厂模式,定义一个 UI 工厂接口,每个具体工厂类负责创建一组相关的 UI 组件(如 Windows 按钮和文本框,Mac 按钮和文本框)。
- 应用:适用于需要创建一组相关产品的场景,如跨平台 UI 库、数据库驱动等。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
建造者模式
建造者模式的定义
建造者模式(Builder Pattern)是一种创建型设计模式,它允许将一个复杂对象的构建
与其表示
分离,使得同样的构建过程可以创建不同的表示。简单来说,就是将一个复杂对象的创建步骤进行拆分,通过不同的组合方式构建出不同配置的对象。
组成成分
建造者模式主要包含以下四个核心组成部分:
- 产品(Product):要创建的复杂对象(最终结果)。
它由多个部分组成,不同的构建方式可能会产生不同配置的产品。例如,在构建电脑的场景中,电脑就是产品,它由 CPU、内存、硬盘、显卡等部件组成。
- 抽象建造者(Abstract Builder):创建产品各个部分的抽象方法。
通常还会有一个返回最终产品的方法。抽象建造者为具体建造者提供了统一的接口,使得具体建造者可以按照规定的步骤来构建产品。
- 具体建造者(Concrete Builder):实现抽象建造者定义的方法,负责完成产品各个部分的具体构建。
不同的具体建造者可以构建出不同配置的产品。比如,有高性能电脑建造者和普通办公电脑建造者,它们会根据不同的需求来选择不同的硬件组件进行构建。
- 指挥者(Director):负责安排复杂对象的建造顺序,调用具体建造者的方法来构建产品。
指挥者并不负责产品的具体构建细节,而是控制构建过程的流程。客户端通常只需要与指挥者交互,由指挥者来协调具体建造者完成产品的构建。
特点
- 封装性好:将产品的构建过程封装在具体建造者中,客户端无需了解产品的具体构建细节,只需要与指挥者交互即可。这样可以降低系统的耦合度,提高代码的可维护性。
- 构建过程灵活:可以通过不同的具体建造者来创建不同配置的产品,并且可以根据需要调整构建步骤的顺序。这使得系统具有很高的灵活性和可扩展性。
- 便于控制构建过程:指挥者负责控制产品的构建顺序,确保产品按照正确的步骤进行构建。这样可以避免因构建顺序错误而导致的问题。
适用场景
- 创建复杂对象:当创建的对象包含多个部分,且构建过程较为复杂时。
使用建造者模式可以将构建过程分解为多个简单的步骤,使代码更易于理解和维护。例如,构建一个包含多个组件的汽车、飞机等复杂产品。
- 对象配置多样化:如果需要创建的对象有多种不同的配置选项.
建造者模式可以方便地实现不同配置的组合。比如,在游戏开发中创建角色,角色可能有不同的外观、技能、装备等配置。
- 构建过程分步进行:当对象的构建过程需要按照一定的顺序进行多个步骤时。
建造者模式可以通过指挥者来控制构建顺序,确保对象的正确构建。例如,在软件开发中创建一个复杂的报表,需要先收集数据、然后进行数据处理、最后进行报表生成等步骤。
例子
- 场景:你需要组装一台电脑,电脑的配置(如 CPU、内存、硬盘)可以根据用户需求变化。
- 解决方案:使用建造者模式,定义一个电脑建造者接口,每个具体建造者类负责组装不同配置的电脑。
- 应用:适用于创建复杂对象,且对象有多个可选部件或步骤的场景,如房屋建造、文档生成等。
非常用创建型模式
原型模式(Prototype Pattern)
定义
原型模式是一种创建型设计模式,它通过复制现有对象来创建新对象,而不是通过调用构造函数。原型模式的核心思想是利用对象的克隆能力,避免重复创建相似对象的开销。
特点
优点
- 简化对象创建:对于复杂的对象,直接克隆比重新构造更简单。
- 动态扩展:可以在运行时动态添加或删除对象的属性。
- 避免构造函数的约束:不需要依赖构造函数,尤其适用于构造函数复杂或不可用的场景。
缺点
- 深拷贝与浅拷贝问题:如果对象包含引用类型的成员变量,需要实现深拷贝,否则克隆对象会共享引用,可能导致意外行为。
- 复杂度增加:需要实现克隆方法,增加了代码的复杂度。
使用场景
-
对象创建成本高:
- 当创建一个对象的成本较高(如需要复杂的初始化操作或依赖外部资源)时,可以通过克隆现有对象来节省资源。
-
需要动态配置对象:
- 当对象的配置需要动态调整,且调整后的对象需要被复用时,可以使用原型模式。
-
避免构造函数的限制:
- 当构造函数不可用或不适合时(如构造函数是私有的),可以通过克隆来创建对象。
-
需要大量相似对象:
- 当系统中需要大量相似对象时,可以通过克隆原型对象来快速创建。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
实现方式
1. 浅拷贝
- 定义:只复制对象的基本类型字段和引用类型字段的地址,不复制引用类型字段的实际内容。
- 适用场景:对象中
没有
引用类型字段,或引用类型字段不需要被复制。
2. 深拷贝
- 定义:复制对象的所有字段,包括引用类型字段的实际内容。
- 适用场景:对象中包含引用类型字段,且需要完全独立的副本。
总结
- 原型模式通过克隆现有对象来创建新对象,避免了重复初始化的开销,适用于对象创建成本高、需要动态配置或需要大量相似对象的场景。
- 实现时需要注意深拷贝与浅拷贝的问题,确保克隆对象的独立性。
- 原型模式是创建型模式中的一种重要模式,能够有效提升系统的性能和灵活性。
例子
- 场景:你正在开发一个图形绘制系统,需要创建大量相似的图形对象(如圆形、矩形)。
- 解决方案:使用原型模式,将图形对象设计为可克隆的原型,通过拷贝原型创建新对象,避免重复初始化。
- 应用:适用于创建成本大(如初始化时间长、占用资源多)的对象,且对象状态初始化后变化不大的场景。
创建型模式对比
模式 | 特点 | 适用场景 |
---|---|---|
单例模式 | 确保一个类只有一个实例,并提供全局访问点。 | 需要全局唯一对象的场景。 |
工厂模式 | 通过工厂类创建对象,解耦对象的创建与使用。 | 需要灵活创建对象,且对象类型较多。 |
建造者模式 | 将一个复杂对象的构建与其表示分离,适用于对象的构造过程复杂且需要灵活配置的场景。 | 需要构建复杂对象,且构造过程灵活。 |
原型模式 | 通过克隆现有对象创建新对象,避免重复初始化。 | 对象创建成本高,需要动态配置对象。 |
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!