Skip to content

设计模式面试题

更新: 8/14/2025 字数: 0 字 时长: 0 分钟

单例模式有哪几种实现?如何保证线程安全?

想象一下,你家里只有一间茅厕(厕所),每次只能一个人进去用。当有人想用的时候,就得去排队,或者看看里面有没有人。用完了,门开着,下一个人才能进去。

单例模式就是这个意思:保证一个类在整个程序运行过程中,只有一个实例(对象)存在,而且提供一个公共的、方便的方法让大家都能访问到这个唯一的实例。

为啥要这样?比如,你家就一个管家,所有需要他做的事情都找他。或者说,程序里有一个配置中心,所有地方都得从这同一个地方读取配置。

有哪几种实现?

  1. 饿汉式(Eager Initialization):

    • 比喻:就像你还没饿,但妈已经把饭做好了放在桌上。等你一说“饿了”,饭马上就能吃。

    • 怎么实现:在类加载的时候,就直接把唯一的实例创建好。

    • 代码大概长这样:

      java
      public class Singleton {
          private static Singleton instance = new Singleton(); // 类加载时就创建了
      
          private Singleton() {} // 构造器私有,外面不能随便 new
      
          public static Singleton getInstance() { // 提供一个公共方法获取实例
              return instance;
          }
      }
    • 优点:简单粗暴,线程安全(因为类加载时就创建了,后面不会再有并发问题)。

    • 缺点:可能会提前创建实例,如果这个实例很占资源,但又一直没用到,就有点浪费。

  2. 懒汉式(Lazy Initialization):

    • 比喻:你说“饿了”,妈才开始给你做饭。如果一直没说饿,饭就一直不烧。

    • 怎么实现:第一次需要用到这个实例的时候,才去创建它。

    • 代码大概长这样(初始版本,有线程安全问题):

      java
      public class Singleton {
          private static Singleton instance; // 默认是 null
      
          private Singleton() {}
      
          public static Singleton getInstance() {
              if (instance == null) { // 第一次用才创建
                  instance = new Singleton();
              }
              return instance;
          }
      }
    • 优点:真正需要的时候才创建,节省资源。

    • 缺点有线程安全问题!(看下面怎么解决)

  3. 双重检查锁定(Double-Checked Locking - DCL):

    • 比喻:你说“饿了”,妈先看冰箱有没有菜。没菜才去买菜做饭,而且为了避免你和弟弟同时说饿都跑去买菜,她会锁上门(同步),一个人去买。买完菜回来,再看看冰箱里是不是已经有菜了(双重检查),确认没有重复买。

    • 怎么实现:在懒汉式的基础上,加锁来保证线程安全,并且进行两次 null 检查,提高性能。

    • 代码大概长这样:

      java
      public class Singleton {
          private volatile static Singleton instance; // 注意这里的 volatile 关键字
      
          private Singleton() {}
      
          public static Singleton getInstance() {
              if (instance == null) { // 第一次检查,如果已经创建了,就直接返回,避免进入同步块
                  synchronized (Singleton.class) { // 进入同步块,保证同一时间只有一个线程执行
                      if (instance == null) { // 第二次检查,防止多个线程同时通过第一次检查,导致重复创建
                          instance = new Singleton();
                      }
                  }
              }
              return instance;
          }
      }
    • 优点:既实现了懒加载,又保证了线程安全,而且性能较好(大部分时间不会进入同步块)。

    • 缺点:实现相对复杂一点点,volatile 关键字很重要,不加可能会有问题(指令重排)。

  4. 静态内部类(Static Inner Class):

    • 比喻:妈把饭店的菜单放在一个小抽屉里,只有你真正想点菜的时候,才去打开抽屉,拿出菜单。这个小抽屉(静态内部类)只有在第一次被打开(使用)时,里面的菜(单例实例)才会被“准备好”。

    • 怎么实现:利用了类加载机制。静态内部类只有在被首次使用时才会被加载,它的静态成员(单例实例)也会随之被初始化。

    • 代码大概长这样:

      java
      public class Singleton {
          private Singleton() {}
      
          // 静态内部类,只有在第一次调用 getInstance() 时才会被加载
          private static class SingletonHolder {
              private static final Singleton INSTANCE = new Singleton();
          }
      
          public static Singleton getInstance() {
              return SingletonHolder.INSTANCE;
          }
      }
    • 优点:完美结合了懒加载和线程安全,代码也比较简洁,是推荐的实现方式之一。

  5. 枚举(Enum):

    • 比喻:枚举天生就是独一无二的,就像你家的门牌号,只有一个,而且是系统自动保证的。

    • 怎么实现:直接定义一个枚举类型,里面就一个枚举值。

    • 代码大概长这样:

      java
      public enum Singleton {
          INSTANCE; // 唯一的实例
      
          public void doSomething() {
              // 做一些事情
          }
      }
      // 使用:Singleton.INSTANCE.doSomething();
    • 优点:最简洁、最推荐的方式!不仅线程安全,还能有效防止反射攻击和序列化问题,简直是“万能解药”。

