尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
用披萨店实战简单工厂模式:从设计到扩展的完整指南
1. 为什么我拿“披萨店”来练简单工厂模式第一次接触工厂模式那会儿我盯着书上那些“产品族”“抽象工厂”“工厂方法”的定义看了半天脑子里全是浆糊。后来带我的老哥说了一句话点醒我你别把它当设计模式你就当自己在盘一家店。这句话我记到现在。所以当我需要给团队新人讲简单工厂模式的时候我从来不用那些UML图开场我直接甩一个“披萨店”的场景过去——因为披萨店这个模型天然就带着“下单、制作、交付”的完整链路而且披萨种类多、制作流程有共性正好能把“把对象的创建和使用分开”这件事讲透。简单工厂模式说白了就一句话客户端不自己new对象而是把“要什么”告诉一个工厂让工厂替你new。放到披萨店里顾客不会跑到后厨自己揉面团顾客只负责点单后厨负责按单做饼。这个“后厨”就是工厂“披萨”就是产品“点单”这个动作就是客户端传参。这个类比之所以好用是因为它把三个角色客户端、工厂、产品的职责边界划得清清楚楚而且每个人都能立刻理解“为什么顾客不该进后厨”。这篇内容我打算把整个披萨店的模拟从零搭起来包括为什么这么设计、每个类怎么落地、参数怎么传、异常怎么兜、后面想扩展新口味时怎么改代码最省事。适合刚学完面向对象、想找个能跑起来的例子练手的朋友也适合已经写过一些代码但对“工厂”这个概念还停留在背定义阶段的同行。我会用Java来写主体代码因为它的类型系统能把“多态”这件事展示得比较直观但思路换成Python、C#、TypeScript都一样核心是那个“创建逻辑收口”的思想语言只是壳。我先把结论摆前面简单工厂模式最大的价值不是“高级”而是把变化关进一个笼子里。披萨店今天加个榴莲口味、明天加个麻辣小龙虾口味顾客点单的代码一行都不用动你只需要改工厂里那个switch。这就是它存在的全部理由。至于它为什么叫“简单”工厂而不是“工厂方法”后面我会专门拿一节讲清楚两者的分水岭在哪因为这是面试和实际选型里最容易混淆的点。2. 先把披萨店的角色关系捋清楚2.1 三个角色一条职责链在动手写代码之前我习惯先把角色画在纸上。披萨店这个模型里角色其实只有三个但每个角色的边界必须卡死否则写着写着就乱了。披萨Product抽象产品定义所有披萨共有的行为比如准备、烘烤、切片、装盒。它是个抽象类或者接口本身不能被直接实例化因为“披萨”这个概念太泛了你得说清楚是哪种披萨。具体披萨ConcreteProduct芝士披萨、海鲜披萨、素食披萨等等每个都继承抽象披萨实现自己的准备环节比如芝士披萨要撒双层芝士海鲜披萨要铺虾仁。披萨工厂Factory唯一负责“根据订单类型创建对应披萨对象”的角色。它内部通常是一个根据类型参数分支的逻辑返回抽象披萨类型。客户端Client也就是“顾客”或者“前台”它只持有工厂引用调用工厂的创建方法拿到披萨然后调用披萨的公共方法。它完全不知道具体披萨类叫什么名字。这条链的关键在于客户端依赖的是抽象披萨和工厂而不是任何具体披萨类。这就是依赖倒置的味道——高层模块不依赖低层模块两者都依赖抽象。很多新手写工厂模式写着写着客户端里又出现了new CheesePizza()那就等于顾客自己溜进后厨了工厂白建。2.2 为什么不让客户端直接new有人会问我就一个披萨店三种披萨客户端直接new不就完了搞个工厂是不是过度设计这个问题问得好我当初也这么想过。答案是看变化点在哪。如果披萨种类永远不变那确实没必要上工厂。但现实是菜单会变。假设客户端代码散落在十个地方每个地方都写着if type cheese new CheesePizza() else if ...某天你要加一个“榴莲披萨”这十处全得改漏一处就是bug。而用了工厂之后创建逻辑只有一份加口味只改工厂一处客户端零改动。这就是“把变化关进笼子”的实际收益。我用一个表格把两种做法的差异摆出来你一看就明白取舍在哪对比维度客户端直接new走简单工厂新增口味改动点所有创建处都要改只改工厂一处客户端对具体类的依赖强依赖编译期绑定只依赖抽象创建逻辑复用重复散落集中复用代码可读性分支多了很乱创建逻辑收口清爽适用场景对象种类极少且稳定种类会变、创建有共性逻辑所以我的判断标准很朴素只要“创建哪种对象”这个决策会在多处出现或者未来可能增加新种类就值得上简单工厂。披萨店正好两条都占拿来练手再合适不过。2.3 简单工厂和工厂方法的分水岭这里必须插一句因为太多人把两者搞混。简单工厂是一个工厂类管所有产品靠参数分支决定造哪个工厂方法是每个产品配一个专属工厂靠多态决定造哪个。前者结构简单但工厂类会随产品增多而膨胀后者扩展性好但类数量爆炸。披萨店这个规模三种五种口味简单工厂完全够用硬上工厂方法就是杀鸡用牛刀。我个人的经验是产品种类在十个以内、且创建逻辑不复杂优先简单工厂一旦产品族开始分层比如“纽约风味披萨”和“芝加哥风味披萨”各自还有子系列才考虑往工厂方法或抽象工厂迁移。这个判断在后面的扩展章节我会再展开。3. 披萨店核心代码的落地细节3.1 抽象披萨把公共流程钉死先写抽象产品。这里有个设计上的小心思我把“准备、烘烤、切片、装盒”这四个步骤做成抽象类里的模板方法其中prepare()是抽象方法交给子类实现其余三步是通用实现。这样做的好处是所有披萨的制作流程骨架统一子类只需要关心“我这种披萨准备阶段有什么特殊动作”。public abstract class Pizza { protected String name; protected String dough; protected String sauce; public void prepare() { System.out.println(开始准备 name); System.out.println(揉面 dough); System.out.println(涂酱 sauce); addToppings(); } protected abstract void addToppings(); 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; } }注意addToppings()是抽象方法这就是子类的差异化入口。为什么把“加配料”单独抽出来而不是整个prepare()都抽象因为揉面和涂酱对所有披萨是共性的只有配料不同。能复用的一律复用只把真正变化的部分抽象出去这是模板方法加简单工厂组合使用时的一个实用技巧能显著减少子类里的重复代码。3.2 具体披萨差异化只写在配料里有了上面的骨架具体披萨类就非常薄了。芝士披萨、海鲜披萨、素食披萨各自只需要实现addToppings()构造函数里设置好名字、面团类型和酱料类型。public class CheesePizza extends Pizza { public CheesePizza() { name 经典芝士披萨; dough 薄脆面团; sauce 番茄酱; } Override protected void addToppings() { System.out.println(撒上双层马苏里拉芝士); } } public class SeafoodPizza extends Pizza { public SeafoodPizza() { name 海鲜至尊披萨; dough 厚底面团; sauce 蒜香白酱; } Override protected void addToppings() { System.out.println(铺上虾仁、鱿鱼圈、蟹柳); } } public class VeggiePizza extends Pizza { public VeggiePizza() { name 田园素食披萨; dough 全麦面团; sauce 罗勒青酱; } Override protected void addToppings() { System.out.println(铺上彩椒、蘑菇、洋葱、橄榄); } }写到这里你会发现具体披萨类里几乎没有逻辑全是数据赋值和一行打印。这是好事说明职责划分对了。具体产品类越薄说明抽象层设计得越到位。如果你发现某个具体披萨类里写了一堆if-else那多半是抽象没抽干净得回头调整。3.3 工厂类创建逻辑的唯一收口点工厂类是简单工厂模式的心脏。它通常只有一个静态方法所以也叫静态工厂接收一个类型标识返回抽象披萨。内部用分支决定实例化哪个具体类。public class PizzaFactory { public static Pizza createPizza(String type) { if (type null) { throw new IllegalArgumentException(披萨类型不能为空); } switch (type.toLowerCase()) { case cheese: return new CheesePizza(); case seafood: return new SeafoodPizza(); case veggie: return new VeggiePizza(); default: throw new IllegalArgumentException(未知的披萨类型 type); } } }几个细节值得说。第一入参做了null判断和toLowerCase归一化因为客户端传进来的字符串大小写不可控Cheese和cheese应该等价。第二未知类型直接抛异常而不是返回null返回null会让调用方在后续使用时才炸排查成本高抛异常能在创建点就暴露问题这是我在实际项目里踩过坑之后改的习惯。第三方法用static因为工厂本身不需要状态静态方法调用最省事客户端连工厂对象都不用持有。注意如果你的工厂创建过程需要读取配置、连接资源或者有状态那就别用静态方法改成实例方法并配合单例管理。静态工厂适合“纯创建、无状态”的场景披萨店这个例子正好符合。3.4 客户端只点单不碰后厨客户端代码是整个模式价值的最终体现。它只做三件事调工厂拿披萨、调披萨的公共方法、打印结果。全程不出现任何具体披萨类名。public class PizzaStore { public static void main(String[] args) { orderPizza(cheese); orderPizza(seafood); orderPizza(veggie); } private static void orderPizza(String type) { Pizza pizza PizzaFactory.createPizza(type); System.out.println( 订单 pizza.getName() ); pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); System.out.println(); } }orderPizza方法里Pizza这个类型是抽象的PizzaFactory.createPizza返回的也是抽象类型客户端对具体实现一无所知。这就是“面向抽象编程”的落地。你把这四段代码拼起来跑一遍控制台会依次输出三种披萨的完整制作流程芝士披萨会打印“撒上双层马苏里拉芝士”海鲜披萨会打印虾仁鱿鱼各走各的配料逻辑但烘烤切片装盒的流程完全一致。这个运行结果本身就是对“模板方法复用共性、工厂收口创建”最好的说明。4. 扩展新口味时到底要改哪里4.1 加一个“榴莲披萨”的完整操作这是检验工厂模式是否真的解耦的最好方式。假设老板说下周上榴莲披萨我们看看要动哪些文件。第一步新建DurianPizza类继承Pizza实现addToppings()public class DurianPizza extends Pizza { public DurianPizza() { name 猫山王榴莲披萨; dough 芝士边面团; sauce 炼乳酱; } Override protected void addToppings() { System.out.println(铺满猫山王榴莲果肉撒少量马苏里拉); } }第二步在PizzaFactory的switch里加一个casecase durian: return new DurianPizza();就这两步。客户端PizzaStore一行没改orderPizza(durian)直接就能用。这就是“对扩展开放、对修改关闭”的直观感受——虽然简单工厂严格来说违反了开闭原则因为改工厂类本身也是修改但它把修改收敛到了一个点相比散落各处的new已经是巨大进步。我在实际项目里衡量一个模式值不值就看“新增需求时改动点的数量”简单工厂在这个指标上表现相当好。4.2 用枚举替代字符串参数的改造字符串参数有个老毛病拼写错误编译期发现不了chese少个d运行时才抛异常。产品种类多了之后这种低级错误防不胜防。我的做法是上一套枚举把类型约束在编译期。public enum PizzaType { CHEESE, SEAFOOD, VEGGIE, DURIAN }工厂方法签名改成接收枚举public static Pizza createPizza(PizzaType type) { if (type null) { throw new IllegalArgumentException(披萨类型不能为空); } switch (type) { case CHEESE: return new CheesePizza(); case SEAFOOD: return new SeafoodPizza(); case VEGGIE: return new VeggiePizza(); case DURIAN: return new DurianPizza(); default: throw new IllegalArgumentException(未支持的披萨类型 type); } }这么一改客户端传错类型直接编译不过比运行时抛异常又提前了一步。代价是新增口味时要同时改枚举和工厂两处但这两处都在“创建侧”客户端依然零感知。我个人在中小型项目里更倾向枚举方案因为它的类型安全性带来的收益远大于多改一个文件的成本。4.3 配置化创建把switch也干掉如果产品种类多到几十个switch会变得很长每次加口味都要动工厂源码、重新编译。这时候可以再进一步用“注册表”的方式把类型和创建逻辑映射起来工厂里只留查表逻辑。public class PizzaFactory { private static final MapPizzaType, SupplierPizza REGISTRY new HashMap(); static { REGISTRY.put(PizzaType.CHEESE, CheesePizza::new); REGISTRY.put(PizzaType.SEAFOOD, SeafoodPizza::new); REGISTRY.put(PizzaType.VEGGIE, VeggiePizza::new); REGISTRY.put(PizzaType.DURIAN, DurianPizza::new); } public static Pizza createPizza(PizzaType type) { SupplierPizza supplier REGISTRY.get(type); if (supplier null) { throw new IllegalArgumentException(未注册的披萨类型 type); } return supplier.get(); } }这个版本里工厂方法本身不再随产品增加而修改新增口味只需要往REGISTRY里put一行。严格说这已经有点“注册式工厂”的味道了但它依然是简单工厂的变体——一个工厂管所有产品靠参数查表。这种写法在插件化、可配置化的场景里特别常见我在做多数据源适配、多支付渠道对接时都用过类似结构。从switch到枚举再到注册表是一条很自然的演进路线你可以根据产品规模选择停在哪一档。5. 实操中踩过的坑和排查清单5.1 五个高频问题与解决思路这套代码看着简单但新手照着写十有八九会在这几个地方卡住。我把它们整理成速查表遇到问题直接对号入座。问题现象根因解决思路客户端编译报错找不到具体披萨类客户端直接new了具体类没走工厂检查客户端是否只依赖Pizza抽象和工厂传Cheese报未知类型大小写未归一化工厂内统一toLowerCase()或改用枚举新增口味后老代码行为变了工厂分支顺序或默认分支写错检查switch的default是否误返回了某个具体类工厂方法越来越长难以维护产品过多switch膨胀迁移到枚举注册表方案单元测试难写工厂用静态方法且内部有硬编码依赖改为实例方法依赖注入或对工厂做薄封装这里面我重点说第一条。很多人写着写着在客户端里为了“方便”又加了一句if (pizza instanceof CheesePizza)去做特殊处理这一加就把抽象依赖破坏了。客户端一旦开始判断具体类型工厂的意义就没了。如果确实有差异化行为正确做法是把行为抽象到Pizza里让子类各自实现而不是在客户端做类型判断。这是面向对象里“用多态替代条件分支”的基本功。5.2 我个人的三条实操心得第一条心得先写客户端再写工厂。听起来反直觉但这样能逼着你先想清楚“客户端到底需要什么接口”然后工厂和产品去满足这个接口而不是先埋头写一堆产品类最后发现客户端用不上。我早期做模块拆分时经常犯“先造轮子后找场景”的毛病后来改成从调用方倒推设计出来的接口干净很多。第二条心得工厂的异常信息要带上下文。throw new IllegalArgumentException(未知类型)这种信息在生产环境里等于没说排查的人根本不知道是谁传了什么。改成未知的披萨类型 type一眼就能定位。这个习惯我从披萨店这种练习项目一直带到线上系统收益巨大。第三条心得别为了模式而模式。我见过有人给只有两个产品、且永远不变的工具类硬套工厂结果多出三个文件读代码的人还得先理解工厂再看业务纯属负优化。简单工厂的适用边界前面表格里列过了产品稳定且少的时候直接new反而是最清晰的。模式是工具不是KPI。5.3 从披萨店延伸到真实场景披萨店练熟了你会发现这套结构在真实项目里到处都是影子。比如支付渠道对接微信、支付宝、银行卡客户端只传渠道类型工厂返回对应的支付处理器比如消息推送短信、邮件、站内信工厂按类型造推送器再比如文件解析CSV、Excel、JSON工厂按扩展名返回解析器。这些场景的共同点都是“同一族对象、创建逻辑有分支、客户端只关心抽象行为”正好是简单工厂的靶心。我拿支付渠道举个例子结构几乎和披萨店一一对应PaymentProcessor是抽象披萨WechatProcessor、AlipayProcessor是具体披萨PaymentFactory是披萨工厂下单服务是客户端。你甚至可以把披萨店代码里的类名整体替换一下逻辑完全不用动。这就是为什么我说拿披萨店练手不亏——它练的不是一个玩具而是一个能直接迁移到生产代码里的思维模型。6. 把练习项目跑起来之后的下一步代码跑通只是起点。我建议你在披萨店基础上再做两个小改造把理解再往下压一层。第一个改造是给工厂加一个“今日推荐”功能让工厂除了按类型创建还能随机返回一个披萨体会一下工厂方法签名变化对客户端的影响。第二个改造是把Pizza的prepare流程里加一个“质检”步骤但只对海鲜披萨生效逼自己去想“这个差异化该放在子类还是放在模板方法里”这个思考过程比看十篇模式讲解都管用。我自己带新人的时候最后会让他们回答一个问题如果披萨店要开分店每个分店有自己的特色菜单简单工厂还够用吗答案通常是不够因为“一个工厂管所有”开始吃力了这时候才是引入工厂方法或者抽象工厂的时机。能自己走到这个判断上说明简单工厂是真的吃透了而不是背下来的。最后分享一个我调试这类代码的小习惯在工厂的每个case里打一行日志输出“创建了XX披萨”。跑起来之后控制台的创建顺序和客户端的点单顺序一对照哪个环节出的问题一目了然。这个习惯在排查“为什么拿到的是错误类型”这类问题时特别省时间比打断点还快。披萨店虽小但这些工程习惯从练习项目里养起来带到哪儿都用得上。
RELATED

