深夜加班到十一点,小林饿得前胸贴后背,只想吃一碗热乎乎的面。他站在面馆门口,发现自己居然有五种获得一碗面的方式:直接冲进后厨自己从和面擀面开始全流程包办;喊一声“来碗面”让后厨师傅代劳;走进连锁品牌点一份成套的牛肉面套餐;站在窗口逐项定制“手工面、番茄汤、加蛋加青菜、微辣”;或者——看见邻桌那碗面正合心意,跟师傅说一句“照这碗再来一份”。 这五种姿势,恰好对应软件世界里创建对象的五种经典设计模式:单例、工厂方法、抽象工厂、建造者、原型。它们都在回答同一个问题:如何把“创建对象”这件差事,从调用者手里优雅地交出去。本文就用这碗面做引子,把五种模式的思路、取舍与实战来一次彻底煮透。

岗位关联

掌握创建型模式,是 AI 工程师写出可维护、可测试、可扩展代码的起点:从模型工厂到实验配置管理,处处离不开这五种解耦思路。

目录
  1. 可视化演示
  2. 模式概览:一碗面的五种获得方式
  3. 单例模式:全城唯一的老字号
  4. 工厂方法:喊一声“来碗面”
  5. 抽象工厂:牛肉面套餐的成套哲学
  6. 建造者模式:逐项定制一碗面
  7. 原型模式:照这碗再来一份
  8. 选型决策:门口的五张菜单
  9. 实战应用:面馆开进 AI 实验室
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态对比五种创建型模式(单例、工厂、抽象工厂、建造者、原型)的复杂度、灵活性与控制权,通过创建流程动画展示各模式的对象实例化路径与解耦方式。

本节配有交互动效(进入视口后自动加载,约 3KB)。

创建型模式对比 单例模式 工厂模式 抽象工厂 OCP=开放扩展 五种模式各擅其场实现创建解耦

模式概览:一碗面的五种获得方式

深夜的小林走进面馆,发现吃面这件事远不止一种打开方式。他可以让后厨师傅代劳、可以点成套的套餐、可以在窗口逐项定制、也可以直接照邻桌的样板复刻,当然,最硬核的是自己冲进后厨从头煮。五种方式对应着五种对“这碗面由谁创造、怎么创造”的不同回答。

软件世界里,这碗面就是“对象”,而对象的诞生方式正是创建型模式的领地。传统代码里一句 new 看似省事,实则把客户端与具体类绑在了一起——未来换实现类,所有 new 过它的地方都要跟着改,这种“牵一发动全身”的毛病就是耦合。创建型模式的核心目标,就是用结构化的方式降低耦合,让“创建”与“使用”各司其职。

创建型模式一共五员大将:单例管唯一实例;工厂方法把创建延迟到子类;抽象工厂按产品族成套生产;建造者把复杂构建拆成步骤;原型靠克隆复制对象。它们都是“开闭原则”的化身——对扩展开放,对修改关闭。用一个简单的指标可以感受解耦的收益:

小林把五种姿势想了一遍:自己下厨最累但掌控感最强,喊师傅代劳最省心但口味不由自己定,套餐最配套但灵活性最低。解耦度就像“你亲自下厨的比例”——当越来越多的面交给专业角色去完成,省下的力气就是未来换口味时的改造成本。创建型模式练的,正是这门“得体地外包做饭”的功夫。

Dc=1−NnewNtotalD_c = 1 - \frac{N_{\text{new}}}{N_{\text{total}}}
解耦度 = 1 减去“直接 new 占比”。这个值越接近 1,说明创建逻辑越少暴露在客户端。
  1. N_total 总创建次数系统中所有对象创建的总量,作为分母代表全部的创建需求。
    NtotalN_{\text{total}}
  2. N_new 直接 new 次数其中由客户端直接 new 出来的次数,这部分把使用者与具体类死死绑在一起。
    NnewN_{\text{new}}
  3. 比值 N_new/N_total直接创建占全部创建的比例,比值越大,客户端知道的细节越多、耦合越重。
    NnewNtotal\frac{N_{\text{new}}}{N_{\text{total}}}
  4. 1 减去把“不直接创建”的比例转化成解耦收益。1 代表完全不直接 new,0 代表全部直接 new。
    Dc=1−NnewNtotalD_c = 1 - \frac{N_{\text{new}}}{N_{\text{total}}}