如何保证线程安全?

  1. 饿汉式:简单粗暴,因为实例在类加载时就已经创建,JVM 会保证这个过程的线程安全。
  2. 懒汉式(非线程安全):存在问题,多个线程可能同时判断 instance == null 为真,导致创建多个实例。
  3. 懒汉式(加同步锁):在 getInstance() 方法上加 synchronized 关键字,但性能较差,因为每次调用都会加锁。
  4. 双重检查锁定(DCL)synchronized 块只在第一次创建实例时才有效,大部分时间不加锁,配合 volatile 关键字防止指令重排,兼顾性能和安全。
  5. 静态内部类:利用 JVM 类加载机制的线程安全性。一个类在首次加载时只会执行一次初始化,这个过程是线程安全的。
  6. 枚举:JVM 从语言层面就保证了枚举实例的唯一性,天生线程安全,且能避免反射和序列化问题。
什么是策略模式?一般用在什么场景?

想象一下,你要去上班,可以坐地铁、骑共享单车、打的,或者走路。每种方式都代表一种“策略”,它们都能把你送到公司,但具体怎么实现(坐地铁怎么买票,骑车怎么扫码)是不同的。你可以根据当天的天气、时间、心情来选择用哪种方式。

策略模式就是这个意思:定义一系列的算法(策略),将每一个算法封装起来,并使它们可以相互替换。客户端(你)可以根据需要选择不同的策略,而不需要改变使用策略的代码。

它解决的问题是:当一个操作有多种实现方式,并且这些方式在运行时需要灵活切换时。

一般用在哪些场景?

  1. 支付方式选择:

    • 比如电商网站,用户可以选择支付宝支付、微信支付、银联支付、银行卡支付等。每种支付方式就是一种策略。
    • 代码里会有一个“支付上下文”,你告诉它用支付宝,它就知道怎么调用支付宝接口;告诉它用微信,它就知道怎么调用微信接口。
  2. 不同排序算法:

    • 比如对一堆数据进行排序,你可以选择冒泡排序、快速排序、插入排序等等。每种排序算法就是一种策略。
    • 你可以写一个排序工具类,传入不同的“排序策略”,它就能用不同的方法给你排序。
  3. 促销活动计算:

    • 比如购物满减、打折、买一送一、积分抵扣等,每种促销规则都是一种策略。
    • 结算时,根据用户符合的条件,选择对应的策略来计算最终价格。
  4. 文件导出/导入格式:

    • 导出数据可以选择导出为 CSV、Excel、PDF 等格式,每种格式对应一种导出策略。
    • 导入数据可以选择从不同格式的文件导入,每种格式对应一种导入策略。
  5. 不同税率计算:

    • 针对不同地区或不同商品类型,税率计算方式不同,每种计算方式是一种策略。

核心思想:把“变”的部分(具体的算法实现)封装起来,把“不变”的部分(调用算法的接口)固定下来。这样,当你需要增加新的算法时,只需要增加一个新的策略类,而不需要修改原有的代码。这符合“开闭原则”(对扩展开放,对修改关闭)。

示例代码:

我们以“计算器操作”为例,实现加法、减法、乘法等不同的计算策略。

  1. 策略接口(Strategy Interface)

    定义一个公共的接口,所有的具体策略都必须实现这个接口。

    java
    // Step 1: 定义策略接口
    public interface OperationStrategy {
        int doOperation(int num1, int num2);
    }
  2. 具体策略类(Concrete Strategy Classes)

    实现策略接口,提供具体的算法或行为。

    java
    // Step 2: 实现具体的策略类 - 加法
    public class AddOperation implements OperationStrategy {
        @Override
        public int doOperation(int num1, int num2) {
            return num1 + num2;
        }
    }
    
    // Step 2: 实现具体的策略类 - 减法
    public class SubtractOperation implements OperationStrategy {
        @Override
        public int doOperation(int num1, int num2) {
            return num1 - num2;
        }
    }
    
    // Step 2: 实现具体的策略类 - 乘法
    public class MultiplyOperation implements OperationStrategy {
        @Override
        public int doOperation(int num1, int num2) {
            return num1 * num2;
        }
    }
  3. 上下文类(Context Class)

    持有策略接口的引用,客户端通过上下文类来使用具体的策略。

    java
    // Step 3: 创建上下文类
    public class CalculatorContext {
        private OperationStrategy strategy;
    
        public CalculatorContext(OperationStrategy strategy) {
            this.strategy = strategy;
        }
    
        public int executeOperation(int num1, int num2) {
            return strategy.doOperation(num1, num2);
        }
    
        // 也可以提供一个方法来动态改变策略
        public void setStrategy(OperationStrategy strategy) {
            this.strategy = strategy;
        }
    }
  4. 客户端(Client)

    使用上下文类和具体的策略来执行操作。

    java
    // Step 4: 客户端代码
    public class StrategyPatternDemo {
        public static void main(String[] args) {
            // 使用加法策略
            CalculatorContext context = new CalculatorContext(new AddOperation());
            System.out.println("10 + 5 = " + context.executeOperation(10, 5)); // 输出:10 + 5 = 15
    
            // 切换到减法策略
            context.setStrategy(new SubtractOperation()); // 或者直接 new CalculatorContext(new SubtractOperation())
            System.out.println("10 - 5 = " + context.executeOperation(10, 5)); // 输出:10 - 5 = 5
    
            // 切换到乘法策略
            context.setStrategy(new MultiplyOperation());
            System.out.println("10 * 5 = " + context.executeOperation(10, 5)); // 输出:10 * 5 = 50
    
            // 也可以直接创建新的上下文
            CalculatorContext multiplyContext = new CalculatorContext(new MultiplyOperation());
            System.out.println("20 * 3 = " + multiplyContext.executeOperation(20, 3)); // 输出:20 * 3 = 60
        }
    }

