C#骑士锦标赛面向对象控制台应用代码评审请求
C#骑士锦标赛代码评审请求
我正在练习C#面向对象编程,开发了一个模拟骑士锦标赛(KnightTournament)的控制台应用。该应用包含武器基类Weapon与派生类(Sword、Lance、Club)、Knight类、Squire类,以及处理重复名称的自定义异常NameAlreadyExistsException,还实现了基于节点的参与者结构(ParticipantList类实现已省略)。现正式请求对代码进行评审,重点关注:
- 整体OOP设计与类结构
- 继承及
virtual/override方法的合理使用 - C#最佳实践与命名规范
代码实现
using System; namespace KnightTournament { public abstract class Weapon { private string _designation; public Weapon(string designation) { this._designation = designation; } public string Designation { get { return _designation; } } public virtual string GetWeaponKind() { return "Unknown"; } public override string ToString() { return _designation + " Kind: " + GetWeaponKind(); } } } namespace KnightTournament { public class Sword : Weapon { public Sword(string designation) : base(designation) { } public override string GetWeaponKind() { return "S"; } } } namespace KnightTournament { public class Knight { // Static variable to keep track of the next available ID // s_ stands for "static" and is a common convention for static fields // --> is a quasi-standard in the industry private static int s_nextId = 1; private int _id; private string _name; private string _phoneNumber; private string _nickname; private Squire _squire; private Weapon _weapon; public Knight(string name, string phoneNumber, string nickname) { this._id = s_nextId; s_nextId++; this._name = name; this._phoneNumber = phoneNumber; this._nickname = nickname; this._squire = null; this._weapon = null; } public int Id { get { return _id; } } public string Name { get { return _name; } } public string PhoneNumber { get { return _phoneNumber; } } public string Nickname { get { return _nickname; } } public void AddSquire(Squire squire) { this._squire = squire; } public void AddWeapon(Weapon weapon) { this._weapon = weapon; } public string GetWeaponKind() { if (_weapon != null) { return _weapon.GetWeaponKind(); } return ""; } public override string ToString() { string output = "Knight " + _id + "\t\tName: " + _name + "\t\tNickname: " + _nickname; if (_weapon != null) { output += "\n\t\tWeapon: " + _weapon.Designation + "\t\tKind: " + _weapon.GetWeaponKind(); } if (_squire != null) { output += "\n\t\tSquire: " + _squire.ToString(); } return output; } } } namespace KnightTournament { internal class Program { static void Main(string[] args) { ParticipantList list = new ParticipantList(); // Knight 1: August the Brave with a lance and a squire Knight knight1 = new Knight("August", "0664/1234567", "the Brave"); knight1.AddWeapon(new Lance("Battle Lance")); knight1.AddSquire(new Squire("Craven", "0664/7654321", 20)); list.AddParticipant(knight1); // Knight 2: Bernulf the Fast without a weapon and squire Knight knight2 = new Knight("Bernulf", "0664/1111111", "the Fast"); list.AddParticipant(knight2); // Knight 3: Christoph the Loyal with a sword Knight knight3 = new Knight("Christoph", "0664/2222222", "the Loyal"); knight3.AddWeapon(new Sword("Battle Sword")); list.AddParticipant(knight3); // Output all participants list.ListAllParticipants(); Console.WriteLine(); // Attempt to add a knight with an already existing name try { Console.WriteLine("Attempting to add a knight with an existing name again"); Knight knight4 = new Knight("Christoph", "0664/3333333", "the Strong"); list.AddParticipant(knight4); } catch (NameAlreadyExistsException ex) { Console.WriteLine("Exception: " + ex.Message); } Console.WriteLine(); // Output all knights with a sword (Weapon kind "S") list.AllWithWeaponKind("S"); //Console.ReadLine(); } } } namespace KnightTournament { public class Node { private Knight _data; private Node _next; public Node(Knight data) { this._data = data; this._next = null; } public Knight Data { get { return _data; } set { _data = value; } } public Node Next { get { return _next; } set { _next = value; } } } } namespace KnightTournament { public class NameAlreadyExistsException : Exception { public NameAlreadyExistsException(string message) : base(message + " already exists!") { } } } namespace KnightTournament { public class Lance : Weapon { public Lance(string designation) : base(designation) { } public override string GetWeaponKind() { return "L"; } } } namespace KnightTournament { public class Squire { private string _name; private string _phoneNumber; private int _trainingLevel; public Squire(string name, string phoneNumber, int trainingLevel) { this._name = name; this._phoneNumber = phoneNumber; this._trainingLevel = trainingLevel; } public string Name { get { return _name; } } public string PhoneNumber { get { return _phoneNumber; } } public int GetTrainingLevel() { return _trainingLevel; } public override string ToString() { return _name + " Level: " + _trainingLevel; } } } namespace KnightTournament { public class Club : Weapon { public Club(string designation) : base(designation) { } public override string GetWeaponKind() { return "C"; } } }
代码评审结果
1. 整体OOP设计与类结构
优点
- 类职责划分清晰:
Weapon作为抽象基类定义武器核心属性与行为,派生类实现具体武器类型;Knight、Squire、Node、自定义异常各司其职,符合单一职责原则。 - 依赖关系合理:
Knight关联Squire和Weapon,Node封装Knight数据,贴合现实业务逻辑。
可优化点
- 扩展性限制:
Node类直接绑定Knight,若未来需添加其他参与者类型(如魔法师),无法复用该节点结构。建议改为泛型Node<T>,提升复用性。 - 关联关系明确化:
Knight的AddSquire和AddWeapon是直接赋值逻辑,意味着骑士只能拥有1个侍从和1件武器。若业务允许多个,需改为集合(如List<Squire>);若需求就是一对一,建议添加注释说明设计意图。 - 属性封装一致性:
Squire的GetTrainingLevel方法可改为只读属性TrainingLevel,更符合C#面向对象的封装习惯。
2. 继承及virtual/override方法的合理使用
优点
- 多态性实现到位:
Weapon基类定义virtual的GetWeaponKind方法,派生类Sword、Lance、Club正确override实现各自类型标识,符合继承的设计初衷。 ToString方法设计合理:Weapon重写ToString展示武器信息,派生类可直接继承或按需扩展,简化了对象字符串输出逻辑。
可优化点
- 强制派生类实现核心方法:
Weapon的GetWeaponKind可改为abstract方法,因为所有具体武器都必须明确类型,避免基类返回"Unknown"的无效情况,强制派生类提供实现,增强代码严谨性。 - 减少冗余代码:
Knight的GetWeaponKind方法可通过暴露只读的Weapon属性替代,让外部直接判断武器是否存在并调用其方法,更灵活且减少冗余:
外部使用时可写:public Weapon Weapon => _weapon;knight.Weapon?.GetWeaponKind() ?? ""。
3. C#最佳实践与命名规范
优点
- 命名规范合规:类名、方法名、属性采用帕斯卡命名法(PascalCase),私有字段使用下划线前缀(如
_designation),静态字段用s_前缀并添加注释说明,符合C#行业规范。 - 异常处理规范:自定义异常
NameAlreadyExistsException正确继承Exception,并实现带消息的构造函数,符合异常处理最佳实践。
可优化点
- 简化代码语法:
- 构造函数中可简化赋值逻辑,如
_id = s_nextId++;代替两行代码;引用类型字段默认值为null,无需在构造函数中显式赋值(如_squire = null;可省略)。 - 字符串拼接建议使用插值字符串(
$""),提升可读性,例如Knight的ToString方法:public override string ToString() { var output = $"Knight {_id}\t\tName: {_name}\t\tNickname: {_nickname}"; if (_weapon != null) { output += $"\n\t\tWeapon: {_weapon.Designation}\t\tKind: {_weapon.GetWeaponKind()}"; } if (_squire != null) { output += $"\n\t\tSquire: {_squire}"; } return output; } - 私有字段可替换为只读自动属性,简化代码,例如
Weapon类:public abstract class Weapon { public string Designation { get; } public Weapon(string designation) { Designation = designation; } // ... 其他代码 }
- 构造函数中可简化赋值逻辑,如
- 统一命名空间:所有类无需单独声明
namespace KnightTournament,可统一放在同一个命名空间下,简化代码结构。 - 异常消息优化:
NameAlreadyExistsException的构造函数中,消息拼接使用插值字符串更清晰:public NameAlreadyExistsException(string message) : base($"{message} already exists!") { }
内容的提问来源于stack exchange,提问作者user32792950
相关产品推荐
相关产品推荐

