Java 设计模式实战:用 “奶茶店” 案例讲透工厂模式 + 单例模式
一、工厂模式
(一)简单工厂模式
想象一下,你走进一家奶茶店,想要一杯珍珠奶茶。在简单工厂模式中,奶茶店就像是一个工厂,它负责创建各种奶茶。我们先来定义一个抽象的奶茶类Drink,作为所有具体奶茶类的父类:
TypeScript取消自动换行复制
// 抽象产品:饮料
然后,创建具体的珍珠奶茶类BubbleTea,它实现了Drink接口:
TypeScript取消自动换行复制
// 具体产品:珍珠奶茶
接下来,就是简单工厂类DrinkFactory登场了,它有一个静态方法createDrink,根据传入的类型参数来创建相应的奶茶对象:
TypeScript取消自动换行复制
// 工厂类
在客户端代码中,我们就可以通过DrinkFactory来获取一杯珍珠奶茶,而无需关心奶茶的具体制作过程:
TypeScript取消自动换行复制
// 使用
简单工厂模式的优点在于它将对象的创建逻辑封装在工厂类中,客户端只需关心传入的参数,就能获取到所需的对象,实现了创建和使用的分离。然而,它也存在一些缺点。比如,当我们需要添加新的奶茶品种时,就需要修改DrinkFactory类的createDrink方法,这违背了开闭原则(对扩展开放,对修改关闭)。而且,如果奶茶的种类不断增加,createDrink方法的逻辑会变得越来越复杂,不利于维护。
(二)工厂方法模式
为了解决简单工厂模式的不足,我们引入工厂方法模式。在工厂方法模式中,我们不再由一个单一的工厂类来创建所有的产品,而是为每种产品创建一个对应的工厂类。
首先,定义一个抽象的工厂接口DrinkFactory,它声明了一个创建Drink对象的抽象方法createDrink:
TypeScript取消自动换行复制
// 抽象工厂
interface DrinkFactory {
Drink createDrink();
}
然后,创建珍珠奶茶的具体工厂类BubbleTeaFactory,它实现了DrinkFactory接口,并在createDrink方法中创建BubbleTea对象:
TypeScript取消自动换行复制
// 具体工厂:珍珠奶茶工厂
class BubbleTeaFactory implements DrinkFactory {
@Override
public Drink createDrink() {
return new BubbleTea();
}
}
客户端代码使用时,通过创建BubbleTeaFactory对象,再调用其createDrink方法来获取珍珠奶茶:
TypeScript取消自动换行复制
// 使用
DrinkFactory factory = new BubbleTeaFactory();
Drink tea = factory.createDrink();
tea.make();
工厂方法模式的优点非常明显。当我们要添加新的奶茶品种时,只需要创建一个新的具体产品类和对应的具体工厂类,而无需修改现有的工厂类代码,完全符合开闭原则。同时,每个工厂类只负责创建一种产品,职责更加单一,代码的可维护性和扩展性大大提高。但是,工厂方法模式也带来了一些额外的类,增加了系统的复杂度。如果奶茶的种类非常多,就会导致大量的工厂类出现。
(三)抽象工厂模式
抽象工厂模式是工厂模式的进一步扩展,它用于创建一系列相关或相互依赖的对象。假设我们的奶茶店不仅提供奶茶,还提供小吃,如薯条、鸡翅等。我们可以使用抽象工厂模式来创建奶茶和小吃的组合。
首先,定义抽象的奶茶类Drink和小吃类Snack:
TypeScript取消自动换行复制
// 抽象产品:饮料
interface Drink {
void make();
}
// 抽象产品:小吃
interface Snack {
void make();
}
然后,创建具体的珍珠奶茶类BubbleTea和薯条类FrenchFries,分别实现Drink和Snack接口:
TypeScript取消自动换行复制
// 具体产品:珍珠奶茶
class BubbleTea implements Drink {
@Override
public void make() {
System.out.println("制作珍珠奶茶:泡茶+加珍珠+加糖");
}
}
// 具体产品:薯条
class FrenchFries implements Snack {
@Override
public void make() {
System.out.println("制作薯条:切土豆+油炸");
}
}
接着,定义抽象工厂接口ShopFactory,它包含创建Drink和Snack对象的抽象方法:
TypeScript取消自动换行复制
// 抽象工厂
interface ShopFactory {
Drink createDrink();
Snack createSnack();
}
再创建具体的工厂类BubbleTeaShopFactory,实现ShopFactory接口,创建珍珠奶茶和薯条对象:
TypeScript取消自动换行复制
客户端代码使用时,通过创建BubbleTeaShopFactory对象,调用其方法来获取珍珠奶茶和薯条的组合:
TypeScript取消自动换行复制
// 使用
ShopFactory factory = new BubbleTeaShopFactory();
Drink tea = factory.createDrink();
Snack snack = factory.createSnack();
tea.make();
snack.make();
抽象工厂模式的优势在于它能够创建一系列相关的产品对象,客户端只需要与抽象工厂交互,而无需关心具体产品的创建细节。这使得系统具有更好的可维护性和可扩展性。当我们要添加新的产品组合时,只需要创建新的具体工厂类,实现抽象工厂接口即可。但是,抽象工厂模式也使得系统更加复杂,因为它涉及到多个产品族和多个抽象层次。如果产品的变化频繁,或者产品之间的依赖关系复杂,抽象工厂模式可能会变得难以维护。
二、单例模式
在奶茶店的运营中,我们可能会遇到一些需要全局唯一的资源或对象。比如,奶茶店的库存管理系统,整个系统只需要一个库存实例来管理所有商品的库存信息。这时候,单例模式就派上用场了。
(一)饿汉式单例
饿汉式单例在类加载时就立即创建唯一实例,就像奶茶店一开业就准备好了唯一的库存实例。其实现代码如下:
TypeScript取消自动换行复制
饿汉式单例的优点是实现简单,并且天然具备线程安全性,因为类加载过程由 JVM 保证线程安全。不过,如果这个实例在整个应用生命周期中从未被使用,就会造成内存的浪费,就好比奶茶店准备了一个永远不会用到的库存实例,占用了宝贵的内存空间。
(二)懒汉式单例(线程不安全)
懒汉式单例则恰恰相反,它直到第一次被调用时才创建实例,就像奶茶店在第一次需要管理库存时才创建库存实例,做到了延迟加载,避免了资源浪费。但是,在多线程并发环境下,这种实现方式会出现问题,可能会创建多个实例。其代码如下:
TypeScript取消自动换行复制
例如,当多个线程同时判断instance为空,都进入创建实例的代码块时,就会导致单例的唯一性被破坏。
(三)懒汉式单例(线程安全版)
为了解决懒汉式线程不安全的问题,我们可以在获取实例的方法上添加synchronized关键字,使其变为线程安全的。这就像给获取库存实例的通道加上了一把锁,每次只允许一个线程进入获取实例。代码如下:
TypeScript取消自动换行复制
// 懒汉式单例(线程安全版)
class Inventory {
private static Inventory instance;
private Inventory() {}
public static synchronized Inventory getInstance() {
if (instance == null) {
instance = new Inventory();
}
return instance;
}
}
然而,这种方式带来了性能损耗,因为每次调用getInstance方法都需要进行加锁和解锁操作,即使实例已经创建好了,后续的调用也会受到锁的影响,降低了程序的并发性能。
(四)双重检查锁(DCL)
双重检查锁(DCL)是对线程安全版懒汉式的优化。它通过两次检查实例是否为空,大大减少了不必要的加锁操作。第一次检查在同步代码块外,若实例已存在,直接返回,避免了进入同步代码块的开销;第二次检查在同步代码块内,确保在多线程环境下只有一个线程能创建实例。同时,使用volatile关键字修饰实例变量,保证了变量的可见性和禁止指令重排序,防止在多线程环境下出现空指针异常。代码如下:
TypeScript取消自动换行复制
// 双重检查锁(DCL)单例
class Inventory {
private static volatile Inventory instance;
private Inventory() {}
public static Inventory getInstance() {
if (instance == null) {
synchronized (Inventory.class) {
if (instance == null) {
instance = new Inventory();
}
}
}
return instance;
}
}
(五)静态内部类实现单例
静态内部类实现单例模式巧妙地利用了 Java 的类加载机制。在外部类加载时,静态内部类并不会被加载,只有当第一次调用getInstance方法时,才会触发静态内部类的加载,从而创建唯一的实例。这就实现了延迟加载,同时 JVM 的类加载机制保证了实例创建的线程安全性,不需要额外的同步操作。代码如下:
TypeScript取消自动换行复制
// 静态内部类单例
class Inventory {
private Inventory() {}
private static class InventoryHolder {
private static final Inventory INSTANCE = new Inventory();
}
public static Inventory getInstance() {
return InventoryHolder.INSTANCE;
}
}
(六)枚举实现单例
枚举实现单例模式是一种简洁且强大的方式。枚举类型在 Java 中是天然线程安全的,并且由 JVM 保证其唯一性。它还能防止反射攻击,因为反射无法创建枚举实例。此外,枚举实现的单例支持序列化,在反序列化时不会创建新的实例,确保了单例在序列化和反序列化过程中的一致性。代码如下:
TypeScript取消自动换行复制
// 枚举单例
enum Inventory {
INSTANCE;
// 可以在枚举中添加其他方法和属性
}
在实际应用中,我们可以根据具体的需求选择合适的单例模式实现方式。如果对性能要求较高,并且不需要延迟加载,饿汉式单例是一个不错的选择;如果需要延迟加载,并且在多线程环境下使用,静态内部类或枚举实现的单例模式是比较推荐的。
更多推荐

所有评论(0)