代码解释:

  • OperationStrategy 是策略接口,定义了所有计算操作的共同行为。
  • AddOperationSubtractOperationMultiplyOperation 是具体策略,分别实现了加、减、乘的具体算法。
  • CalculatorContext 是上下文类,它持有一个 OperationStrategy 对象的引用。客户端通过 CalculatorContext 来执行操作,而不需要知道具体的策略类是哪个,实现了策略与客户端的解耦。
  • 客户端可以根据需要,传入不同的策略对象给 CalculatorContext,从而在运行时改变其行为。
什么是模板方法模式?一般用在什么场景?

想象一下,你有一张做菜的通用食谱。比如“做一道家常菜”:

  1. 准备食材
  2. 切菜
  3. 下锅炒
  4. 调味出锅

这个流程是固定的,但具体到“准备什么食材”、“怎么切”、“炒什么菜”、“放什么调料”,每个人家可以有不同的做法。比如,做番茄炒蛋,准备番茄鸡蛋;做青椒肉丝,准备青椒肉。

模板方法模式就是这个意思:在一个抽象类中定义一个操作中的算法骨架(模板),而将一些步骤延迟到子类中去实现。这使得子类可以在不改变算法结构的情况下,重新定义该算法的某些特定步骤。

它解决的问题是:当多个类有共同的算法骨架,但其中某些步骤的实现方式不同时。

一般用在哪些场景?

  1. 通用算法框架:

    • 比如一个框架,定义了数据处理的通用流程:读取数据 -> 预处理 -> 核心业务逻辑 -> 保存结果
    • “读取数据”和“保存结果”可能是固定的(比如都从数据库读写),但“预处理”和“核心业务逻辑”可以由不同的子类来实现(比如一个子类做数据清洗,另一个做数据加密;一个子类是报表生成,另一个是数据分析)。
  2. 报表生成:

    • 生成报表通常有固定步骤:连接数据库 -> 查询数据 -> 格式化数据 -> 输出报表
    • 但不同的报表(比如销售报表、财务报表)在“查询数据”和“格式化数据”上会有差异,这些差异的部分就可以由子类去实现。
  3. 算法骨架,具体实现交给子类:

    • 比如一个游戏,定义了“玩游戏”的通用步骤:开始游戏 -> 进行中 -> 结束游戏
    • 但“进行中”的具体玩法(比如玩象棋、玩扑克)可以由不同的子类来实现。
  4. Web 开发中的请求处理:

    • 比如一个 Spring MVC 控制器,它的请求处理流程可以看作是模板方法:接收请求 -> 参数绑定 -> 业务逻辑处理 -> 视图渲染
    • 其中“业务逻辑处理”就是留给开发者实现的“具体步骤”。
  5. 构建工具的编译/打包流程:

    • 比如 Maven 或 Gradle,它们定义了构建项目的标准生命周期(validate -> compile -> test -> package -> install -> deploy)。
    • 这些阶段是固定的,但每个阶段的具体执行(比如用什么编译器编译、如何打包)可以有不同的插件(子类)来实现。

核心思想:封装“不变”的部分(算法骨架),把“可变”的部分(特定步骤的实现)留给子类。这样,当你需要改变某个步骤的实现时,只需要修改或创建新的子类,而不需要动到算法的整体结构。这也符合“开闭原则”。

示例代码:

