判断一个场景是否适合工厂方法,先问:
业务流程基本相同,但其中创建的对象,需要由不同子类决定吗?
如果答案是“是”,工厂方法就值得考虑。
注意:这里说的是通过子类重写创建方法的工厂方法模式,而不只是把Instantiate()包进一个方法。
一、典型应用场景
| 场景 | 不变的业务流程 | 变化的创建步骤 |
|---|---|---|
| 武器开火 | 检查条件 → 创建弹体 → 发射 | 创建普通子弹、火箭、魔法弹 |
| 宝箱奖励 | 检查开启条件 → 创建奖励 → 发放 | 创建金币奖励、道具奖励 |
| 敌人生成 | 创建敌人 → 初始化 → 注册 | 创建近战敌人、远程敌人 |
| UI 窗口 | 创建窗口 → 绑定数据 → 显示 | 创建背包、商店、任务窗口 |
| 技能释放 | 检查消耗 → 创建效果 → 执行 | 创建伤害、治疗、召唤效果 |
这些只是候选场景,不意味着每种武器、每个窗口都应该有一个工厂子类。
下面用宝箱奖励说明具体实现。
二、实战:不同宝箱创建不同奖励
需求:
金币宝箱 → 发放金币 药水宝箱 → 发放药水开启宝箱的流程统一:
检查玩家 ↓ 检查宝箱是否已开启 ↓ 创建奖励 ↓ 发放奖励 ↓ 标记已开启我们把“创建奖励”作为可变化的步骤。
1. 定义背包和奖励接口
这是一个纯 C# 示例,不依赖 Unity 组件生命周期。
publicsealedclassPlayerInventory{publicintGold{get;privateset;}publicintPotions{get;privateset;}publicvoidAddGold(intamount){Gold+=amount;}publicvoidAddPotions(intamount){Potions+=amount;}}publicinterfaceIReward{voidApply(PlayerInventoryinventory);}IReward是抽象产品,表示“可以发放给玩家的奖励”。
2. 实现具体奖励
publicsealedclassGoldReward:IReward{privatereadonlyint_amount;publicGoldReward(intamount){_amount=amount;}publicvoidApply(PlayerInventoryinventory){inventory.AddGold(_amount);}}publicsealedclassPotionReward:IReward{privatereadonlyint_amount;publicPotionReward(intamount){_amount=amount;}publicvoidApply(PlayerInventoryinventory){inventory.AddPotions(_amount);}}不同奖励负责各自的发放行为。
3. 抽象宝箱:保留公共流程
usingSystem;publicabstractclassTreasureChest{publicboolIsOpened{get;privateset;}publicboolTryOpen(PlayerInventoryinventory){if(inventory==null)thrownewArgumentNullException(nameof(inventory));if(IsOpened)returnfalse;IRewardreward=CreateReward();if(reward==null)thrownewInvalidOperationException("奖励不能为空。");reward.Apply(inventory);IsOpened=true;returntrue;}// 工厂方法:由子类决定创建哪种奖励。protectedabstractIRewardCreateReward();}这里的关键是:
protectedabstractIRewardCreateReward();宝箱的公共流程不关心奖励具体是什么,只要求它实现IReward。
4. 具体宝箱决定奖励类型
publicsealedclassGoldChest:TreasureChest{protectedoverrideIRewardCreateReward(){returnnewGoldReward(100);}}publicsealedclassPotionChest:TreasureChest{protectedoverrideIRewardCreateReward(){returnnewPotionReward(3);}}5. 调用方统一处理
varinventory=newPlayerInventory();TreasureChestchest=newGoldChest();chest.TryOpen(inventory);// inventory.Gold == 100chest.TryOpen(inventory);// 同一个宝箱已经开启,不再重复发奖。换成药水宝箱:
TreasureChestchest=newPotionChest();chest.TryOpen(inventory);开启流程不需要修改。
示例只处理单线程、同步、简单发奖。联网游戏中的重复请求、并发开启、部分发奖失败,需要由服务端事务或幂等机制解决,不能只靠一个
bool。
三、这个例子为什么是工厂方法?
角色对应关系:
| 模式角色 | 示例 |
|---|---|
| 抽象产品 | IReward |
| 具体产品 | GoldReward、PotionReward |
| 抽象创建者 | TreasureChest |
| 具体创建者 | GoldChest、PotionChest |
| 工厂方法 | CreateReward() |
调用链:
TreasureChest.TryOpen() │ ▼ 调用 CreateReward() │ ▼ 由实际宝箱子类决定创建哪种奖励 │ ▼ 通过 IReward 统一发放创建者不一定要叫XXXFactory。
这里的宝箱既组织开启流程,又声明工厂方法,是工厂方法模式很典型的用法。
四、迁移到其他游戏系统
1. 武器系统
Weapon.Fire() ├── 检查弹药和冷却 ├── CreateProjectile() ← 工厂方法 ├── 配置伤害和阵营 └── 发射不同子类:
Rifle → 普通子弹 RocketGun → 火箭 MagicStaff → 魔法弹适合不同武器确实需要不同弹体类型或创建步骤的情况。
如果只是伤害和 Prefab 不同,配置驱动通常更简单。
2. UI 系统
WindowController.Open() ├── CreateView() ← 工厂方法 ├── 绑定数据 └── 显示窗口不同控制器创建不同窗口视图。
但同步加载和 Addressables 异步加载不能随意混用:创建接口应明确表达异步行为,而不是假装资源总能立即返回。
3. 敌人生成系统
EnemySpawner.Spawn() ├── CreateEnemy() ← 工厂方法 ├── 初始化等级 └── 注册到战斗系统具体创建方法可以通过:
Instantiate();- 对象池;
- 已加载资源;
获取敌人实例。
如果使用对象池,还必须明确重置、激活和归还规则。工厂不会自动管理这些问题。
五、什么时候不适合?
只有配置不同
例如 100 种宝箱只是奖励数量不同:
小金币宝箱:100 金币 中金币宝箱:500 金币 大金币宝箱:1000 金币不必创建 100 个子类。
用同一种实现,加配置即可。
只需要简单选择
CreateReward(RewardTypetype);少量类型用简单工厂和switch,可能更清晰。
没有稳定的公共流程
如果各个子类的流程完全不同,连公共父类都要不断增加特例,那么继承关系可能不合适。
可以考虑组合、策略或独立的创建服务。
总结
工厂方法最有价值的应用结构是:
父类保留稳定流程 ↓ 把创建对象留成扩展点 ↓ 子类决定具体产品 ↓ 后续流程通过统一接口使用产品选型时记住:
行为或创建步骤不同,考虑工厂方法;只有数值和资源不同,优先考虑数据配置。