1. 为什么我拿“披萨店”来练简单工厂模式第一次接触工厂模式那会儿我盯着书上那些Product、ConcreteProduct、Factory的类图看了半天概念都懂可一合上书就写不出来。后来我换了个思路别去背那些抽象名词找一个生活里天天见的场景把它“演”一遍。披萨店就是最合适的靶子——它有明确的菜单、有固定的制作流程、有顾客点单这个触发动作而且披萨种类会随季节和促销不断变化天然带着“对象创建该不该集中管理”的矛盾。简单工厂模式说白了就一句话把“造对象”这件事从调用方手里拿走塞进一个专门的工厂里调用方只负责说“我要什么”不关心“怎么造”。放到披萨店场景里顾客不会跑到后厨去揉面、抹酱、撒料他只需要对着菜单说“来一份榴莲披萨”剩下的由后厨统一调度。这个“后厨”就是工厂“菜单上的名字”就是传入工厂的参数“端出来的披萨”就是工厂返回的对象。这个模拟项目适合谁我觉得有三类人最该动手写一遍。第一类是刚学完面向对象、对多态和接口还半懂不懂的初学者披萨店能把“父类引用指向子类对象”这件事变得非常具体第二类是准备面试、被问到“工厂模式有几种、区别是什么”的求职者自己敲一遍比背八股强十倍第三类是已经工作但代码里到处new、想重构又不知道从哪下手的人披萨店是个零风险的练手沙盘。我打算用 Java 来写这个模拟原因是它的接口和继承语义清晰类型系统能逼着你想清楚“谁是父类、谁是子类、工厂返回什么类型”。当然你用 Python、C#、TypeScript 甚至 Go 都能复现同样的思路语言只是壳子核心是那套“集中创建 参数分发”的结构。下面我会从设计思路、类结构拆解、完整实现、常见坑四个层面把它讲透代码可以直接抄去跑跑完你再去翻那些模式书会发现那些拗口的定义突然就顺了。2. 先把“披萨店”的业务拆明白再动手写类2.1 从顾客点单到出餐到底发生了什么在写任何一行代码之前我习惯先把业务流程用大白话走一遍这一步偷懒后面类结构一定乱。顾客走进店里看菜单选一个口味告诉店员店员把订单丢给后厨后厨根据口味决定用哪套配方、哪个烤盘、烤多久烤好之后交给店员店员端给顾客。整个链条里顾客只跟“口味名字”打交道后厨只跟“配方和工序”打交道两边通过“订单”这个信息解耦。这个流程里藏着一个关键判断“根据口味决定做哪种披萨”这个决策应该放在哪一层如果放在顾客那一层顾客就得知道每种披萨怎么做显然荒谬如果放在每个披萨自己的类里那顾客还得先new一个具体披萨才能问它怎么做也说不通。唯一合理的位置是中间那个“后厨”它掌握全部配方对外只暴露一个“按名字出餐”的入口。这就是简单工厂的核心动机——创建逻辑集中化。我把这个判断再往深挖一层。假设不加工厂顾客代码会写成这样Pizza p new DurianPizza();。看起来没问题但一旦菜单改名、新增口味、或者某个口味下架所有new的地方都得改。更麻烦的是如果披萨的构造过程变复杂了比如要先校验库存、要记录日志、要按门店调整配方这些逻辑会散落在每一个new的调用点。工厂把这些“创建前后的杂事”收拢到一处调用方永远只写factory.create(榴莲)菜单怎么变都跟它无关。这就是我在这个模拟里最想让大家体会到的“解耦感”。2.2 为什么选简单工厂而不是工厂方法或抽象工厂这里必须说清楚不然写完容易被人问住。简单工厂、工厂方法、抽象工厂是三个不同重量级的东西披萨店这个场景用简单工厂是“刚好够用”不是“越高级越好”。简单工厂的结构是一个工厂类一个create方法方法里用if/else或switch根据参数决定new哪个具体类。它的优点是结构极简、一个类搞定、调用方零负担缺点是新增口味要改工厂类的switch违反开闭原则。工厂方法是给每种披萨配一个专属工厂子类新增口味时新增工厂类而不改老代码符合开闭原则但类的数量翻倍。抽象工厂是“产品族”级别的比如同时要造“披萨”和“饮料”两套产品线简单工厂和工厂方法都只能管一条线。披萨店这个模拟产品线只有“披萨”一条口味变化虽然频繁但总量可控用简单工厂的switch完全撑得住而且代码量小、一眼看懂特别适合教学和练手。我实测下来初学者用简单工厂入门理解成本最低等他把switch改到第三遍觉得烦了再引入工厂方法那个“为什么要多态工厂”的痛点他自己就悟出来了。所以这个模拟我坚持用简单工厂不是因为它最优雅而是因为它最适合作为理解工厂思想的起点。2.3 类结构规划父类、子类、工厂各管什么规划类结构时我遵循一个原则父类管“不变”子类管“变化”工厂管“选择”。披萨的制作流程里“准备面团、抹酱、撒料、烘烤、切片、装盒”这几步是每款披萨都要走的属于不变的部分放进父类的prepare方法里每一步具体用什么面团、什么酱、什么料是变化的交给子类去覆盖。工厂则只负责“根据名字选子类”选完就撒手不掺和制作细节。具体到类我设计了这几个Pizza抽象父类持有name、dough、sauce、toppings几个字段定义prepare()模板方法和bake()、cut()、box()几个通用动作还有一个getName()。CheesePizza、VeggiePizza、DurianPizza、SeafoodPizza四个具体子类各自在构造时设定名字并覆盖prepare()里的配料环节。SimplePizzaFactory工厂类只有一个createPizza(String type)方法内部用switch分发。PizzaStore门店类持有工厂引用orderPizza(String type)方法负责“下单—制作—交付”的编排。PizzaShopDemo入口类模拟顾客点单。这个结构里有个细节值得说PizzaStore不直接new披萨而是持有一个SimplePizzaFactory的引用。这样做的好处是门店和工厂可以独立替换——今天用中央厨房工厂明天换成门店自建工厂门店代码一行不用改。这就是“面向接口/抽象编程”在生活场景里的落地门店只认“能出餐的工厂”这个契约不认具体是哪家工厂。3. 核心代码逐层拆解与实操要点3.1 抽象父类 Pizza把制作流程的骨架搭好父类我写成抽象类而不是接口原因是披萨有共享的字段名字、面团、酱料、配料列表和共享的方法实现烘烤、切片、装盒用抽象类能把这些“公共资产”直接沉淀下来子类只覆盖需要变的部分。如果写成接口每个子类都得重复声明字段和实现通用方法代码会臃肿。public abstract class Pizza { protected String name; protected String dough; protected String sauce; protected ListString toppings new ArrayList(); public void prepare() { System.out.println(开始制作 name); System.out.println( 揉面团 dough); System.out.println( 抹酱料 sauce); System.out.println( 撒配料 toppings); } public void bake() { System.out.println( 烘烤 25 分钟温度 220 度); } public void cut() { System.out.println( 沿对角线切成 8 块); } public void box() { System.out.println( 装入官方披萨盒); } public String getName() { return name; } }这里prepare()我故意写成“打印 使用字段”的形式而不是让子类完全重写整个方法。这是模板方法思想的雏形父类定好“先揉面、再抹酱、再撒料”的顺序子类只需要在构造时把dough、sauce、toppings这几个字段填对流程自然就跑通了。这样做的好处是流程顺序只有一份不会出现某个子类忘了切片或者顺序颠倒。注意toppings用ListString而不是String是因为真实披萨的配料是多个用列表能自然表达“撒多种料”的语义也方便后续扩展“加双份芝士”这类需求。初学者容易图省事用字符串拼接后面想统计配料种类时就抓瞎了。3.2 四个具体披萨子类变化点集中在这里子类的职责非常单一在构造时把“我是谁、我用什么料”说清楚。我写了四个口味覆盖了“纯素”“重口”“海鲜”“水果”几个典型方向方便演示工厂的分发逻辑。public class CheesePizza extends Pizza { public CheesePizza() { name 经典芝士披萨; dough 薄脆面团; sauce 番茄酱; toppings.add(马苏里拉芝士); toppings.add(帕玛森芝士); } } public class VeggiePizza extends Pizza { public VeggiePizza() { name 田园蔬菜披萨; dough 全麦面团; sauce 罗勒青酱; toppings.add(彩椒); toppings.add(洋葱); toppings.add(蘑菇); toppings.add(橄榄); } } public class DurianPizza extends Pizza { public DurianPizza() { name 榴莲芝士披萨; dough 厚底面团; sauce 炼乳酱; toppings.add(榴莲果肉); toppings.add(双倍芝士); } } public class SeafoodPizza extends Pizza { public SeafoodPizza() { name 海鲜至尊披萨; dough 薄脆面团; sauce 蒜香白酱; toppings.add(虾仁); toppings.add(鱿鱼圈); toppings.add(蟹柳); } }写子类时有个实操心得字段赋值放在构造器里不要放在字段声明处。比如name 经典芝士披萨写在构造器里而不是protected String name 经典芝士披萨。原因是父类字段声明处赋值会在父类构造时执行子类构造器再覆盖虽然结果一样但语义上“子类决定自己的名字”应该发生在子类构造阶段读代码的人一眼能看出这是子类的定制点。这个细节不影响运行但影响可读性团队协作时很值钱。3.3 简单工厂类一个 switch 撑起整个后厨工厂类是简单工厂模式的灵魂也是被吐槽最多的地方——所有口味的分发都挤在一个switch里。但在这个模拟里我恰恰要保留这个switch因为它是理解“创建逻辑集中”的最直观载体。public class SimplePizzaFactory { public Pizza createPizza(String type) { Pizza pizza null; switch (type) { case 芝士: pizza new CheesePizza(); break; case 蔬菜: pizza new VeggiePizza(); break; case 榴莲: pizza new DurianPizza(); break; case 海鲜: pizza new SeafoodPizza(); break; default: throw new IllegalArgumentException(菜单上没有这个口味 type); } return pizza; } }这段代码有几个点值得掰开说。第一返回类型是Pizza而不是具体子类这是多态的关键——调用方拿到的是父类引用具体是哪个子类由工厂内部决定调用方不需要知道。第二default分支我选择抛异常而不是返回null因为返回null会让调用方在后续pizza.prepare()时抛空指针错误位置离真正的原因很远排查起来费劲直接抛带信息的异常问题当场暴露。第三switch的case用中文口味名是为了贴合“顾客说口味”的场景真实项目里建议用枚举或常量避免字符串拼写错误。提示如果你用 Java 14 以上这个switch可以写成箭头语法case 芝士 - new CheesePizza();更简洁且不会漏break。但教学场景我保留传统写法因为break漏写导致的“穿透”是初学者经典坑亲手踩一次印象更深。3.4 门店类 PizzaStore编排下单到交付的全流程门店类不负责造披萨它负责“编排”。顾客说口味它调工厂拿披萨然后按固定流程走制作、烘烤、切片、装盒。这个类体现了“单一职责”——门店只管流程编排创建交给工厂制作交给披萨自己。public class PizzaStore { private final SimplePizzaFactory factory; public PizzaStore(SimplePizzaFactory factory) { this.factory factory; } public Pizza orderPizza(String type) { Pizza pizza factory.createPizza(type); System.out.println(门店收到订单 pizza.getName()); pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); System.out.println(出餐完成请取餐。\n); return pizza; } }构造函数注入工厂而不是在门店里new SimplePizzaFactory()这是依赖注入的朴素形态。好处是测试时可以传入一个“假工厂”来隔离门店逻辑生产时可以传入“真工厂”。我见过不少初学者在门店里直接new工厂结果门店和工厂焊死想换工厂就得改门店源码白白丢掉了简单工厂带来的那点灵活性。orderPizza的流程顺序也值得注意先prepare再bake先cut再box这个顺序不能乱。真实披萨店如果先装盒再切片盒子就废了。代码里的顺序就是业务规则的映射写的时候脑子里要过一遍真实场景别为了省事随便调换。3.5 入口类模拟一次完整的点单入口类把上面所有零件串起来模拟顾客连续点几单顺便验证工厂分发是否正确。public class PizzaShopDemo { public static void main(String[] args) { SimplePizzaFactory factory new SimplePizzaFactory(); PizzaStore store new PizzaStore(factory); store.orderPizza(芝士); store.orderPizza(榴莲); store.orderPizza(海鲜); try { store.orderPizza(麻辣香锅); } catch (IllegalArgumentException e) { System.out.println(点单失败 e.getMessage()); } } }跑起来之后控制台会依次打印每种披萨的制作过程最后那个不存在的口味会触发异常并被捕获演示了工厂对非法输入的处理。我特意在入口里加了异常捕获是想让大家看到“工厂抛异常、调用方决定怎么处理”这个分工——工厂只负责报告“没有这个口味”至于门店是提示顾客换一个还是直接报错那是门店的事。4. 实操中踩过的坑与排查实录4.1 常见问题速查表下面这张表是我自己写这个模拟以及带别人写时反复出现的几个问题按出现频率排序遇到报错可以先对照排查。现象可能原因排查与解决编译报错“无法实例化抽象类 Pizza”在门店或入口里直接new Pizza()披萨只能由工厂通过子类创建检查是否有绕过工厂的new运行时空指针位置在pizza.prepare()工厂switch没有匹配分支且返回了null给default分支抛异常或调用前判空某个口味打印的配料是空的子类构造器里忘了toppings.add(...)检查子类构造器toppings是父类初始化的列表子类只负责添加新增口味后老口味行为异常switch漏写break导致穿透每个case后检查break或改用箭头语法门店换工厂要改源码门店内部new了具体工厂改为构造器注入工厂引用中文口味名匹配不上输入字符串有空格或全半角差异输入前trim()或改用枚举统一管理口味4.2 三个我亲自踩过的坑第一个坑是在父类构造器里调用可覆盖方法。我一开始想在Pizza构造器里直接调prepare()想着“创建完就准备”结果子类字段还没赋值prepare()打印出来全是null。原因是 Java 构造器执行顺序是父类先于子类父类构造器跑的时候子类字段赋值还没发生。后来我把prepare()的调用挪到门店的orderPizza里等对象完全构造好再调问题消失。这个坑很经典凡是“父类构造器里调子类可能覆盖的方法”都要警惕。第二个坑是工厂返回类型写成具体子类。有次我图省事把createPizza的返回类型写成CheesePizza结果其他口味全编译不过。工厂方法的返回类型必须是所有产品的共同父类这里是Pizza这样调用方才能用统一类型接收多态才有意义。这个错误本质是没理解“工厂返回的是抽象不是具体”。第三个坑是把制作逻辑写进工厂。我一度觉得“工厂既然负责造披萨那顺便把配料也配好呗”于是在switch里写了一堆pizza.setToppings(...)。结果工厂越来越胖新增口味要改工厂、调配料也要改工厂两个变化原因挤在一个类里。后来我把配料设定挪回子类构造器工厂只留“选哪个子类”这一件事职责立刻清爽。这个教训让我记住工厂管选择产品管自己边界一旦模糊后面维护就是灾难。4.3 一个容易被忽略的细节口味参数的类型口味参数用String最直观但字符串是“弱类型”拼错一个字母编译器不报错运行时才炸。我在模拟里保留String是为了贴近“顾客口头点单”的场景但如果你要把这个模式用到真实项目强烈建议换成枚举public enum PizzaType { CHEESE, VEGGIE, DURIAN, SEAFOOD }换成枚举后switch的分支由编译器检查完整性新增枚举值如果忘了加caseIDE 会提示。这个改动很小但对“菜单频繁变动”的场景价值很大。我在自己的练习项目里两种都写过字符串版适合演示枚举版适合落地你可以根据目的选。5. 从“能跑”到“好用”的扩展思路5.1 给工厂加一层缓存或库存校验基础版工厂每次调用都new一个新对象这在披萨场景没问题因为每份披萨本来就该是新的。但如果产品创建成本很高比如要查数据库、要读配置文件可以在工厂里加缓存用一个MapString, Pizza存已创建的实例命中就返回没命中再new。加缓存的位置就在createPizza方法开头几行代码的事但要想清楚“产品是否允许共享”——披萨显然不行你不想和别人吃同一份但配置对象、连接池这类就适合。库存校验也是同理在switch之前先查“这个口味今天还有没有料”没有就抛“售罄”异常。这个逻辑放工厂里比放门店里合适因为“有没有料”是后厨的事门店不该知道。5.2 用配置文件驱动菜单避免改代码switch最大的痛点是新增口味要改源码、重新编译。一个轻量改进是把“口味名 → 子类全限定名”的映射写进配置文件工厂用反射根据配置Class.forName(...).newInstance()创建对象。这样新增口味只需加一个类、改一行配置工厂代码不动。反射有性能开销但披萨店这种低频创建场景完全无感。这个改法我在一个练习项目里试过代码量增加不多但“开闭原则”的体验感很强——新增口味真的不用碰工厂类。5.3 什么时候该升级到工厂方法当你发现switch里的case超过十个、每次加口味都要动工厂、团队里两个人同时改工厂频繁冲突时就是升级信号。工厂方法的做法是给每个口味配一个PizzaFactory子类门店持有一个抽象工厂具体用哪个由门店子类决定。代价是类数量翻倍收益是新增口味只加类不改老代码。我的经验是口味少于八个、变动不频繁简单工厂足够超过这个量级或者多人协作再考虑工厂方法。别一上来就上重武器简单工厂的“简单”本身就是优点。6. 我个人在这个模拟里最大的收获写这个披萨店模拟之前我对工厂模式的理解停留在“知道有这么个东西”。写完并反复改了几遍之后我最大的收获不是记住了switch怎么写而是真正体会到了“把变化关进笼子”这件事的价值。披萨的口味会变、配料会变、菜单会变但门店的下单流程、披萨的制作骨架、工厂的对外契约这三样是稳定的。简单工厂做的事情就是把“变”的部分集中到一个switch里让“不变”的部分干干净净。后来我在实际项目里遇到“根据类型创建不同处理器”的需求脑子里第一个蹦出来的就是披萨店那个switch然后我会问自己这个switch会不会越来越长如果会是不是该升级这种“从场景反推模式”的直觉是看多少遍书都换不来的只能靠亲手敲、亲手改、亲手踩坑。如果你也在学设计模式我建议你别急着看下一个模式先把披萨店这个模拟从头到尾自己写一遍再故意加两个口味、故意把工厂写胖、故意踩一次空指针那个“哦原来是这样”的瞬间比任何教程都值钱。