我们以“构建房子”为例,房子建造有固定的步骤,但不同类型的房子(木屋、砖房)在某些步骤的实现上有所不同。

  1. 抽象模板类(Abstract Template Class)

    定义算法的骨架(模板方法),并声明一些抽象方法,供子类实现。也可以包含一些具体方法和钩子方法。

    java
    // Step 1: 定义抽象模板类
    public abstract class HouseBuilder {
    
        // 模板方法:定义了建造房子的通用算法骨架
        public final void buildHouse() { // 使用 final 关键字防止子类修改模板方法的执行顺序
            layFoundation();      // 步骤 1: 打地基 (具体方法)
            buildWalls();         // 步骤 2: 建造墙壁 (抽象方法,子类实现)
            buildRoof();          // 步骤 3: 建造屋顶 (抽象方法,子类实现)
            decorateHouse();      // 步骤 4: 装修房子 (钩子方法,子类可选实现)
            System.out.println("房子建造完成!");
            System.out.println("--------------------");
        }
    
        // 具体方法:所有房子都一样,无需子类修改
        private void layFoundation() {
            System.out.println("打地基:挖掘地基,浇筑混凝土。");
        }
    
        // 抽象方法:留给子类实现,不同类型的房子墙壁建造方式不同
        protected abstract void buildWalls();
    
        // 抽象方法:留给子类实现,不同类型的房子屋顶建造方式不同
        protected abstract void buildRoof();
    
        // 钩子方法:子类可以选择是否实现(重写),提供默认的空实现或通用实现
        // 比如:不是所有房子都需要装修,或者默认是简单装修
        protected void decorateHouse() {
            System.out.println("装修房子:进行基础装修。");
        }
    }
  2. 具体模板实现类(Concrete Template Classes)

    实现抽象类中定义的抽象方法,完成算法的具体步骤。

    java
    // Step 2: 实现具体的模板子类 - 木屋
    public class WoodenHouseBuilder extends HouseBuilder {
        @Override
        protected void buildWalls() {
            System.out.println("建造墙壁:使用木头和木板搭建墙壁。");
        }
    
        @Override
        protected void buildRoof() {
            System.out.println("建造屋顶:铺设木质瓦片屋顶。");
        }
    
        // 可以选择性地重写钩子方法,进行特定的装修
        @Override
        protected void decorateHouse() {
            System.out.println("装修房子:进行木质风格的内外装修。");
        }
    }
    
    // Step 2: 实现具体的模板子类 - 砖房
    public class BrickHouseBuilder extends HouseBuilder {
        @Override
        protected void buildWalls() {
            System.out.println("建造墙壁:使用砖块和水泥砌墙。");
        }
    
        @Override
        protected void buildRoof() {
            System.out.println("建造屋顶:铺设陶瓷瓦片屋顶。");
        }
    
        // 这个砖房就不重写 decorateHouse,使用父类的默认装修方法
    }
  3. 客户端(Client)

    使用具体模板实现类来执行模板方法。

    java
    // Step 3: 客户端代码
    public class TemplateMethodPatternDemo {
        public static void main(String[] args) {
            System.out.println("--- 建造木屋 ---");
            HouseBuilder woodenHouse = new WoodenHouseBuilder();
            woodenHouse.buildHouse(); // 调用模板方法
    
            System.out.println("\n--- 建造砖房 ---");
            HouseBuilder brickHouse = new BrickHouseBuilder();
            brickHouse.buildHouse(); // 调用模板方法
        }
    }

代码解释:

  • HouseBuilder 是抽象模板类,它定义了 buildHouse() 这个模板方法。这个方法包含了建造房子的固定步骤顺序。
  • layFoundation() 是一个具体方法,所有房子打地基的方式都一样,所以在父类中直接实现。
  • buildWalls()buildRoof() 是抽象方法,它们的具体实现留给子类 WoodenHouseBuilderBrickHouseBuilder 去完成。
  • decorateHouse() 是一个钩子方法。它在父类中有一个默认的(可能是空的或通用的)实现,子类可以选择性地重写它来添加自己的特定行为。
  • buildHouse() 方法被声明为 final,这是为了防止子类修改算法的骨架,保证了算法的稳定性和一致性。
  • 客户端通过创建具体子类的实例,然后调用它们的模板方法 buildHouse() 来启动整个建造流程。
谈谈你了解的最常见的几种设计模式,说说他们的应用场景

