如何处理实体复杂度及实体方法高内聚问题?以IUnit接口重构为例
重构臃肿IUnit接口的实践探讨
问题背景
正在重构代码,当前的IUnit接口定义如下:
public interface IUnit : Entity, IUpdatable { byte Team { get; } bool CanHelp(IUnit unit); bool CanHurt(IUnit unit); Position Position { get; } void CastSpell(ISpell spell, IUnit target); void GiveSpell(ISpell spell); void RemoveSpell(ISpell spell); Duration GetCooldown(ISpell spell); void SetCooldown(ISpell spell, Duration cooldown); bool Alive { get; } float CurrentHealth { get; } float MaxHealth { get; } void TakeDamage(HealthChangeEvent @event); void Heal(HealthChangeEvent @event); void ApplyStatus(EffectApplicationData data, Aura aura, long duration); void RemoveStatus(SpellId aura); bool HasStatus(SpellId aura); bool CanPay(AbilityCost cost); bool HasResource(ResourceType type); float GetResourceValue(ResourceType type); void GiveResource(ResourceType type, float value); void SpendResource(AbilityCost cost); Vector3 GetMovement(); }
当前最大的问题是IUnit作为所有ISpell/IAura效果的目标,每次新增功能都要往这个已经臃肿的接口里加新方法。目前想到的方案是将IUnit作为IAuraOwner、IHealthOwner等的容器,采用组件系统,通过IUnit.GetComponent<T>()或直接访问IUnit.Health/IUnit.Auras来实现。
通用解决方案与实践
1. 基于职责拆分的组件化设计
将IUnit的职责拆分为多个单一职责的子接口,每个接口只负责一个领域的功能:
IUnitIdentity:包含团队(Team)、存活状态(Alive)、位置(Position)等核心标识信息IHealthSystem:封装生命值相关操作(CurrentHealth/MaxHealth、TakeDamage、Heal)ISpellSystem:处理法术的施放、管理、冷却(CastSpell、GiveSpell、GetCooldown等)IAuraSystem:负责光环的施加、移除与检测(ApplyStatus、RemoveStatus、HasStatus)IResourceSystem:处理资源的查询与消耗(GetResourceValue、SpendResource、CanPay)IMovementSystem:提供移动相关数据(GetMovement)
然后让IUnit作为这些子系统的容器,通过显式属性暴露:
public interface IUnit : Entity, IUpdatable { IUnitIdentity Identity { get; } IHealthSystem Health { get; } ISpellSystem Spells { get; } IAuraSystem Auras { get; } IResourceSystem Resources { get; } IMovementSystem Movement { get; } bool CanHelp(IUnit unit); bool CanHurt(IUnit unit); }
这种方式的优势是:
- 每个子接口职责单一,符合单一职责原则
- 新增功能只需扩展对应的子系统或新增子接口,不会污染核心
IUnit接口 ISpell/IAura可以根据需求依赖特定的子系统,而非整个IUnit,降低耦合
2. 基于服务定位的组件查询模式
如果需要更灵活的组件扩展(比如未来可能新增大量个性化组件),可以采用类似ECS(实体组件系统)的通用组件查询接口:
public interface IUnit : Entity, IUpdatable { byte Team { get; } bool CanHelp(IUnit unit); bool CanHurt(IUnit unit); Position Position { get; } bool Alive { get; } T GetComponent<T>() where T : class; bool HasComponent<T>() where T : class; }
然后将各个功能模块实现为独立的组件类:
public class HealthComponent { public float CurrentHealth { get; private set; } public float MaxHealth { get; private set; } public void TakeDamage(HealthChangeEvent @event) { /*...*/ } public void Heal(HealthChangeEvent @event) { /*...*/ } } public class SpellComponent { public void CastSpell(ISpell spell, IUnit target) { /*...*/ } // 其他法术相关方法 }
这种模式的好处是:
- 完全解耦功能模块,新增功能只需添加新组件,无需修改
IUnit接口 - 可以根据不同
Unit类型动态挂载不同组件,支持更灵活的实体配置 ISpell/IAura在使用时,先通过HasComponent<T>判断目标是否具备对应功能,再调用组件方法,避免不必要的依赖
3. 针对Spell/Aura的目标抽象
既然IUnit是ISpell/IAura的核心目标,可以为这些效果定义更精准的目标接口,而非直接依赖臃肿的IUnit。例如:
- 伤害类法术只需依赖
IDamageable接口:
public interface IDamageable { void TakeDamage(HealthChangeEvent @event); }
- 治疗类法术依赖
IHealable接口:
public interface IHealable { void Heal(HealthChangeEvent @event); }
- 光环效果依赖
IAuraTarget接口:
public interface IAuraTarget { void ApplyStatus(EffectApplicationData data, Aura aura, long duration); void RemoveStatus(SpellId aura); bool HasStatus(SpellId aura); }
然后让IUnit实现这些精准接口,同时内部将实现委托给对应的组件。这种方式的优势是:
ISpell/IAura只依赖自身需要的最小接口,符合依赖倒置原则- 未来如果有非
Unit类型的目标(比如场景物体),只需实现对应接口即可被法术/光环影响,扩展性更强
4. 剥离跨职责判断逻辑
对于CanHelp/CanHurt这类跨领域的判断逻辑,建议将其移到专门的规则服务中,而非放在IUnit接口里:
public interface IUnitInteractionRules { bool CanHelp(IUnit source, IUnit target); bool CanHurt(IUnit source, IUnit target); }
这样可以避免IUnit承担不属于自身职责的判断逻辑,同时方便后续修改交互规则(比如新增阵营关系、状态限制等),无需修改IUnit接口。
内容的提问来源于stack exchange,提问作者DaemendLock
相关产品推荐
相关产品推荐

