LOADING

加载过慢请开启缓存 浏览器默认开启

面向对象设计原则系列(7):组合复用原则(CRP)


一、CRP 定义与核心思想

“Prefer composition over inheritance.”

优先使用组合,而非继承。

——Gang of Four(《设计模式》)

  • 组合(Composition):在对象内部持有其他对象的引用,通过调用它们来扩展功能;
  • 继承(Inheritance):子类自动获得父类的所有行为和接口。

CRP 的核心:把易变行为封装成独立组件,按需组合到宿主对象,而不是通过继承层层扩展


二、为什么 CRP 重要

  1. 降低耦合
    继承会让子类与父类绑定在一起:父类的任何改动都可能影响所有子类;
  2. 提高内聚
    组合让每个组件只关注自身职责,易于理解和测试;
  3. 支持运行时动态扩展
    通过组合,可在运行时替换或增加新组件,无需重新编译继承体系;
  4. 避免多重继承陷阱
    C# 不支持多重继承,组合能模拟多重行为的混入(mixin)效果。

三、继承 vs 组合:对比分析

特性 继承 组合
耦合度 高度耦合:父类改动影响子类 低耦合:组件改动只影响自身
重用方式 通过子类继承父类所有行为 通过引用组件,仅调用所需方法
运行时灵活性 静态:继承关系在编译期固定 动态:可在运行时增删组件
接口契约 子类自动继承所有父接口 宿主类只实现自己需要的接口
多重继承支持 C# 不支持多继承 组合可同时持有多个组件

四、Unity 中的 CRP 反例

// 反例:继承地狱——多个层级的 Enemy 类型
public class Enemy : MonoBehaviour {
    public virtual void Attack() { … }
    public virtual void Die() { … }
}

public class RangedEnemy : Enemy {
    public float range;
    public override void Attack() { /* 远程射击 */ }
}

public class FlyingRangedEnemy : RangedEnemy {
    public float flyHeight;
    public override void Attack() {
        // 飞行 + 远程射击
    }
}

public class Boss : FlyingRangedEnemy {
    public void SpecialAttack() { … }
}

问题

  • 修改 Attack() 的签名或行为,所有子类都需同步修改;
  • 想给 Boss 添加一个“护盾”功能,只能在继承链上再插一层或修改 Boss 类;
  • 多维度行为(飞行、射击、护盾)无法灵活组合。

五、CRP 重构策略

5.1 将行为抽象为组件

  • 攻击组件IAttackBehavior
  • 移动组件IMoveBehavior
  • 特效组件IShieldIFly
public interface IAttackBehavior {
    void Attack(GameObject user);
}

public class MeleeAttack : IAttackBehavior {
    public void Attack(GameObject user) {
        // 近战逻辑
    }
}

public class RangedAttack : IAttackBehavior {
    public void Attack(GameObject user) {
        // 远程射击逻辑
    }
}

public interface IMoveBehavior {
    void Move(GameObject user, Vector3 direction);
}

public class GroundMove : IMoveBehavior {
    public void Move(GameObject user, Vector3 dir) { /* 地面移动 */ }
}

public class FlyMove : IMoveBehavior {
    public void Move(GameObject user, Vector3 dir) { /* 飞行移动 */ }
}

5.2 在 Enemy 中组合行为

public class Enemy : MonoBehaviour {
    [SerializeField] IAttackBehavior attackBehavior;
    [SerializeField] IMoveBehavior   moveBehavior;
    [SerializeField] IShield         shieldBehavior; // 可选

    void Awake() {
        // 也可通过工厂或 DI 容器注入
    }

    void Update() {
        Vector3 dir = /* ... */;
        moveBehavior.Move(gameObject, dir);
        if (ShouldAttack()) attackBehavior.Attack(gameObject);
    }

    public void ActivateShield() {
        shieldBehavior?.Enable(gameObject);
    }
}
  • 新增 Boss 护盾:只需提供 ShieldBehavior 实现并赋值给 Enemy 上的字段,无需继承。
  • 动态切换:可在运行时修改 attackBehaviormoveBehavior,实现状态模式、形态变换。

六、高级模式与扩展

6.1 装饰器(Decorator)+组合

public class PoisonDecorator : IAttackBehavior {
    private IAttackBehavior wrappee;
    public PoisonDecorator(IAttackBehavior baseAttack) {
        wrappee = baseAttack;
    }
    public void Attack(GameObject user) {
        wrappee.Attack(user);
        // 附加中毒效果
    }
}

// 运行时组合
IAttackBehavior atk = new RangedAttack();
if (hasPoison) atk = new PoisonDecorator(atk);
enemy.attackBehavior = atk;

6.2 ECS 思想下的纯组合

在 Unity ECS 中,所有行为都是 Component,系统(System)按组件组合来驱动:

struct AttackData : IComponentData { public float damage; }
struct RangedTag : IComponentData { public float range; }

// RangedAttackSystem 根据 Entities.ForEach 拥有 AttackData + RangedTag 的实体执行射击

七、工具

  • 继承深度度量
    • NDepend 中查看 TypeInheritanceDepth,深度 > 3 警告继承链过长;
  • 耦合度量
    • Afferent/Efferent Coupling 监测组件间依赖;
  • 可视化
    • Visual Studio Class Diagram / Rider Architecture View 展示组合关系;
  • 静态分析
    • SonarQube 建议使用组合替代继承的警告。

八、注意事项

  1. 过度组合:将所有行为拆成组件可能导致字段过多、管理复杂;
  2. 性能开销:组件调用频繁时,尽量在 Awake 缓存引用,减少接口虚调用;
  3. 生命周期管理:组合模式下,宿主需负责组件的启停,注意 null 检查;
  4. 接口粒度:避免“上帝接口”,保持接口职责单一,与 ISP 配合使用。

九、小结

  • 识别变化维度:先梳理系统中哪些行为会扩展、新增或组合;
  • 抽象行为接口:将可变的功能点定义为接口或数据驱动组件;
  • 按需组合:在宿主类中持有接口引用,通过 Inspector、工厂或 DI 容器注入;
  • 动态切换:利用装饰器、状态模式,支持运行时行为变化;
  • 配合 ECS:在 DOTS/ECS 场景中,将所有行为组件化,系统驱动。