单例模式:全城唯一的老字号

老城区有一家做了六十年的牛肉面馆,只此一家,别无分号。无论你哪天去,门口都是同一块褪色的招牌,锅里滚着同一锅老汤。食客再多,也从没见过第二家分店——有些东西,全城只能有一份。

程序世界里,“老汤”就是那些全局共享的资源。AI 实验里的配置(学习率、批量大小、数据路径)就是典型:如果每个模块各存一份,改一次配置就要全网同步,改漏一处立刻翻车。单例模式(Singleton)的使命就是保证“一个类在整个进程里只有一个实例,并提供全局唯一的访问入口”。

实现单例有三个要点:构造函数私有化,杜绝外界 new;用静态字段保存唯一实例;提供静态方法 getInstance() 作为唯一入口。按创建时机不同,又分饿汉式(类加载时立刻创建,简单但可能浪费资源)和懒汉式(第一次调用才创建,省资源但多线程下有风险)。多线程环境的经典解法是双重检查锁:先判空,再加锁,锁内再判空,既保证线程安全又避免每次调用都加锁的损耗。懒汉式单例的返回逻辑可以写成:

全城唯一的老字号,优点是极致的一致——东城食客和西城食客喝到的是同一锅汤;缺点也同样明显——想换个口味,全城都得跟着改。单例给了你“一锅汤”的确定性,却也悄悄藏起了全局状态,测试时想换一份 mock 汤底就得绕路。工程上,为这碗独一份的汤买单之前,先问一句:它真的全城只需要一份吗?

getInstance():  instance=(instance=null)  ?  new Singleton():instance\text{getInstance}(): \; instance = (instance = \text{null}) \; ? \; \text{new Singleton}() : instance
单例的入口方法:为空就创建,不为空就直接复用,任何时刻全城只有一碗汤。
  1. instance 实例字段静态成员变量,整个类只有这一份,用来保存唯一实例。
    instanceinstance
  2. instance == null 判空第一次调用时为空,说明还没创建过;后续调用不为空,直接走复用分支。
    instance=nullinstance = \text{null}
  3. new Singleton() 创建只有首次调用才执行构造函数,之后永远不会再创建新对象。
    new Singleton()\text{new Singleton}()
  4. 三元表达式把“创建 / 复用”两个分支压成一行,保证任何时刻最多只有一个实例活在全城。
    (instance=null)  ?  new Singleton():instance(instance = \text{null}) \; ? \; \text{new Singleton}() : instance

工厂方法:喊一声“来碗面”

小林懒得自己下厨,走进一家挂着“面馆”招牌的店,对服务员说:来碗面。他不关心后厨师傅是谁、用的是什么面粉,端上来的到底是兰州拉面、重庆小面还是刀削面,取决于他走进的是哪家分店。每家门面都挂同一块“面馆”的招牌,后厨却各显神通。

工厂方法模式(Factory Method)正是如此:定义一个创建对象的工厂接口,把“到底创建哪个具体产品”的决策延迟到子类。客户端只面对抽象产品与抽象工厂,不接触任何具体类——这就把“依赖具体实现”转成了“依赖抽象接口”,是依赖倒置原则的日常实践。

结构上包含四个角色:抽象产品(面条接口)、具体产品(拉面、小面)、抽象工厂(声明 create 方法)、具体工厂(实现 create 返回具体产品)。当新增一种面时,只需新增具体产品和对应工厂,客户端代码零改动。这个“产品类型由工厂类型决定”的关系可以写成:

点餐时你只管“要碗面”,至于后厨流派是子类的事。这省掉的正是“知道太多”的负担——当你想新增一家越南河粉分店时,老顾客菜单上那句“来碗面”一个字都不用改,这正是“对修改关闭、对扩展开放”的回报。

Product=ConcreteFactory.create(),Type(Product)=f(Type(Factory))\text{Product} = \text{ConcreteFactory}.create(), \quad \text{Type(Product)} = f(\text{Type(Factory)})
产品由工厂方法创建,且产品的类型是工厂类型的函数——走进哪家分店,就得到哪家的招牌面。
  1. ConcreteFactory 具体工厂继承抽象工厂的子类,是真正干活的“后厨流派”。
    ConcreteFactory\text{ConcreteFactory}
  2. create() 工厂方法创建产品的统一入口,返回类型写成抽象产品,客户端不知道具体是谁。
    ConcreteFactory.create()\text{ConcreteFactory}.create()
  3. Type(Product) = f(Type(Factory))产品类型由工厂类型唯一映射,工厂换人,产品跟着换。
    Type(Product)=f(Type(Factory))\text{Type(Product)} = f(\text{Type(Factory)})