设计模式就像是前辈们在编程路上遇到一些常见问题时总结出来的“解决方案模板”,不是死板的代码,而是一种解决问题的思路。最常见的几种:

  1. 单例模式 (Singleton Pattern)

    • 理解方式:全局只有一份,独一无二。就像公司的 CEO,只能有一个。
    • 应用场景:
      • 日志记录器:整个应用只需要一个日志记录器来处理所有日志,避免多个实例导致日志混乱。
      • 配置管理器:应用程序的配置信息通常全局一份,所有模块都从这里获取。
      • 数据库连接池:为了节省资源和提高效率,通常只创建一个数据库连接池,供所有需要数据库操作的地方使用。
  2. 工厂模式 (Factory Pattern)

    • 理解方式:我想买个手机,但我不用知道具体是富士康生产还是比亚迪生产,我只告诉商店我需要一个手机,它就给我一个。
    • 应用场景:
      • 创建不同类型的图形对象:比如画图软件,用户选择画“圆形”、“方形”或“三角形”,你不需要写一堆 if/else 来判断创建哪个,而是通过一个工厂方法来创建,它会根据你的选择返回对应的图形对象。
      • 多数据库支持:你的应用可能需要支持 MySQL、Oracle 或 SQL Server。你可以有一个数据库连接工厂,根据配置返回对应的数据库连接对象,而调用者无需关心是哪种数据库。
  3. 观察者模式 (Observer Pattern)

    • 理解方式:就像订阅报纸。报社(被观察者)一出新报纸,所有订阅者(观察者)都会收到通知。
    • 应用场景:
      • 事件处理:比如用户点击按钮,按钮(被观察者)会通知所有监听这个点击事件的函数(观察者)去执行相应的操作。
      • 股票行情系统:股票价格(被观察者)一变动,所有订阅了这只股票的用户(观察者)的客户端都会收到更新。
      • MVC 架构:Model 层数据变化,通知 View 层和 Controller 层进行更新。
  4. 策略模式 (Strategy Pattern)

    • 理解方式:解决一件事有好几种方法,我把这些方法都封装起来,然后根据情况选择其中一个来执行。就像你出门,可以开车、坐地铁、骑自行车,每种都是一种“策略”。
    • 应用场景:
      • 不同支付方式:在线购物时,可以选择支付宝支付、微信支付、银行卡支付。每种支付方式都是一个策略,根据用户的选择来执行对应的支付逻辑。
      • 排序算法:对数据进行排序,可以选择冒泡排序、快速排序、归并排序。根据数据量或特性选择不同的排序算法。
      • 计算打折:商场促销有满减、打八折、送优惠券等多种促销策略,根据活动的具体规则选择不同的计算方式。
你认为好的代码应该是什么样的?

好的代码就像一本好书,它有以下几个特点:

  1. 易读性 (Readable):

    • 像读故事一样:你能快速理解这段代码在做什么,而不是猜谜语。变量名、函数名清晰直观,就像“用户名”而不是“u”、“计算总价”而不是“cal”。
    • 格式整齐:缩进规范,空行合理,就像文章分段清晰。
  2. 可维护性 (Maintainable):

    • 改动不“牵一发而动全身”:修改一个功能时,不会导致其他不相关的功能崩溃或需要大量修改。就像换个车灯,不用把整个发动机都拆下来。
    • 容易找到问题:出错了能快速定位是哪一部分代码有问题。
  3. 可扩展性 (Extensible):

    • 未来可期:当有新需求或新功能要加入时,能轻松地添加,而不是推翻重来。就像盖房子,留好了接口,以后可以轻松加盖一层或加个阳台。
    • 对修改关闭,对扩展开放 (OCP 原则):尽量不要修改已有的稳定代码,而是通过新增代码来扩展功能。
  4. 健壮性 (Robust):

    • 不怕“折腾”:能处理各种异常情况和错误输入,不会轻易崩溃。就像一个好的士兵,在各种恶劣环境下都能完成任务。
    • 错误处理完善:及时给出错误提示,或者能优雅地从错误中恢复。
  5. 高内聚低耦合 (High Cohesion, Low Coupling):

    • 高内聚:一个模块只负责一件事,并且把这件事做到极致。就像一个专业团队,每个人只负责自己擅长的领域。
    • 低耦合:模块之间依赖关系少,一个模块的改动对其他模块影响小。就像不同部门之间协作,通过接口而不是直接干涉对方内部细节。
  6. 符合设计原则:比如遵循单一职责原则(一个类只做一件事)、里氏替换原则(子类可以替换父类且不破坏程序功能)等,这些都是通往好代码的“秘籍”。

总的来说,好的代码是让人舒服的代码,无论是写的人、读的人还是未来维护的人,都能感觉到它的清晰、高效和可靠。

工厂模式和抽象工厂模式有什么区别?

这就像“生产线”的两种不同管理方式:

