OOP架构疑问:实体动作逻辑的类归属与调用最佳实践
我在搭建AI结构时,接口设计上遇到了问题,相关代码如下(注释较为简略):
//Probability是我自定义的类,用于表示概率 public interface IAction { /// <summary> /// 返回IEntity参数的可能未来状态及其预估概率的Dictionary /// </summary> Dictionary<IEntity, Probability> EstimatePossibleOutcomeSpectrum(IEntity entity); /// <summary> /// 让IEntity参数执行此动作 /// </summary> void Do(IEntity entity); } public interface IEntity { }按常理应为实体(Entity)执行动作(Action),而非动作作用于实体。我可重命名方法优化语法,但希望将Do方法移至IEntity中。不过实现接口时,我不知如何让实体执行动作,只能通过类似以下的转发调用方式:
public class EntityExample { /// <summary> /// 返回执行IAction参数后的预估未来状态Dictionary /// </summary> Dictionary<IEntity, Probability> EstimatePossibleOutcomeSpectrum(IAction action) { action.[Some method or a Action<Entity> call](this); } /// <summary> /// 执行传入的Action参数 /// </summary> void Do(IAction action) { action.[Some method or a Action<Entity> call](this); } }这种方式感觉和原方案一样甚至更糟。虽然初始方案功能正常,但我认为这近乎不良写法。请问此类场景下的最佳编码实践是什么?
首先得说,你能注意到“实体执行动作”这个语义上的合理性,已经比很多只追求功能实现的开发者想得深了——确实,从领域建模的角度,动作(Action)更应该是实体(Entity)可以选择的行为,而不是反过来让动作“操作”实体。
先拆解下你目前遇到的核心矛盾:
- 原方案里
IAction.Do(IEntity)的语义是“动作作用于实体”,不符合领域直觉 - 想把
Do移到IEntity里,但又陷入了转发调用的尴尬,本质上还是把逻辑丢回给了IAction,没解决问题
下面给你几种适配不同场景的最佳实践方向:
1. 双重分派(Double Dispatch)——语义与灵活性兼顾
这是解决这类“跨接口交互”问题的经典方案,既能保留IEntity执行动作的语义,又能让不同的IAction实现各自的逻辑,完美平衡语义正确性和扩展性。
调整后的接口设计
// 自定义概率类(你已实现,这里仅示意) public class Probability { /* ... */ } // 动作接口,新增Accept方法支持实体访问 public interface IAction { Dictionary<IEntity, Probability> EstimatePossibleOutcomeSpectrum(IEntity entity); // 让实体可以"访问"动作,触发双重分派 void Accept(IEntity entity); } // 实体接口,定义执行动作的方法和动作适配入口 public interface IEntity { void Do(IAction action); Dictionary<IEntity, Probability> EstimateOutcome(IAction action); // 为每个具体动作提供重载(双重分派的核心) void ExecuteAction(AttackAction action); void ExecuteAction(MoveAction action); Dictionary<IEntity, Probability> EstimateAttackOutcome(AttackAction action); Dictionary<IEntity, Probability> EstimateMoveOutcome(MoveAction action); }
具体实体实现示例
public class EnemyEntity : IEntity { public int Health { get; set; } public int Position { get; set; } public void Do(IAction action) { // 让动作反过来调用实体的适配方法,完成语义反转 action.Accept(this); } public Dictionary<IEntity, Probability> EstimateOutcome(IAction action) { return action.EstimatePossibleOutcomeSpectrum(this); } // 具体动作的执行逻辑,完全属于实体职责 public void ExecuteAction(AttackAction action) { // 敌人执行攻击的具体逻辑 Console.WriteLine($"Enemy attacks for {action.Damage} damage!"); } public void ExecuteAction(MoveAction action) { // 敌人移动的具体逻辑 Position += action.Distance; Console.WriteLine($"Enemy moves to position {Position}"); } // 具体动作的预估逻辑 public Dictionary<IEntity, Probability> EstimateAttackOutcome(AttackAction action) { // 模拟攻击后的状态预估 var result = new Dictionary<IEntity, Probability>(); var damagedSelf = new EnemyEntity { Health = Health - 5, Position = Position }; result.Add(damagedSelf, new Probability { Value = 0.8 }); return result; } public Dictionary<IEntity, Probability> EstimateMoveOutcome(MoveAction action) { var result = new Dictionary<IEntity, Probability>(); var movedSelf = new EnemyEntity { Health = Health, Position = Position + action.Distance }; result.Add(movedSelf, new Probability { Value = 1.0 }); return result; } }
具体动作实现示例
public class AttackAction : IAction { public int Damage { get; set; } = 10; public Dictionary<IEntity, Probability> EstimatePossibleOutcomeSpectrum(IEntity entity) { if (entity is EnemyEntity enemy) { return enemy.EstimateAttackOutcome(this); } throw new NotSupportedException("AttackAction only supports EnemyEntity"); } public void Accept(IEntity entity) { if (entity is EnemyEntity enemy) { enemy.ExecuteAction(this); } else { throw new NotSupportedException("AttackAction only supports EnemyEntity"); } } } public class MoveAction : IAction { public int Distance { get; set; } = 5; public Dictionary<IEntity, Probability> EstimatePossibleOutcomeSpectrum(IEntity entity) { if (entity is EnemyEntity enemy) { return enemy.EstimateMoveOutcome(this); } throw new NotSupportedException("MoveAction only supports EnemyEntity"); } public void Accept(IEntity entity) { if (entity is EnemyEntity enemy) { enemy.ExecuteAction(this); } else { throw new NotSupportedException("MoveAction only supports EnemyEntity"); } } }
这种方式的核心优势:
- 语义完全符合直觉:
var enemy = new EnemyEntity(); enemy.Do(new AttackAction()); - 实体和动作的职责清晰:实体负责“执行动作的逻辑”,动作负责“定义动作的属性/规则”
- 扩展性强:新增动作时只需新增
IAction实现,以及在实体中添加对应的适配方法,无需修改原有接口逻辑
2. 将动作作为实体固有方法——极简设计(适合动作固定场景)
如果你的AI系统中动作类型很少,且属于实体的固有行为(比如敌人只有攻击、移动两种动作),那可以直接把动作定义为IEntity的方法,完全省去IAction接口:
public interface IEntity { Dictionary<IEntity, Probability> EstimateAttackOutcome(); void Attack(); Dictionary<IEntity, Probability> EstimateMoveOutcome(); void Move(); } public class EnemyEntity : IEntity { public int Health { get; set; } public int Position { get; set; } public void Attack() { // 攻击逻辑 } public Dictionary<IEntity, Probability> EstimateAttackOutcome() { // 攻击预估逻辑 return new Dictionary<IEntity, Probability>(); } public void Move() { // 移动逻辑 } public Dictionary<IEntity, Probability> EstimateMoveOutcome() { // 移动预估逻辑 return new Dictionary<IEntity, Probability>(); } }
这种方式的优点是极其简单、语义最直接,但缺点是违反开闭原则——新增动作时必须修改IEntity接口,适合小型或动作固定的AI系统。
3. 中介者模式——解耦复杂交互(适合多实体多动作场景)
如果你的系统中有大量实体类型和动作类型,且它们的交互逻辑复杂(比如不同实体执行同一动作的逻辑差异极大),可以引入中介者模式来集中管理所有实体与动作的交互逻辑:
核心设计
public interface IActionMediator { Dictionary<IEntity, Probability> EstimateOutcome(IEntity entity, IAction action); void ExecuteAction(IEntity entity, IAction action); } public class ActionMediator : IActionMediator { public Dictionary<IEntity, Probability> EstimateOutcome(IEntity entity, IAction action) { // 集中处理所有实体+动作的预估逻辑 switch (entity, action) { case (EnemyEntity enemy, AttackAction attack): return enemy.EstimateAttackOutcome(attack); case (PlayerEntity player, MoveAction move): return player.EstimateMoveOutcome(move); // 其他组合逻辑 default: throw new NotSupportedException(); } } public void ExecuteAction(IEntity entity, IAction action) { // 集中处理所有实体+动作的执行逻辑 switch (entity, action) { case (EnemyEntity enemy, AttackAction attack): enemy.Attack(attack); break; case (PlayerEntity player, MoveAction move): player.Move(move); break; // 其他组合逻辑 default: throw new NotSupportedException(); } } } // 简化后的实体接口 public interface IEntity { void Do(IAction action, IActionMediator mediator); Dictionary<IEntity, Probability> EstimateOutcome(IAction action, IActionMediator mediator); } // 实体实现 public class EnemyEntity : IEntity { public void Do(IAction action, IActionMediator mediator) { mediator.ExecuteAction(this, action); } public Dictionary<IEntity, Probability> EstimateOutcome(IAction action, IActionMediator mediator) { return mediator.EstimateOutcome(this, action); } // 内部动作逻辑,仅对中介者开放 internal void Attack(AttackAction action) { /* ... */ } internal Dictionary<IEntity, Probability> EstimateAttackOutcome(AttackAction action) { /* ... */ } }
这种方式的优势是完全解耦实体与动作的依赖,所有交互逻辑集中在中介者中,便于维护复杂的交互规则,但会增加一层抽象,适合大型、复杂的AI系统。
最终选择建议
- 如果追求语义正确+灵活性强:双重分派是首选方案,兼顾了领域建模和代码扩展性
- 如果动作类型固定、系统简单:直接将动作作为实体方法最简洁
- 如果实体和动作种类繁多、交互复杂:中介者模式能更好地管理依赖
内容的提问来源于stack exchange,提问作者Shefeto




