1. 面试官为什么总爱问工厂模式?
如果你参加过几次Java或后端开发的面试,大概率会被问到设计模式,而工厂模式几乎是必考题。为什么面试官对它情有独钟?我面过不少人,也被人面过,总结下来就三点:第一,它太常用了,从JDK到Spring,再到各种中间件,工厂模式无处不在,是考察候选人是否具备“读源码”能力的基础门槛。第二,它理解门槛适中,不像抽象工厂那么绕,也不像单例那么简单,正好能区分出“背概念”和“真理解”的候选人。第三,它能引出很多延伸问题,比如和Spring IoC容器的关系、各种工厂变体的区别、设计原则的体现,是一个绝佳的“话题起点”。
很多朋友在准备时,往往只记住了“简单工厂、工厂方法、抽象工厂”这三个名词和UML图,但一到追问细节就露怯。比如,Spring的BeanFactory是哪种工厂?为什么Calendar.getInstance()也被称为工厂?工厂模式和建造者模式到底怎么选?这篇文章,我就结合自己十多年的开发、面试和带团队的经验,把工厂模式从“面试八股”还原成“工程利器”,让你不仅能在面试中对答如流,更能真正在项目中用得恰到好处。
2. 从“new”的困境到工厂的诞生:一个真实场景的推演
要理解工厂模式,我们得先回到问题的源头:为什么我们不全用new来创建对象?直接new不是最简单吗?我们来看一个我早期在电商项目中遇到的真实案例。
当时我们需要对接多个第三方物流公司(比如顺丰、中通、圆通)来计算运费。最初,代码长这样:
public class ShippingService { public BigDecimal calculateFee(String logisticsCompany, String orderId) { LogisticsCalculator calculator = null; if ("SF".equals(logisticsCompany)) { calculator = new SfLogisticsCalculator(); } else if ("ZTO".equals(logisticsCompany)) { calculator = new ZtoLogisticsCalculator(); } else if ("YTO".equals(logisticsCompany)) { calculator = new YtoLogisticsCalculator(); } // ... 后续计算逻辑 return calculator.calculate(orderId); } }这段代码在只有两三家物流公司时还能忍受。但很快问题接踵而至:
- 违反开闭原则:每增加一家新物流公司(比如极兔),我都要修改这个
if-else堡垒,ShippingService这个核心业务类被频繁改动,风险极高。 - 职责过重:
ShippingService既要负责核心的运费计算逻辑,又要负责具体物流计算器的创建,违反了单一职责原则。 - 难以测试:我想单元测试
ShippingService的计算逻辑,但因为它内部直接new了具体的物流计算器,导致我无法轻松地注入一个Mock对象进行隔离测试。 - 代码重复:项目其他地方(比如订单状态跟踪模块)也需要根据物流公司创建对应的跟踪器对象,又得把这段
if-else复制一遍。
这就是“new”的困境:创建逻辑与使用逻辑强耦合。工厂模式要解决的,正是将对象的创建过程封装起来,让使用方(Client)无需关心对象的具体实现类,只需通过一个统一的“工厂”接口来获取对象。
注意:这里的关键转变是思维上的——从“我知道我要创建什么具体类”转变为“我知道我需要一个具备某种能力的对象,至于它具体是谁,由工厂告诉我”。这是理解所有工厂变体的基础。
3. 简单工厂:快速解耦的实用主义方案
面对上面的问题,最直接的改进就是引入一个专门的类来负责创建LogisticsCalculator对象。这个类就是简单工厂(Simple Factory),它也叫静态工厂,因为其创建方法通常是静态的。
我们把创建逻辑从ShippingService中抽离出来:
// 简单工厂类 public class LogisticsCalculatorFactory { public static LogisticsCalculator createCalculator(String company) { if ("SF".equals(company)) { return new SfLogisticsCalculator(); } else if ("ZTO".equals(company)) { return new ZtoLogisticsCalculator(); } else if ("YTO".equals(company)) { return new YtoLogisticsCalculator(); } throw new IllegalArgumentException("Unsupported logistics company: " + company); } } // 使用方变得非常简洁 public class ShippingService { public BigDecimal calculateFee(String logisticsCompany, String orderId) { LogisticsCalculator calculator = LogisticsCalculatorFactory.createCalculator(logisticsCompany); return calculator.calculate(orderId); } }简单工厂的核心价值与面试要点:
- 价值:实现了对象的创建和使用分离,
ShippingService现在只依赖工厂和抽象接口,不再依赖任何具体实现类,上面的问题2、3、4都得到了缓解。 - 面试常问:“简单工厂是23种设计模式之一吗?”答案是否定的。在GoF的经典著作中,并没有“简单工厂”这个独立模式,它更像是一种编程习惯或技巧。但这不影响它的广泛使用和考察价值。
- 优点:结构简单,易于理解,对于创建逻辑不复杂、产品类型固定的场景,它是性价比最高的解耦方案。
- 缺点:它并没有完全解决开闭原则的问题。新增产品类型(如极兔)时,我们仍然需要修改
LogisticsCalculatorFactory类的createCalculator方法。只不过修改点从业务类集中到了工厂类,破坏性降低了。
实操心得:简单工厂非常适合在项目初期或内部工具类中使用。例如,JDK中的Calendar.getInstance()、NumberFormat.getNumberInstance()都是简单工厂的体现。它们根据本地化环境等参数,返回不同的具体实现。在面试中,能举出这些JDK中的例子,会让面试官觉得你有很好的知识迁移能力。
4. 工厂方法模式:将“选择权”下放,彻底拥抱扩展
当产品家族变得庞大,或者我们希望将产品的创建延迟到子类时,简单工厂的集中式处理就会显得笨重。这时,工厂方法模式(Factory Method Pattern)就该登场了。它的定义是:定义一个用于创建对象的接口,但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
听起来有点绕,我们继续用物流的例子改造。假设现在业务扩展了,我们不仅有“国内物流”,还有“跨境物流”,两者的计算器创建逻辑和依赖的资源完全不同。强行用一个简单工厂会使得方法内部if-else膨胀且难以维护。
工厂方法模式的实现:
- 抽象产品:
LogisticsCalculator(不变)。 - 具体产品:
SfCalculator,ZtoCalculator等(不变)。 - 抽象工厂:定义一个创建产品的抽象方法。
- 具体工厂:每个具体工厂负责创建一种具体产品。
// 1. 抽象工厂接口 public interface LogisticsCalculatorFactory { LogisticsCalculator createCalculator(); } // 2. 具体工厂类,每个负责创建一种产品 public class SfCalculatorFactory implements LogisticsCalculatorFactory { @Override public LogisticsCalculator createCalculator() { // 这里可以包含复杂的SF计算器初始化逻辑,比如加载专属配置、建立连接池等 return new SfLogisticsCalculator(); } } public class ZtoCalculatorFactory implements LogisticsCalculatorFactory { @Override public LogisticsCalculator createCalculator() { // ZTO的特定初始化 return new ZtoLogisticsCalculator(); } } // 3. 使用方:现在它依赖工厂接口 public class DomesticShippingService { private LogisticsCalculatorFactory factory; // 通过构造器或Setter注入工厂 public DomesticShippingService(LogisticsCalculatorFactory factory) { this.factory = factory; } public BigDecimal calculateFee(String orderId) { // 使用工厂创建产品,完全不知道具体产品类 LogisticsCalculator calculator = factory.createCalculator(); return calculator.calculate(orderId); } } // 4. 客户端代码决定使用哪个工厂(通常由Spring等容器完成) public class Client { public static void main(String[] args) { // 创建顺丰工厂 LogisticsCalculatorFactory factory = new SfCalculatorFactory(); // 将工厂注入服务 DomesticShippingService service = new DomesticShippingService(factory); service.calculateFee("order123"); } }工厂方法模式的核心价值与面试要点:
- 价值:完美符合开闭原则。现在要新增一个“极兔”计算器,我只需要新增
JituLogisticsCalculator和JituCalculatorFactory两个类。原有的SfCalculatorFactory、ZtoCalculatorFactory和DomesticShippingService都不需要做任何修改。系统的可扩展性得到了质的提升。 - 与简单工厂的对比:这是面试高频题。简单工厂把创建逻辑放在一个类里做集中判断(参数化创建);工厂方法则是将创建逻辑分散到各个平行的工厂子类中,由客户端决定使用哪个子类。工厂方法更符合“单一职责”和“开闭原则”。
- Spring中的体现:Spring Framework的
BeanFactory本身就是工厂模式思想的集大成者。而更具体的,ApplicationContext可以看作是BeanFactory的增强版工厂。当我们使用@Bean注解在一个配置类中定义方法时,这个方法就是一个“工厂方法”,Spring会调用它来创建和返回Bean的实例。
踩坑实录:我曾见过有团队过度使用工厂方法,为每一个简单的、无状态的DAO或Service都创建一个对应的工厂接口和实现类,导致项目里工厂类爆炸,维护成本陡增。切记,设计模式是工具,不是信仰。如果产品的创建逻辑非常简单(就是一个简单的new),且未来不太可能变化或扩展,直接new或者用简单工厂是更务实的选择。工厂方法模式适用于“产品创建过程复杂,且不同产品创建逻辑差异大”的场景。
5. 抽象工厂模式:应对“产品家族”的复杂性
工厂方法模式处理的是单一产品等级结构的创建问题。但如果我们的系统需要创建多个相互关联或依赖的产品对象(即一个“产品家族”),工厂方法就显得力不从心了。这时就需要抽象工厂模式(Abstract Factory Pattern)。
举个例子,我们上面的物流系统需要升级。一个完整的物流方案不仅包括“运费计算器”(Calculator),还包括“物流面单生成器”(WaybillGenerator)和“物流状态追踪器”(Tracker)。并且,顺丰、中通、圆通各自都有自己的一套实现。我们不可能让客户端去分别创建顺丰的计算器、中通的面单生成器,这会导致对象不匹配。
抽象工厂模式的定义:提供一个接口,用于创建相关或依赖对象的家族,而无需指定它们具体的类。
// ---------- 抽象产品族 ---------- public interface Calculator { /* ... */ } public interface WaybillGenerator { /* ... */ } public interface Tracker { /* ... */ } // ---------- 抽象工厂 ---------- public interface LogisticsFactory { Calculator createCalculator(); WaybillGenerator createWaybillGenerator(); Tracker createTracker(); } // ---------- 具体产品族(顺丰套件) ---------- public class SfCalculator implements Calculator { /* ... */ } public class SfWaybillGenerator implements WaybillGenerator { /* ... */ } public class SfTracker implements Tracker { /* ... */ } // ---------- 具体工厂(顺丰工厂) ---------- public class SfLogisticsFactory implements LogisticsFactory { @Override public Calculator createCalculator() { return new SfCalculator(); } @Override public WaybillGenerator createWaybillGenerator() { return new SfWaybillGenerator(); } @Override public Tracker createTracker() { return new SfTracker(); } } // ---------- 具体产品族(中通套件) ---------- (类似,略) // ---------- 客户端使用 ---------- public class LogisticsClient { private Calculator calculator; private WaybillGenerator generator; private Tracker tracker; public LogisticsClient(LogisticsFactory factory) { // 通过一个工厂,创建出一套兼容的产品对象 this.calculator = factory.createCalculator(); this.generator = factory.createWaybillGenerator(); this.tracker = factory.createTracker(); } public void processOrder(String order) { BigDecimal fee = calculator.calculate(order); String waybill = generator.generate(order); tracker.track(waybill); // ... 使用这套兼容的对象协同工作 } }抽象工厂模式的核心价值与面试要点:
- 价值:保证产品家族的兼容性。客户端代码(
LogisticsClient)只与抽象工厂和抽象产品交互。它不需要知道创建的是顺丰套件还是中通套件,但它能确保得到的计算器、面单生成器和追踪器一定是来自同一家物流公司,可以无缝协作。这对于需要“成套”使用对象的UI主题切换、跨平台应用开发(如一套代码生成Windows和Mac控件)等场景至关重要。 - 与工厂方法的对比:这是另一个面试必问题。工厂方法关注单一产品的创建,而抽象工厂关注系列产品的创建。抽象工厂的接口里通常有多个工厂方法,每个方法创建一个不同类型的产品。可以理解为,抽象工厂是工厂方法模式的升级版,用于更复杂的对象创建场景。
- 缺点:难以支持新种类的产品。如果要在产品家族中增加一个新成员(比如新增一个
InsuranceProvider保险提供者),就需要修改LogisticsFactory接口以及所有它的实现类(SfLogisticsFactory,ZtoLogisticsFactory等),这违反了开闭原则。因此,抽象工厂模式适用于产品家族结构稳定,不会频繁新增产品种类的场景。
个人经验:在业务系统中,纯正的抽象工厂模式使用频率低于工厂方法。但在框架和中间件层面,它非常常见。例如,在Java数据库连接中,java.sql.Connection就是一个抽象工厂,它可以创建Statement,PreparedStatement,CallableStatement等一套相关的数据库操作对象,而具体是哪个数据库的实现(MySQL, PostgreSQL),由驱动决定。
6. 面试深度追问与实战辨析
掌握了三种工厂的基本形态,面试官通常会从两个方向深入追问:1. 与其他模式的对比;2. 在主流框架中的应用。
6.1 工厂模式 vs. 建造者模式
这也是一个高频混淆点。两者都用于创建对象,但侧重点不同。
- 工厂模式:关注的是对象的整体创建,尤其是将创建过程封装起来,隐藏具体类型。客户端说“我要一个A类型对象”,工厂就给一个创建好的、完整的A对象。产品通常是“标准品”。
- 建造者模式:关注的是对象的组装过程,特别是当对象构造过程复杂,且需要分步骤、个性化配置时。客户端通过指挥者(Director)或直接使用建造者,一步步设置属性,最后构建出一个对象。产品通常是“定制品”。
一个简单类比:
- 工厂模式:去麦当劳点一个“巨无霸套餐”,后厨(工厂)按标准流程做好整套给你。
- 建造者模式:去Subway点一个三明治,你告诉店员(建造者):要全麦面包、加烤鸡胸肉、不要洋葱、多加生菜、配蛋黄酱……最后得到你定制的那一个。
在代码中,如果对象的构造参数很多且可选,或者构造过程有严格的顺序要求,建造者模式(尤其是Lombok的@Builder)比工厂模式更合适。
6.2 Spring框架中的工厂模式
Spring的核心——IoC容器,本质上就是一个超级工厂。它管理的BeanFactory和ApplicationContext是工厂模式的终极实践。
BeanFactory:是Spring容器的基础接口,定义了最基本的工厂方法getBean(),这就是一个工厂方法模式的体现。ApplicationContext:作为BeanFactory的子接口,是更高级的容器,它在工厂方法的基础上,集成了更多企业级功能(事件、国际化等)。FactoryBean:这是一个特殊的接口。如果一个Bean实现了FactoryBean,那么从容器中获取这个Bean时,得到的不是它本身,而是它getObject()方法返回的对象。这是一种更灵活的工厂模式,常用于集成第三方库(如MyBatis的SqlSessionFactoryBean)或创建复杂对象。- 静态工厂方法 & 实例工厂方法:在Spring XML配置或Java Config (
@Configuration)中,你可以指定一个静态方法或一个Bean的实例方法来创建另一个Bean,这分别是简单工厂和工厂方法模式在Spring配置中的直接应用。
在面试中,如果能主动将Spring的IoC原理和工厂模式联系起来,并清晰地说出BeanFactory、FactoryBean的区别,绝对是加分项。
6.3 简单工厂的“配置化”升级
在实际项目中,我们如何让简单工厂也能符合开闭原则?答案是结合反射和配置。这是很多中高级面试会问到的实践。
我们可以将“物流公司编码”与“具体计算器类名”的映射关系放到配置文件(如properties, YAML)或数据库中。工厂类读取配置,利用反射动态创建实例。
// config.properties sf=com.xxx.logistics.SfLogisticsCalculator zto=com.xxx.logistics.ZtoLogisticsCalculator yto=com.xxx.logistics.YtoLogisticsCalculator // 升级版的简单工厂 public class LogisticsCalculatorFactory { private static Map<String, Class<? extends LogisticsCalculator>> cachedMap = new HashMap<>(); static { // 启动时加载配置,初始化映射缓存 Properties props = loadProperties(); for (String key : props.stringPropertyNames()) { String className = props.getProperty(key); Class<?> clazz = Class.forName(className); cachedMap.put(key, clazz.asSubclass(LogisticsCalculator.class)); } } public static LogisticsCalculator createCalculator(String company) { Class<? extends LogisticsCalculator> clazz = cachedMap.get(company); if (clazz == null) { throw new IllegalArgumentException("Unsupported company: " + company); } try { // 通过反射创建实例,这里假设都有无参构造 return clazz.newInstance(); } catch (Exception e) { throw new RuntimeException("Failed to create calculator for " + company, e); } } }这样,新增物流公司时,我们只需要在配置文件中加一行映射,然后新增对应的计算器类,工厂类代码无需重新编译和部署。这本质上是将变化转移到了外部配置,是一种非常实用的“开闭原则”实现方式。当然,它牺牲了编译期的类型安全检查,需要良好的异常处理和类加载机制来保障。
7. 如何在项目中正确选用与落地
理论懂了,面试能说了,但回到日常开发,到底该怎么用?我总结了一个简单的决策流:
- 对象创建是否简单且唯一?如果是,直接
new。别为了模式而模式。 - 是否需要隔离创建逻辑,方便测试和替换?如果是,引入简单工厂。这是性价比最高的解耦。
- 未来是否会频繁增加新的产品类型,且各产品创建逻辑复杂?如果是,使用工厂方法模式。将变化封装在各自的具体工厂里。
- 是否需要创建一整套相互关联的产品对象(产品族)?如果是,考虑抽象工厂模式。
- 产品的构建是否需要复杂的、分步骤的装配过程?如果是,考虑建造者模式。
落地时的注意事项:
- 命名规范:工厂类名最好以
Factory结尾,方法名常用createXXX(),getInstance(),makeXXX()等,保持团队统一。 - 依赖注入:在现代Spring Boot项目中,工厂本身也常常被注册为Bean,通过
@Autowired注入到使用方。具体工厂的实现可以通过@ConditionalOnProperty等条件注解来动态选择,这比在代码中写if-else或switch更优雅。 - 不要滥用:如果一个工厂类里面只有一个
if-else创建两个对象,并且三年都没变过,那么它可能带来的抽象收益小于其增加的复杂度。时刻权衡模式的收益与成本。
工厂模式不是银弹,但它为我们提供了一种至关重要的设计思维:依赖倒置。高层模块不应该依赖低层模块,二者都应该依赖其抽象。工厂模式正是通过依赖抽象接口,将高层模块(业务逻辑)从低层模块(具体对象创建)的泥潭中解放出来的经典实践。理解它,用好它,你的代码离“高内聚、低耦合”的目标就更近了一步。