工厂模式 (Factory Pattern) - 简单工厂或工厂方法

  • 通俗理解:假设你是一个披萨店老板。
    • 简单工厂:你有一个“点餐员”(就是工厂),顾客来点餐,无论是“海鲜披萨”还是“培根披萨”,点餐员都会给你做好。你只管告诉点餐员要什么,他就会给你一个做好的披萨。这个点餐员能生产“一种类型”的产品(披萨),但是可以生产这种类型的“不同口味/品种”。
    • 工厂方法:你有多个分店,每个分店都有自己的“厨师长”(就是工厂方法),这个厨师长专门做一种披萨。比如“纽约分店的厨师长”专门做纽约风味披萨,“芝加哥分店的厨师长”专门做芝加哥风味披萨。每种厨师长都只生产一种“特定风味”的披萨。
  • 主要解决:创建“一类产品”的不同“具体实现”。例如,创建不同种类的汽车(轿车、SUV),或者不同种类的文档(PDF、Word)。
  • 特点:
    • 通常一个工厂(或一个工厂方法)只负责创建一种类型的产品。
    • 当需要新增一种产品时,可能需要修改工厂代码(简单工厂)或者新增一个工厂类(工厂方法)。

示例:我们来创建一个披萨工厂,它可以根据用户的需求生产不同类型的披萨。

  • 首先,定义披萨的抽象类或接口:

    java
    // 披萨的抽象基类
    abstract class Pizza {
        protected String name;
    
        public abstract void prepare();
        public abstract void bake();
        public abstract void cut();
        public abstract void box();
    
        public String getName() {
            return name;
        }
    }
    
    // 具体披萨类型 1:芝士披萨
    class CheesePizza extends Pizza {
        public CheesePizza() {
            name = "芝士披萨";
        }
    
        @Override
        public void prepare() {
            System.out.println("准备芝士披萨的原料:面团、番茄酱、大量芝士");
        }
    
        @Override
        public void bake() {
            System.out.println("烘烤芝士披萨,200 度 15 分钟");
        }
    
        @Override
        public void cut() {
            System.out.println("将芝士披萨切成 8 块");
        }
    
        @Override
        public void box() {
            System.out.println("将芝士披萨装入披萨盒");
        }
    }
    
    // 具体披萨类型 2:培根披萨
    class BaconPizza extends Pizza {
        public BaconPizza() {
            name = "培根披萨";
        }
    
        @Override
        public void prepare() {
            System.out.println("准备培根披萨的原料:面团、番茄酱、培根、洋葱");
        }
    
        @Override
        public void bake() {
            System.out.println("烘烤培根披萨,200 度 18 分钟");
        }
    
        @Override
        public void cut() {
            System.out.println("将培根披萨切成 6 块");
        }
    
        @Override
        public void box() {
            System.out.println("将培根披萨装入披萨盒");
        }
    }
  • 然后,定义披萨工厂的抽象接口和具体工厂类:

    java
    // 披萨工厂的抽象接口
    // 每个具体工厂负责生产一种类型的披萨
    interface PizzaFactory {
        Pizza createPizza();
    }
    
    // 芝士披萨工厂
    class CheesePizzaFactory implements PizzaFactory {
        @Override
        public Pizza createPizza() {
            System.out.println("----通过芝士披萨工厂生产芝士披萨----");
            return new CheesePizza();
        }
    }
    
    // 培根披萨工厂
    class BaconPizzaFactory implements PizzaFactory {
        @Override
        public Pizza createPizza() {
            System.out.println("----通过培根披萨工厂生产培根披萨----");
            return new BaconPizza();
        }
    }
  • 最后,客户端如何使用:

    java
    public class FactoryMethodDemo {
        public static void main(String[] args) {
            System.out.println("欢迎来到披萨店!");
    
            // 客户想要一个芝士披萨
            PizzaFactory cheeseFactory = new CheesePizzaFactory();
            Pizza cheesePizza = cheeseFactory.createPizza();
            System.out.println("你点的是:" + cheesePizza.getName());
            cheesePizza.prepare();
            cheesePizza.bake();
            cheesePizza.cut();
            cheesePizza.box();
            System.out.println();
    
            // 客户想要一个培根披萨
            PizzaFactory baconFactory = new BaconPizzaFactory();
            Pizza baconPizza = baconFactory.createPizza();
            System.out.println("你点的是:" + baconPizza.getName());
            baconPizza.prepare();
            baconPizza.bake();
            baconPizza.cut();
            baconPizza.box();
        }
    }
  • 运行结果:

    txt
    欢迎来到披萨店!
    ----通过芝士披萨工厂生产芝士披萨----
    你点的是:芝士披萨
    准备芝士披萨的原料:面团、番茄酱、大量芝士
    烘烤芝士披萨,200 度 15 分钟
    将芝士披萨切成 8 块
    将芝士披萨装入披萨盒
    
    ----通过培根披萨工厂生产培根披萨----
    你点的是:培根披萨
    准备培根披萨的原料:面团、番茄酱、培根、洋葱
    烘烤培根披萨,200 度 18 分钟
    将培根披萨切成 6 块
    将培根披萨装入披萨盒
  • 解释:

    在这个例子中,PizzaFactory 是抽象工厂,定义了创建披萨的方法。CheesePizzaFactoryBaconPizzaFactory 是具体工厂,每个工厂只负责创建一种具体类型的披萨(芝士披萨或培根披萨)。当需要增加一种新的披萨类型(比如“蔬菜披萨”)时,我们只需要创建一个新的 VegetablePizza 类和一个新的 VegetablePizzaFactory 类,而不需要修改已有的工厂或披萨类。这符合“开闭原则”。