抽象工厂:牛肉面套餐的成套哲学

连锁品牌不会只卖单品。你点一个“牛肉面套餐”,端上来的是一整套:一碗红烧牛肉面、一碟小菜、一杯酸梅汤、一副印着品牌 logo 的木质餐具。所有东西风格统一、搭配和谐。如果单点,你可能会得到一碗与餐具完全不搭的面——这正是抽象工厂要消灭的混乱。

抽象工厂模式(Abstract Factory)解决的是“产品族”的一致性问题:当你要创建一组彼此关联、必须配套使用的对象(比如一套 UI 控件、一组数据访问组件)时,用同一个抽象工厂创建,就能保证这组对象的“风格”始终一致。与工厂方法每次只创建一个产品不同,抽象工厂一次创建一族产品。

代价是接口膨胀:每增加一种产品,所有工厂实现都要跟着改。所以它适合“产品族结构稳定、需要成套替换”的场景,比如换数据库时把 MySQL 系列组件整体换成 PostgreSQL 系列,像换一整套餐具而不是只换一个盘子。产品族的严格隔离可以这样表达:

就像你家的餐厅可以整桌换装:今天是中式八仙桌配青花瓷,明天是西式长桌配银质刀叉,但绝不会出现中式桌子配西式刀叉的混搭。抽象工厂给的就是这种“整桌成套”的确定性,代价是——想往套餐里加一道甜品,每个门店的配置单都得改一轮。

Familyi={Ai,Bi,Ci},∀j≠i:Familyj∩Familyi=∅\text{Family}_i = \{A_i, B_i, C_i\}, \quad \forall j \neq i: \text{Family}_j \cap \text{Family}_i = \varnothing
第 i 个工厂生产一族互相配套的产品,不同族之间的产品交集为空——绝不允许混搭。
  1. Family_i 产品族编号 i 的工厂生产的一整套配套产品。
    Familyi\text{Family}_i
  2. {A_i, B_i, C_i} 成员同一族内的不同产品,比如面、汤、小菜属于同一个套餐。
    {Ai,Bi,Ci}\{A_i, B_i, C_i\}
  3. 交集为空任意两个不同工厂的产品集合互不相交,确保不会出现“中式桌子配西式刀叉”。
    ∀j≠i:Familyj∩Familyi=∅\forall j \neq i: \text{Family}_j \cap \text{Family}_i = \varnothing

建造者模式:逐项定制一碗面

最挑剔的食客不会点套餐,而是走到窗口,对着师傅一步一步交代:面条要手工的,汤底要番茄的,加一个卤蛋,再加一把青菜,辣度微辣。师傅按着步骤逐一落实,最后端出一碗完全符合心意的面——这就是建造者的“分步构建”。

建造者模式(Builder)适用于“构造过程复杂且步骤可组合”的对象。它与工厂的区别在于:工厂关心“创建什么类型”,建造者关心“一个对象如何被一步步拼装”。当构造函数参数超过四五个时,直接 new 会面临参数顺序混乱、可读性差的困境。建造者的现代实现常用链式调用:每个 setter 返回自身,方法链一气呵成,把“参数多”变成了“步骤清晰”。

建造者模式包含指挥者(编排构建顺序)、抽象建造者(声明各步骤方法)、具体建造者(实现步骤)和产品四部分。步骤的编排可以抽象成函数复合:

定制窗口的妙处在于:师傅的记忆(指挥者)和你的需求(建造者)是分开的——换一个师傅,同样的步骤也可能做出不同风味;同一个师傅,按不同顺序也能玩出花样。这就是建造者的价值:把“怎么一步步做”和“最终做成什么”彻底拆开,各管各的。

