一、OCP 定义与核心思想
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
“软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。”
——Bertrand Meyer
- 对扩展开放:在不修改既有代码的情况下,能够增加新的行为或功能;
- 对修改关闭:已有的经过验证和测试的代码一旦发布,就不应再被直接改动,以减少回归风险。
二、为什么 需要OCP
- 减少回归风险
每次修改都可能破坏原有功能。遵循 OCP,新增功能只需新增类或资源,不触碰旧代码。 - 提升可维护性
系统随需求增长而平滑演化,核心模块稳定、边缘功能可自由扩展。 - 支持团队并行开发
不同开发者能在不冲突的情况下各自新增扩展,降低代码合并痛苦。 - 促进模块化设计
强制将变化点抽象为接口或脚本化配置(ScriptableObject),驱动架构更清晰。
三、Unity 中常见的 OCP 违反场景
武器伤害类型:
switch (weapon.Type) { case WeaponType.Sword: damage = baseDamage; break; case WeaponType.Fireball: damage = baseDamage * 1.5f; break; case WeaponType.Ice: damage = baseDamage * 0.8f; break; }
每新增一种武器,都要修改这个 switch。
- 技能效果叠加:
if (hasBurn) ApplyBurn();
if (hasFreeze) ApplyFreeze();
if (hasPoison) ApplyPoison();
新增效果时需要改动组件。
- AI 行为分支:
if (state == AIState.Patrol) Patrol();
else if (state == AIState.Chase) Chase();
else if (state == AIState.Attack) Attack();
每新增状态,核心 Update() 函数必须变动。
四、OCP 重构策略与常用模式
4.1 策略模式(Strategy Pattern)
将可变行为封装成策略类,通过接口调用而非 switch 分支。
public interface IDamageStrategy {
float CalculateDamage(Weapon w, float baseDmg);
}
public class SwordDamage : IDamageStrategy {
public float CalculateDamage(Weapon w, float baseDmg) => baseDmg;
}
public class FireballDamage : IDamageStrategy {
public float CalculateDamage(Weapon w, float baseDmg) => baseDmg * 1.5f;
}
// … 新增其他策略只需新增类
[RequireComponent(typeof(Weapon))]
public class DamageCalculator : MonoBehaviour {
IDamageStrategy strategy;
Weapon weapon;
void Awake() {
weapon = GetComponent<Weapon>();
// 可以通过 DI、Factory 或 ScriptableObject 配置
strategy = StrategyFactory.Get(weapon.Type);
}
public float GetDamage(float baseDmg) {
return strategy.CalculateDamage(weapon, baseDmg);
}
}
4.2 装饰器模式(Decorator Pattern)
对已有行为进行动态“打补丁”,而不修改原始类。
public interface IAttack {
void Execute(GameObject target);
}
public class BasicAttack : IAttack {
public void Execute(GameObject target) {
// 普通伤害逻辑
}
}
public abstract class AttackDecorator : IAttack {
protected IAttack wrappee;
public AttackDecorator(IAttack atk) { wrappee = atk; }
public virtual void Execute(GameObject target) {
wrappee.Execute(target);
}
}
public class BurnDecorator : AttackDecorator {
public BurnDecorator(IAttack atk) : base(atk) {}
public override void Execute(GameObject target) {
base.Execute(target);
// 附加燃烧效果
}
}
// 运行时组合
IAttack atk = new BasicAttack();
if (hasBurn) atk = new BurnDecorator(atk);
if (hasFreeze) atk = new FreezeDecorator(atk);
atk.Execute(enemy);
4.3 工厂模式 & 依赖注入(Factory & DI)
将创建逻辑从使用逻辑中分离,新增类型时只修改 Factory。
public static class StrategyFactory {
public static IDamageStrategy Get(WeaponType type) {
switch (type) {
case WeaponType.Sword: return new SwordDamage();
case WeaponType.Fireball: return new FireballDamage();
// 新增类型时只改这里一次
default: return new SwordDamage();
}
}
}
或使用容器自动注册,消除 switch 完全依赖反射与配置。
4.4 ScriptableObject 方案
利用 Unity 的资源驱动特性,将扩展点放在可编辑资产中。
// 定义 ScriptableObject
public abstract class DamageConfig : ScriptableObject {
public abstract float Calc(float baseDmg);
}
[CreateAssetMenu("Damage/Sword")]
public class SwordConfig : DamageConfig {
public override float Calc(float baseDmg) => baseDmg;
}
[CreateAssetMenu("Damage/Fireball")]
public class FireballConfig : DamageConfig {
public float multiplier = 1.5f;
public override float Calc(float baseDmg) => baseDmg * multiplier;
}
// 使用方式
public class Weapon : MonoBehaviour {
public DamageConfig damageConfig;
public float baseDamage = 10f;
public float Damage => damageConfig.Calc(baseDamage);
}
- 新增类型:只需创建新的
DamageConfig资产,无需触碰代码; - 配置化:可在 Inspector 调整参数,极大提升迭代效率。
五、Unity 具体示例:武器系统重构
反例:在
Weapon内部用switch分支计算伤害public enum WeaponType { Sword, Fireball, Ice } public class Weapon : MonoBehaviour { // 1. 用一个枚举记录武器类型 public WeaponType type; // 2. 基础伤害值 public float baseDamage = 10f; // 3. 根据 type 分支计算不同的伤害 public float GetDamage() { switch (type) { case WeaponType.Sword: // 近战武器,直接返回基础伤害 return baseDamage; case WeaponType.Fireball: // 火球法术,附加 50% 伤害 return baseDamage * 1.5f; case WeaponType.Ice: // 冰霜法术,降低 20% 伤害 return baseDamage * 0.8f; default: // 如果忘记处理某个类型,保证有一个默认值 return baseDamage; } } }策略模式
- 定义
IDamageStrategy与若干实现; - 在
Weapon上通过依赖注入或ScriptableObject引用策略/配置;
- 定义
ScriptableObject 驱动
- 直接将 ScriptableObject 作为“扩展插件”挂载到
Weapon;
- 直接将 ScriptableObject 作为“扩展插件”挂载到
工厂注册
- 利用反射扫描所有
IDamageStrategy实现,自动注册到容器; - 运行时按
type或配置动态解析,无需switch。
- 利用反射扫描所有
六、工具
- 静态分析:Rider/Resharper 可检测大型
switch语句,建议使用策略模式; - 架构可视化:NDepend 提供依赖图谱,帮助识别扩展点与变化点;
- 测试覆盖:为每种策略编写单元测试,无需 Mock 其他分支。
七、注意事项
- 过度抽象:每个变化点都抽象策略,易导致类爆炸;
- 性能开销:动态策略或装饰器过多时,可能产生额外分配,注意缓存实例;
- 配置管理:ScriptableObject 滥用会让项目资产管理混乱,需按命名空间和文件夹组织;
- 接口演进:谨慎修改策略接口签名,避免所有实现都需同步改动。
八、小结
- 识别变化点:先找到项目中频繁改动的代码块或
switch分支; - 抽象行为接口:将变化行为封装为接口或基类;
- 使用策略/装饰器:在运行时组合或切换行为;
- ScriptableObject 配置化:让策划/美术也能增删扩展;
- 自动化注册:结合反射与 DI 容器,彻底消除工厂中的
switch。