相关推荐

ponytail插件使用指南:轻量级任务聚合工具的设计与实操

ponytail插件使用指南:轻量级任务聚合工具的设计与实操

1. 从“ponytail”这个词说起:它到底是什么第一次看到“ponytail”这个项目标题,很多人脑子里蹦出来的画面大概是扎起来的马尾辫。没错,字面意思确实是马尾辫,但在技术圈和工具圈里,ponytail 早就不是发型那么简单了。…

📅 2026/10/27 22:23:50
SpringBoot共享单车用户大数据分析:从订单数据到运营决策指标

SpringBoot共享单车用户大数据分析:从订单数据到运营决策指标

简介:面向高校大数据与Java开发课程设计场景,这份项目以Spring Boot为后端框架,集成Hadoop、Hive完成共享单车用户数据的存储与离线分析,并通过ECharts和百度地图API实现可视化展示,适合用作大二、大三学生的课设参考或…

📅 2026/10/27 22:23:50
Android USB串口驱动全链路解析:PL2303/CH340/FT232/CP2102支持实战

Android USB串口驱动全链路解析:PL2303/CH340/FT232/CP2102支持实战

简介:本资源是面向Android嵌入式开发与物联网调试场景的USB转串口通用驱动集成方案,专为需在Android设备上稳定接入PL2303、CH34X(CH340/CH341)、FT232系列及CP2102等主流芯片串口设备的开发者设计,解决原生Android缺乏…