Product=build∘stepn∘⋯∘step1∘init\text{Product} = \text{build} \circ step_n \circ \dots \circ step_1 \circ \text{init}
从空状态 init 出发,依次应用 step_1 到 step_n,最后 build 定型——每一步只添加一个部件。
  1. init 初始状态建造者从一个“空半成品”开始,像师傅面前的一张空白订单。
    init\text{init}
  2. step_1 到 step_n 逐层构建每个步骤只给对象添加一个部件或属性,步骤之间互相独立。
    step1∘step2∘⋯∘stepnstep_1 \circ step_2 \circ \dots \circ step_n
  3. build 组装交付最后一步把累积的半成品定型为完整产品,交付给客户端。
    build\text{build}
  4. 复合运算步骤按顺序复合,顺序可调整、部件可替换,构建过程与最终产品彻底解耦。
    build∘stepn∘⋯∘step1∘init\text{build} \circ step_n \circ \dots \circ step_1 \circ \text{init}

原型模式:照这碗再来一份

邻桌那碗面端上来,面条筋道、汤色红亮,小林看得直咽口水。他不想等厨房从零重做,也不想费劲跟师傅描述,只消说一句“照这碗再来一份”。师傅瞄一眼样板,快速复制出一碗一模一样的——耗时之短,比自己下厨还快。

原型模式(Prototype)正是通过克隆已有对象来创建新对象,而不是重新走一遍构造函数。它有两类关键场景:一是创建对象成本高昂(比如要读数据库、跑复杂校验),二是新对象与原型只有少量差异。Python 里的 copy.copy 和 copy.deepcopy 就是现成的克隆工具。

克隆的隐患在于拷贝方式。浅拷贝只复制引用,新对象和原型共享内部对象——改一个会影响另一个;深拷贝递归复制整个对象图,各自独立但开销大。若对象图里含有可变容器,选择哪种拷贝直接影响正确性。拷贝成本可以这样建模:

“照这碗再来一份”看似取巧,实则需要谨慎:如果面条里藏着一碟众人共享的辣酱(可变引用),复制时是共用一个瓶子还是各分一瓶,直接决定两位食客会不会吃串味。搞懂深浅拷贝的边界,原型模式才能真正用得顺手、复制得放心。

