当持有基类Player对象但需访问派生类专属Stats字段时,应采用何种设计模式?
优雅解决玩家行为与专属统计字段的访问问题
首先,先明确你的核心需求:既要通过统一的基类调用doThing(),又要在特定行为类中安全访问MeleeStats/RangedStats的专属字段,同时保留Player和Stats体系的分层设计。你提到的几种方案各有缺陷,这里推荐两个更优的设计思路:
方案一:泛型抽象类 + 类型约束(推荐,简洁且类型安全)
这个方案通过泛型给行为类添加类型约束,既保留基类的通用性,又能在派生类中直接访问特定类型的Player和Stats,完全避免强制转换。
先看修改后的代码实现:
// 保留你原有的Player与Stats层级结构,仅做少量可读性优化 abstract class Player { public abstract Stats GetStats(); } abstract class RangedPlayer : Player { public RangedStats RangedStats { get; init; } public override Stats GetStats() => RangedStats; } abstract class MeleePlayer : Player { public MeleeStats MeleeStats { get; init; } public override Stats GetStats() => MeleeStats; } class Stats { public int MaxHealth { get; set; } } class RangedStats : Stats { public int MaxAmmo { get; set; } } class MeleeStats : Stats { public int MeleeComboLength { get; set; } } // 非泛型基类:用于统一调用doThing(),满足你"无需关注具体类型"的需求 abstract class ThingPlayersCanDo { protected Player BasePlayer { get; } protected ThingPlayersCanDo(Player player) { BasePlayer = player; } public abstract void DoThing(); } // 泛型抽象类:添加类型约束,让派生类可以直接绑定到特定Player/Stats类型 abstract class ThingPlayersCanDo<TPlayer, TStats> : ThingPlayersCanDo where TPlayer : Player where TStats : Stats { protected TPlayer Player { get; } protected TStats PlayerStats { get; } protected ThingPlayersCanDo(TPlayer player) : base(player) { Player = player; // 这里的转换是安全的:因为TPlayer的GetStats必然返回对应TStats类型 PlayerStats = (TStats)player.GetStats(); } } // 近战专属行为类:直接使用MeleePlayer和MeleeStats,无需转换 class ThingMeleePlayersCanDo : ThingPlayersCanDo<MeleePlayer, MeleeStats> { public ThingMeleePlayersCanDo(MeleePlayer player) : base(player) { } public override void DoThing() { // 访问通用属性 Console.WriteLine($"当前生命值上限:{PlayerStats.MaxHealth}"); // 直接访问近战专属字段 Console.WriteLine($"可触发连击次数:{PlayerStats.MeleeComboLength}"); } } // 远程专属行为类:同理 class ThingRangedPlayersCanDo : ThingPlayersCanDo<RangedPlayer, RangedStats> { public ThingRangedPlayersCanDo(RangedPlayer player) : base(player) { } public override void DoThing() { Console.WriteLine($"当前生命值上限:{PlayerStats.MaxHealth}"); Console.WriteLine($"当前弹药上限:{PlayerStats.MaxAmmo}"); } }
这个方案的优势:
- 类型安全:编译时就能校验Player与Stats的类型匹配,完全避免运行时转换错误;
- 通用性保留:非泛型的
ThingPlayersCanDo基类允许你在不关心具体玩家类型的场景下,统一调用DoThing(); - 扩展性强:后续新增
MagicPlayer这类角色时,只需要新增对应的ThingMagicPlayersCanDo继承泛型基类即可; - 代码简洁:无需冗余的字段合并或字典存储,保持原有的类分层设计。
方案二:访问者模式(适合频繁新增行为的场景)
如果你的游戏后续需要给玩家体系添加大量不同的行为,且不想频繁修改Player类本身,访问者模式会更符合开闭原则(对扩展开放,对修改关闭)。
实现思路是让Player类接受一个访问者,由访问者来处理不同类型玩家的专属逻辑:
// 定义访问者接口,每个Player类型对应一个Visit方法 interface IPlayerActionVisitor { void Visit(MeleePlayer player); void Visit(RangedPlayer player); } // 修改Player基类,添加Accept方法 abstract class Player { public abstract Stats GetStats(); public abstract void Accept(IPlayerActionVisitor visitor); } class RangedPlayer : Player { public RangedStats RangedStats { get; init; } public override Stats GetStats() => RangedStats; public override void Accept(IPlayerActionVisitor visitor) => visitor.Visit(this); } class MeleePlayer : Player { public MeleeStats MeleeStats { get; init; } public override Stats GetStats() => MeleeStats; public override void Accept(IPlayerActionVisitor visitor) => visitor.Visit(this); } // 把"玩家能做的事"实现为访问者 class MeleeComboAction : IPlayerActionVisitor { public void Visit(MeleePlayer player) { // 直接访问近战玩家的专属统计字段 Console.WriteLine($"触发连击,最大连击数:{player.MeleeStats.MeleeComboLength}"); } public void Visit(RangedPlayer player) { // 对不支持的玩家类型,可以抛出异常或做兼容处理 throw new InvalidOperationException("远程玩家无法触发近战连击"); } } // 使用方式 var meleePlayer = new MeleePlayer { MeleeStats = new MeleeStats { MaxHealth = 120, MeleeComboLength = 4 } }; var comboAction = new MeleeComboAction(); meleePlayer.Accept(comboAction);
这个方案的优势:
- 行为与玩家解耦:新增行为时不需要修改Player类,只需要新增访问者实现;
- 集中处理逻辑:同类型的行为逻辑可以集中在一个访问者类中,便于维护;
缺点:
- Player类型扩展成本高:如果新增
MagicPlayer,需要修改访问者接口和所有现有访问者实现,违反开闭原则,所以适合Player类型相对固定的场景。
方案对比
- 如果你只是需要解决当前的类型访问问题,且Player类型不会频繁新增,泛型方案是更简洁直接的选择;
- 如果你需要给玩家体系添加大量不同的行为,且Player类型相对稳定,访问者模式会更适合。
内容的提问来源于stack exchange,提问作者Houou In Kyouma
相关产品推荐
相关产品推荐