📅 2026/10/27 22:23:50
MORE NEWS

更多资讯

📰

Lua脚本在游戏动态道具系统中的设计与优化

1. 项目概述:Lua脚本在现代游戏开发中的独特价值十年前我第一次接触游戏开发时,就发现了一个有趣的现象:几乎所有大型游戏项目都在使用Lua。从《魔兽世界》的插件系统到《愤怒的小鸟》的关卡逻辑,这个轻量级脚本语言无处不在。直到…

📰

高校心理咨询系统开发:SpringBoot+小程序技术解析

1. 项目背景与核心价值 高校心理咨询系统是一个典型的"互联网心理健康"应用场景。随着大学生心理问题日益受到重视,传统线下咨询模式暴露出诸多痛点:学生预约难、咨询师资源分配不均、咨询记录管理混乱、隐私保护不足等。这套基于SpringBoot小…

📰

校园点餐管理系统源代码:从课设到高并发实战的避坑指南

简介:这份校园点餐管理系统源代码面向高校计算机与软件工程专业学生、Java Web初学者及需要课程设计或毕业设计参考的开发者,基于Java、Spring、Spring MVC、MyBatis、Tomcat与Maven构建,完整呈现从用户注册登录、菜单浏览、订单提交与状态追…

📰

Python文件读写与CSV操作在游戏测试中的高效应用

1. 为什么游戏测试工程师必须掌握文件读写与CSV操作 在游戏测试领域,数据处理能力往往是被低估的核心竞争力。我见过太多测试工程师把大量时间浪费在手工记录和整理测试数据上,而真正高效的测试专家则通过Python脚本将这个过程自动化。以最近测试的一款M…

📰

text-to-CAD技术解析:从工程语义理解到STEP/URDF/DXF多格式交付

1. 什么是text-to-CAD:不是“文字变图纸”的魔法,而是工程语义的精准翻译text-to-CAD这个标题乍看像AI绘画的CAD版——输入“一个带M6螺纹孔的铝制支架,长120mm,宽60mm,厚10mm,底部有4个Φ5安装孔”&#x…

📰

impeccable用法深度解析:从词源到搭配,彻底告别与perfect、flawless的混用

最近在好几个英文视频和社媒评论区里都撞见同一个词:impeccable。有人用它夸博主的穿搭,有人用来形容某场演出的临场发挥,弹幕里中文观众零零散散地飘着"这词到底啥意思""跟perfect有区别吗"。作为一个常年和英语打交道的…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