Tclone={O(1),浅拷贝且字段不可变O(N),深拷贝遍历节点总数为N 的对象图T_{\text{clone}} = \begin{cases} O(1), & \text{浅拷贝且字段不可变} \\ O(N), & \text{深拷贝遍历节点总数为} N \text{ 的对象图} \end{cases}
浅拷贝几乎瞬间完成,深拷贝耗时与对象图中可达节点总数成正比。
  1. O(1) 浅拷贝只复制一层引用,不深入内部对象,耗时恒定。
    O(1)O(1)
  2. O(N) 深拷贝需要递归复制每个节点,N 是对象图中可到达的节点总数。
    O(N)O(N)
  3. 对象图从原型根对象出发,沿引用能到达的全部对象构成一张图。
    对象图节点数=N\text{对象图节点数} = N
  4. 不可变字段约束字段都是不可变类型时浅拷贝也安全;一旦出现可变容器,就必须用深拷贝。
    含可变字段⇒必须深拷贝\text{含可变字段} \Rightarrow \text{必须深拷贝}

选型决策:门口的五张菜单

站在面馆门口,小林该选哪一张菜单?自己下厨、单点、套餐、定制、复刻,没有绝对的好坏,只有合不合适的取舍。创建型模式的选型也一样,本质是在三个维度之间找平衡:控制权(创建逻辑握在谁手里)、扩展性(能不能不修改旧代码就支持新产品)、复杂度(为此多引入多少类与接口)。

用一张决策表辅助判断:对象简单、创建逻辑固定时直接 new 最省事;需要全局唯一时上单例;有一组同类产品且类型可能扩展时用工厂方法;需要成套产品保持一致时用抽象工厂;对象构造步骤多、参数杂时用建造者;创建成本高、差异小时用原型。

五者之间各有短板:工厂方法最均衡;抽象工厂扩展力强但接口重;单例实现简单但隐藏全局状态;原型性能好但拷贝语义有坑;建造者灵活但类会膨胀。用加权评分可以量化权衡:

面馆门口的选择题没有标准答案:一个人吃饭图快就自己下厨,请客要体面就点套餐,想独一无二就逐项定制。关键是别让“用设计模式”本身成为一种炫技——一把钥匙开一把锁,给锁配钥匙,而不是背着满口袋钥匙满街找锁。

Score=α×灵活性+β×一致性−γ×复杂度,α+β+γ=1\text{Score} = \alpha \times \text{灵活性} + \beta \times \text{一致性} - \gamma \times \text{复杂度}, \quad \alpha + \beta + \gamma = 1
选型分数 = 灵活性加权 + 一致性加权 − 复杂度加权。三个权重加起来为 1,按项目现实动态分配。
  1. α × 灵活性需求频繁变化时调高 α,工厂类模式的得分会明显上升。
    α×灵活性\alpha \times \text{灵活性}
  2. β × 一致性强调产品配套、风格统一时调高 β,抽象工厂的优势凸显。
    β×一致性\beta \times \text{一致性}
  3. −γ × 复杂度团队小、工期紧时调高 γ,简单方案反而胜出,复杂度要被“扣分”。
    −γ×复杂度-\gamma \times \text{复杂度}
  4. 权重归一化三个权重之和恒为 1,代表在有限的资源里做取舍,而不是全都要。
    α+β+γ=1\alpha + \beta + \gamma = 1

实战应用:面馆开进 AI 实验室

面馆的哲学正在 AI 工程里生根发芽。训练一个深度学习模型,要“煮”的东西可不少:实验配置、数据集、模型、训练流水线,每一样都是对象,每一样都可能面对五种获得方式的选择——而选对了,能让整个训练流程清爽得多。

配置管理是单例的天然主场:学习率、批量大小、数据路径、随机种子,全进程只有一份 Config,所有模块统一读取,改一处全同步,实验复现也只需要记录这一份配置。模型工厂则是工厂方法最直观的落地:根据名称字符串创建 CNN、RNN、Transformer,调用方不必关心类名与 import 路径,为框架切换留出余地。

数据处理流水线适合建造者:读取 → 清洗 → 特征工程 → 归一化,按步骤链式组装,不同实验可复用不同步骤组合。超参数模板适合原型:从一份基准模板深拷贝出多份实验变体,只改少数参数就提交新一轮训练,避免从零构建的开销。三者的协作可以写成:

从面馆到实验室,底层逻辑相通:把“做面”的复杂性封装在专业角色里,让调用的代码像点餐一样简洁。AI 工程师每天面对成千上万次对象创建,用创建型模式把它们管得井井有条,才能在千行代码中气定神闲地调控那一碗“模型浓汤”。

C=Singleton.getInstance(),M=Factory.create(t),P=Build(S1,S2,…,Sn)C = \text{Singleton}.getInstance(), \quad M = \text{Factory}.create(t), \quad P = \text{Build}(S_1, S_2, \dots, S_n)
配置靠单例拿、模型靠工厂建、流水线靠建造者组——三种模式在一套系统里各司其职。
  1. C = Singleton.getInstance()配置对象来自单例入口,全程只有一份,所有模块读到同一份参数。
    C=Singleton.getInstance()C = \text{Singleton}.getInstance()
  2. M = Factory.create(t)模型由工厂按类型名 t 创建,新增模型不改调用方代码。
    M=Factory.create(t)M = \text{Factory}.create(t)
  3. P = Build(S_1, ……, S_n)流水线由建造者把 n 个步骤依次组装,步骤可插拔、可复用。
    P=Build(S1,S2,…,Sn)P = \text{Build}(S_1, S_2, \dots, S_n)

AI 应用场景

场景一:AI 实验配置中心

阶段:实验管理 / 配置管理

解决的问题:传统方式下每个模块各自加载一份配置文件,学习率、批量大小散落各处,调一次参就要全局搜索替换,漏改一处整个实验结果失真。单例把配置收敛到一处,任何模块拿到的都是同一份全局配置——就像全城唯一的老字号面馆,无论谁去,都是同一锅汤。

具体案例:一个 CV 训练框架里,数据加载器、模型初始化、优化器都要读取 batch_size 和 learning_rate。用单例 Config 后,训练入口只需 Config.set(...),其余模块 getInstance() 自动读到最新值,实验复现时把这份配置序列化存档即可。

场景二:模型工厂按名创建

阶段:模型构建 / 模型推理服务

解决的问题:直接 new 各种模型类会让调用方耦合具体类名和 import 路径,换框架或加新模型要改多处调用点。模型工厂把“根据名称创建模型”统一收口,客户端只需喊一声“来碗面”——至于端上来的是拉面还是小面,由后厨(工厂)决定。

具体案例:推理服务端接收请求中的 model_type 参数("cnn"、"rnn"、"transformer"),通过 ModelFactory.create() 返回对应的模型处理器。新增模型时只需在工厂里加一个分支,服务主流程零改动。

场景三:数据流水线的建造者

阶段:数据工程 / 特征工程

解决的问题:数据处理步骤多、步骤顺序可变、不同实验组合不同,硬编码成一个大函数难以复用和调整。建造者把步骤拆开,用链式调用自由组合——就像站在窗口逐项定制一碗面,选什么、加什么、按什么顺序,都由你说了算。

具体案例:推荐系统离线流水线:读取用户日志 → 清洗异常 → 特征交叉 → 归一化。通过 PipelineBuilder 定义四个步骤,线上与离线实验分别 build() 出不同的流水线对象,步骤完全复用。

Python 演示

python97 行
# -*- coding: utf-8 -*-"""演示创建型模式: 单例-工厂方法-抽象工厂-建造者-原型"""import copyimport threading  class Config:    """单例模式: 全局唯一的配置中心, 双重检查锁保证线程安全"""    _instance = None    _lock = threading.Lock()     def __new__(cls):        # 第一重判断: 已存在则直接返回, 避免每次加锁        if cls._instance is None:            with cls._lock:                # 第二重判断: 锁内再次确认, 防止并发重复创建                if cls._instance is None:                    ins = super().__new__(cls)                    ins.data = {'lr': 0.001, 'batch': 32}                    cls._instance = ins        return cls._instance     def get(self, key):        return self.data.get(key)     def set(self, key, value):        self.data[key] = value  class ModelFactory:    """工厂方法模式: 按名称创建模型, 客户端不接触具体类"""    @staticmethod    def create(kind):        if kind == 'lr':            return 'LogisticRegressor'        if kind == 'cnn':            return 'ConvNet'        return 'RecurrentNet'  def make_combo(platform):    """抽象工厂模式: 按硬件平台成套返回 模型与加载器 产品族"""    if platform == 'gpu':        return ('Fp16Model', 'CudaLoader')    return ('Fp32Model', 'CpuLoader')  class PipelineBuilder:    """建造者模式: 分步构建数据流水线, 支持链式组合"""    def __init__(self):        self.parts = []     def add_read(self, path):        self.parts.append('read_' + path)        return self     def add_clean(self):        self.parts.append('clean')        return self     def add_norm(self):        self.parts.append('norm')        return self     def build(self):        return ' -> '.join(self.parts)  class HpTemplate:    """原型模式: 基准超参与模板, 深拷贝出独立实验变体"""    def __init__(self, lr=0.001, batch=32):        self.lr = lr        self.batch = batch     def clone(self, **kw):        obj = copy.deepcopy(self)        for k, v in kw.items():            setattr(obj, k, v)        return obj  def main():    c1 = Config()    c2 = Config()    c1.set('lr', 0.0001)    print('singleton:', c1 is c2, c2.get('lr'))    print('factory:', ModelFactory.create('cnn'))    print('abstract factory:', make_combo('gpu'))    p = (PipelineBuilder().add_read('train.csv').add_clean().add_norm().build())    print('builder:', p)    base = HpTemplate()    exp = base.clone(batch=64)    print('prototype:', exp.lr, exp.batch)  if __name__ == '__main__':    main()
目的:用 5 个极简类把五种创建型模式浓缩进一段可运行的 AI 实验基础设施演示。
  1. if cls._instance is None:第一重判空,已创建就直接返回,省去每次加锁的开销。
  2. with cls._lock:进入加锁临界区,保证并发下只有一个线程能执行创建。
  3. ins = super().__new__(cls)真正分配实例内存的动作只发生一次,之后全部复用。
  4. return cls._instance无论哪个线程调用,拿到的都是同一个对象。
  5. def create(kind):工厂方法的统一入口,客户端只面向字符串和抽象接口。
  6. if platform == 'gpu':抽象工厂按平台返回成套的“模型+加载器”产品族。
  7. .add_read('train.csv').add_clean().add_norm()链式调用一步一步组装流水线,步骤可插拔。
  8. obj = copy.deepcopy(self)深拷贝整个对象图,保证实验变体之间互不干扰。
  9. for k, v in kw.items():在克隆体上逐项覆盖差异参数,实现“只改少数就开新实验”。

常见误区

误区为了显得专业,我把所有对象创建都改成了工厂。直接 new 太 low 了,设计模式越多代码越高级。

正解设计模式是“在特定场景下解决特定问题的成熟方案”,不是装饰品。一个简单的配置类,直接 new 十行代码搞定,引入工厂反而多出两三个类,把复杂度抬了上去。就像一个人吃饭非要给自己张罗一桌满汉全席——不是不行,是没必要。选型第一原则是“杀鸡用鸡刀”。

为何常见:初学者常把“使用了设计模式”当作工程质量标准,忽略了模式本身也是需要维护的成本。

误区单例不就是把全局变量包装一下吗?反正都是处处可以访问,何必分那么清楚。

正解全局变量是“谁都能改的数据”,单例是“类自己管住唯一实例”的封装。单例把创建权限收进类内部(私有构造函数 + 统一入口),还控制了初始化时机和线程安全,这些全局变量都做不到。就像老字号面馆——食客再多也只能从窗口点餐,没人能随便闯进后厨改老汤配方。

为何常见:两者的访问形式相似,都提供了“从任何地方获取同一个东西”的效果,但封装与治理能力天差地别。

误区抽象工厂比工厂方法高级,名字里带“抽象”嘛,能用抽象工厂的地方就用它。

正解两者解决的问题不同。工厂方法解决“一个产品类型的创建”,抽象工厂解决“一族产品的成套创建”。用抽象工厂强行造单一产品,等于为了吃一碗面强迫自己点下一整套套餐——接口膨胀、改造成本高。它们是两种工具,不是两个版本。

为何常见:名字里都带“工厂”,字面层级看起来一个比另一个复杂,误以为后者是前者的增强版。

误区clone 出来的对象和原对象肯定完全独立,随便改哪个都不会互相影响。

正解默认的浅拷贝只复制引用,嵌套的可变对象仍被两个实例共享。比如 Python 里用 copy.copy 复制含列表的对象,改列表两个对象都变;必须用 copy.deepcopy 才完全独立。就像“照这碗再来一份”——如果师傅只是把同一碟辣酱端过来,两位食客共用一瓶,迟早吃串味。

为何常见:直觉上“复制 = 完全独立的副本”,忽略了引用共享这个底层机制。

误区建造者不就是把 setter 串起来变成链式调用吗?核心就是那个 `.xxx().xxx()` 的写法。

正解链式调用只是建造者模式的现代语法糖,核心是“把复杂对象的构建过程拆成独立步骤,允许用不同步骤组合出不同产品”。没有指挥者编排、没有分步构建的链式调用,只是普通方法串烧。形式可以换,本质不能丢——定制窗口真正厉害的不是“连说三句话”,而是每句话只负责一件事。

为何常见:很多教程只展示链式写法的代码,让人把“外在形式”当成了“内在本质”。

面试题

Q1. 创建型模式要解决的核心问题是什么?

单选题 · easy

  • A.加速对象创建运行速度
  • B.解耦对象创建与使用← 正确答案
  • C.减少类的定义数量
  • D.提高界面的展示效果

答案:B。创建型模式的核心是“解耦”,把 new 具体类的逻辑从客户端里抽出去交给专业角色。就像小林把煮面交给后厨,自己只管点单。A、C、D 都不是创建型模式的职责范围——它不管运行速度、类数量、界面效果,只管创建过程如何更灵活、更低耦合。

Q2. 单例模式保证的核心性质是什么?

单选题 · easy

  • A.实例全局唯一且共享← 正确答案
  • B.实例支持并发创建
  • C.实例数量由参数决定
  • D.实例必须按周销毁

答案:A。单例模式最核心的一条就是“全局唯一、统一访问”——整个进程只有一个实例,所有调用方共享它,就像全城唯一的老字号面馆。B 与单例相悖,并发创建会破坏唯一性;C 是工厂/原型才有的灵活性;D 是对象生命周期管理,与唯一性承诺无关。

Q3. 客户端直接 new 对象的主要问题是什么?

单选题 · easy

  • A.客户端与具体类型耦合← 正确答案
  • B.代码执行速度会更快
  • C.对象必定无法被回收
  • D.内存占用能够更小

答案:A。直接 new 让客户端必须知道具体类型名与构造细节,一旦实现类变化,所有 new 过它的地方都得跟着改——这就是耦合。B、C、D 都是性能或生命周期层面的无关联想。就像你每次吃面都自己进后厨,一旦面粉换了牌子,你所有的“煮面流程”都得重学。

Q4. 工厂方法模式中,决定创建哪个具体产品的是?

单选题 · mid

  • A.客户端的条件分支语句
  • B.具体工厂的子类实现← 正确答案
  • C.产品类自身的构造器
  • D.抽象工厂的接口定义

答案:B。工厂方法把“创建哪个产品”的决策权延迟到具体工厂子类——走进哪家分店,就得到哪家的招牌面。客户端只面对抽象工厂和抽象产品,条件分支从客户端移到了具体工厂内部。A 把决策留在客户端,正是我们要消除的;C 是普通构造;D 说的是抽象工厂模式。

Q5. 抽象工厂模式与工厂方法模式的区别包括?

多选题 · mid

  • A.抽象工厂围绕产品族生产← 正确答案
  • B.工厂方法每次创建一个产品← 正确答案
  • C.抽象工厂可以替代任何封装
  • D.工厂方法要求使用原型模式

答案:AB。抽象工厂按“产品族”成套生产,保证族内风格一致;工厂方法按“产品等级”单一创建,一次只产出一个产品。C 夸大其词,每种封装各有适用场景;D 更是子虚乌有,工厂方法与原型模式毫无绑定关系。

Q6. 建造者模式最适合解决什么问题?

单选题 · mid

  • A.对象构造步骤多且组合复杂← 正确答案
  • B.需要保证全局只有一个对象
  • C.希望快速复制现有对象
  • D.类型相同但实例互不相关

答案:A。建造者的目标是“分步组装复杂对象”。参数多、步骤可组合、不同组合得出不同产品时,它能把构造过程拆解得清晰可读,就像窗口定制一碗面逐项交代。B 是单例,C 是原型,D 描述的是普通多实例场景,均与“步骤组装”无关。

Q7. 下列哪项最适合原型模式的用武之地?

单选题 · mid

  • A.创建对象成本高且差异小← 正确答案
  • B.需要严格保证全局唯一
  • C.需要分步骤搭建对象
  • D.需要成套生产配套对象

答案:A。原型模式的价值在“高成本创建 + 小差异复制”:与其重走昂贵的构造流程,不如克隆一个样板再微调——就像“照这碗再来一份”。B 是单例,C 是建造者,D 是抽象工厂,四个选项正好对应四种不同模式的画像。

Q8. 关于单例模式的实现,下列说法正确的有?

多选题 · hard

  • A.构造函数通常声明为私有← 正确答案
  • B.双重检查锁兼顾安全与性能← 正确答案
  • C.饿汉式在类加载时即创建← 正确答案
  • D.懒汉式不会存在线程隐患

答案:ABC。单例的构造函数必须私有,杜绝外部 new;双重检查锁先判空再加锁、锁内二次判空,既安全又避免每次调用都加锁;饿汉式利用类加载时机提前创建,简单直接。D 错——懒汉式在多线程下若不同步,可能同时创建多个实例,老字号就可能开出两家分店。

Q9. 深拷贝与浅拷贝的核心区别在于?

单选题 · hard

  • A.是否复制嵌套可变对象← 正确答案
  • B.克隆速度是否完全相同
  • C.是否必须使用关键字 new
  • D.是否保证原型不可修改

答案:A。浅拷贝只复制一层引用,嵌套的可变对象仍被新旧实例共享;深拷贝递归复制整个对象图,使副本完全独立——这决定了改辣酱会不会串味。B 把“拷贝深度”偷换成“速度”;C 与拷贝机制无关;D 描述的不是拷贝的职责,而是设计层面的可变性控制。

Q10. 关于创建型模式选型,下列说法正确的有?

多选题 · mid

  • A.简单对象可优先直接创建← 正确答案
  • B.所有场景都得套用模式
  • C.模式会引入类的膨胀成本← 正确答案
  • D.工厂适合解耦创建依赖← 正确答案

答案:ACD。选型的第一原则是“按需使用”:简单对象直接 new 足够,自己煮碗面最快;引入模式必然增加类与接口,这是不可忽视的成本;工厂把创建依赖收口,是解耦利器。B 属于模式迷信,违背设计模式“解决特定问题”的初衷。