抽象工厂模式 (Abstract Factory Pattern)

  • 通俗理解:回到披萨店。现在你的店不只卖披萨了,还卖饮品和甜点。
    • 抽象工厂就像是“整个餐厅的运营管理团队”。
    • 你有一个“纽约风格餐厅运营团队”,它知道怎么生产“纽约风味披萨”、“纽约风味饮料”和“纽约风味甜点”。
    • 你还有一个“芝加哥风格餐厅运营团队”,它知道怎么生产“芝加哥风味披萨”、“芝加哥风味饮料”和“芝加哥风味甜点”。
    • 你想要一套“纽约风格”的产品(披萨、饮料、甜点),你只需要告诉“纽约风格餐厅运营团队”,它就会给你生产出来。
  • 主要解决:创建“一系列相关或相互依赖的产品族”。例如,你不仅仅需要“芝士披萨”,可能还需要一套“芝士披萨家族”的产品,包括“芝士披萨”、“芝士口味饮料”、“芝士口味甜点”。
  • 特点:
    • 一个抽象工厂可以生产多个不同类型但属于同一“产品族”的产品
    • 当需要新增一个“产品族”时(例如,新增“意大利风格”的产品系列),需要新增一个具体的抽象工厂实现类。而新增单个产品(例如,在现有产品族里新增一种披萨),可能需要修改所有相关工厂。

示例:披萨店升级了,不只卖披萨,还卖饮料和甜点,而且披萨店有“纽约风味”和“芝加哥风味”两种风格。每种风格的披萨、饮料、甜点都是一套。

  • 首先,定义产品族(披萨、饮料、甜点)的抽象类或接口:

    java
    // --- 产品族:披萨 ---
    abstract class AbsPizza {
        protected String style;
    
        public abstract void prepare();
        public abstract void bake();
        public String getStyle() { return style; }
    }
    
    class NYStyleCheesePizza extends AbsPizza {
        public NYStyleCheesePizza() { style = "纽约风味芝士披萨"; }
        @Override public void prepare() { System.out.println("准备纽约风味芝士披萨:薄底面团、马苏里拉芝士..."); }
        @Override public void bake() { System.out.println("烘烤纽约风味芝士披萨..."); }
    }
    
    class ChicagoStyleCheesePizza extends AbsPizza {
        public ChicagoStyleCheesePizza() { style = "芝加哥风味芝士披萨"; }
        @Override public void prepare() { System.out.println("准备芝加哥风味芝士披萨:厚底面团、深盘、大量芝士..."); }
        @Override public void bake() { System.out.println("烘烤芝加哥风味芝士披萨..."); }
    }
    
    // --- 产品族:饮料 ---
    abstract class AbsDrink {
        protected String style;
    
        public abstract void serve();
        public String getStyle() { return style; }
    }
    
    class NYStyleSoda extends AbsDrink {
        public NYStyleSoda() { style = "纽约风味苏打水"; }
        @Override public void serve() { System.out.println("提供纽约风味苏打水:加冰、柠檬片..."); }
    }
    
    class ChicagoStyleCola extends AbsDrink {
        public ChicagoStyleCola() { style = "芝加哥风味可乐"; }
        @Override public void serve() { System.out.println("提供芝加哥风味可乐:大杯装、少冰..."); }
    }
    
    // --- 产品族:甜点 ---
    abstract class AbsDessert {
        protected String style;
    
        public abstract void serve();
        public String getStyle() { return style; }
    }
    
    class NYStyleCheesecake extends AbsDessert {
        public NYStyleCheesecake() { style = "纽约风味芝士蛋糕"; }
        @Override public void serve() { System.out.println("提供纽约风味芝士蛋糕:小巧精致..."); }
    }
    
    class ChicagoStyleBrownie extends AbsDessert {
        public ChicagoStyleBrownie() { style = "芝加哥风味布朗尼"; }
        @Override public void serve() { System.out.println("提供芝加哥风味布朗尼:浓郁厚实..."); }
    }
  • 然后,定义抽象工厂和具体工厂:

    java
    // 抽象工厂接口:定义创建一组产品的方法
    interface RestaurantFactory {
        AbsPizza createPizza();
        AbsDrink createDrink();
        AbsDessert createDessert();
    }
    
    // 纽约风味餐厅的具体工厂
    class NYStyleRestaurantFactory implements RestaurantFactory {
        @Override
        public AbsPizza createPizza() {
            return new NYStyleCheesePizza();
        }
    
        @Override
        public AbsDrink createDrink() {
            return new NYStyleSoda();
        }
    
        @Override
        public AbsDessert createDessert() {
            return new NYStyleCheesecake();
        }
    }
    
    // 芝加哥风味餐厅的具体工厂
    class ChicagoStyleRestaurantFactory implements RestaurantFactory {
        @Override
        public AbsPizza createPizza() {
            return new ChicagoStyleCheesePizza();
        }
    
        @Override
        public AbsDrink createDrink() {
            return new ChicagoStyleCola();
        }
    
        @Override
        public AbsDessert createDessert() {
            return new ChicagoStyleBrownie();
        }
    }
  • 最后,客户端如何使用:

    java
    public class AbstractFactoryDemo {
        public static void main(String[] args) {
            System.out.println("欢迎来到全球披萨店连锁!");
    
            // 客户想点一套纽约风味的餐品
            System.out.println("\n--- 顾客点了一套纽约风味餐品 ---");
            RestaurantFactory nyFactory = new NYStyleRestaurantFactory();
            AbsPizza nyPizza = nyFactory.createPizza();
            AbsDrink nyDrink = nyFactory.createDrink();
            AbsDessert nyDessert = nyFactory.createDessert();
    
            System.out.println("你的纽约风味套餐包含:");
            System.out.println("- " + nyPizza.getStyle());
            nyPizza.prepare();
            nyPizza.bake();
            System.out.println("- " + nyDrink.getStyle());
            nyDrink.serve();
            System.out.println("- " + nyDessert.getStyle());
            nyDessert.serve();
    
            // 客户想点一套芝加哥风味的餐品
            System.out.println("\n--- 顾客点了一套芝加哥风味餐品 ---");
            RestaurantFactory chicagoFactory = new ChicagoStyleRestaurantFactory();
            AbsPizza chicagoPizza = chicagoFactory.createPizza();
            AbsDrink chicagoDrink = chicagoFactory.createDrink();
            AbsDessert chicagoDessert = chicagoFactory.createDessert();
    
            System.out.println("你的芝加哥风味套餐包含:");
            System.out.println("- " + chicagoPizza.getStyle());
            chicagoPizza.prepare();
            chicagoPizza.bake();
            System.out.println("- " + chicagoDrink.getStyle());
            chicagoDrink.serve();
            System.out.println("- " + chicagoDessert.getStyle());
            chicagoDessert.serve();
        }
    }
  • 运行结果:

    txt
    欢迎来到全球披萨店连锁!
    
    --- 顾客点了一套纽约风味餐品 ---
    你的纽约风味套餐包含:
    - 纽约风味芝士披萨
    准备纽约风味芝士披萨:薄底面团、马苏里拉芝士...
    烘烤纽约风味芝士披萨...
    - 纽约风味苏打水
    提供纽约风味苏打水:加冰、柠檬片...
    - 纽约风味芝士蛋糕
    提供纽约风味芝士蛋糕:小巧精致...
    
    --- 顾客点了一套芝加哥风味餐品 ---
    你的芝加哥风味套餐包含:
    - 芝加哥风味芝士披萨
    准备芝加哥风味芝士披萨:厚底面团、深盘、大量芝士...
    烘烤芝加哥风味芝士披萨...
    - 芝加哥风味可乐
    提供芝加哥风味可乐:大杯装、少冰...
    - 芝加哥风味布朗尼
    提供芝加哥风味布朗尼:浓郁厚实...
  • 抽象工厂模式解释:

    在这个例子中,RestaurantFactory 是抽象工厂,定义了创建披萨、饮料和甜点的方法。NYStyleRestaurantFactoryChicagoStyleRestaurantFactory 是具体工厂,每个工厂负责创建一整套特定风格的产品(纽约风味的披萨、饮料、甜点;或芝加哥风味的披萨、饮料、甜点)。

    如果我们要新增一种“意大利风味”的餐厅,我们只需要创建一个新的 ItalianStyleRestaurantFactory 类,实现 RestaurantFactory 接口,并让它创建对应的意大利风味披萨、饮料和甜点即可。客户端代码只需要切换具体的工厂实例,就能获得不同风格的整套产品,而无需关心内部创建的细节。

核心区别总结:

  • 工厂模式:生产一种产品类型的不同具体实现
    • 目标:创建一个产品。
    • 例子:我想要一个“车”,工厂给我一个“轿车”或“SUV”。
  • 抽象工厂模式:生产一系列相关或相互依赖的产品家族
    • 目标:创建一套产品。
    • 例子:我想要一套“德国品牌”的“车、电视、洗衣机”,抽象工厂给我“奔驰轿车、西门子电视、博世洗衣机”。

简单来说,工厂模式关注的是“造一个什么东西”,而抽象工厂模式关注的是“造一套什么风格的东西”。

贡献者

The avatar of contributor named as LI SIR LI SIR

页面历史