现有C#控制台应用代码的最佳组织结构方案咨询
C#控制台项目合理组织结构与规范实现
把代码拆分到不同文件、按职责划分模块是非常好的实践,能大幅提升代码的可读性、可维护性和可测试性。针对你的阿森纳球员统计项目,这里给出一套清晰的重构方案:
一、核心拆分原则:单一职责
每个类/文件只负责一件事,避免把模型、数据读取、业务逻辑、UI交互混在一起。
二、重构后的项目结构
ArsenalPlayerTracker/ ├── Models/ # 数据模型层:存放实体类 │ └── Player.cs ├── Data/ # 数据访问层:负责数据加载/持久化 │ └── PlayerDataReader.cs ├── Services/ # 业务逻辑层:处理统计、筛选等核心逻辑 │ └── PlayerStatisticsService.cs ├── Program.cs # UI层:控制台交互与输出 └── appsettings.json # 配置文件:存放CSV路径等可配置项
三、各模块代码实现示例
1. Models/Player.cs
纯粹作为数据载体,不包含业务逻辑或数据解析代码:
namespace ArsenalPlayerTracker.Models { public class Player { public string PlayerId { get; set; } public string Name { get; set; } public int BirthYear { get; set; } public string Nationality { get; set; } public string Position { get; set; } public string ClubId { get; set; } public string ClubName { get; set; } public int ShirtNumber { get; set; } public int Rating { get; set; } public string StrongFoot { get; set; } public int Height { get; set; } public DateTime ContractStart { get; set; } public DateTime ContractEnd { get; set; } public int FormRating { get; set; } } }
2. Data/PlayerDataReader.cs
专门负责从CSV加载并解析球员数据,处理可能的格式异常:
using System.Collections.Generic; using System.IO; using System.Linq; using ArsenalPlayerTracker.Models; namespace ArsenalPlayerTracker.Data { public class PlayerDataReader { private readonly string _csvFilePath; public PlayerDataReader(string csvFilePath) { _csvFilePath = csvFilePath; } public IEnumerable<Player> LoadPlayers() { var players = new List<Player>(); var lines = File.ReadAllLines(_csvFilePath).Skip(1); // 跳过表头 foreach (var line in lines) { var data = line.Split(';'); if (data.Length != 14) continue; // 跳过格式错误的行 if (int.TryParse(data[2], out int birthYear) && int.TryParse(data[7], out int shirtNumber) && int.TryParse(data[8], out int rating) && int.TryParse(data[10], out int height) && DateTime.TryParse(data[11], out DateTime contractStart) && DateTime.TryParse(data[12], out DateTime contractEnd) && int.TryParse(data[13], out int formRating)) { players.Add(new Player { PlayerId = data[0], Name = data[1], BirthYear = birthYear, Nationality = data[3], Position = data[4], ClubId = data[5], ClubName = data[6], ShirtNumber = shirtNumber, Rating = rating, StrongFoot = data[9], Height = height, ContractStart = contractStart, ContractEnd = contractEnd, FormRating = formRating }); } } return players; } } }
3. Services/PlayerStatisticsService.cs
封装所有统计、筛选逻辑,依赖抽象而非具体数据:
using System.Collections.Generic; using System.Linq; using ArsenalPlayerTracker.Models; namespace ArsenalPlayerTracker.Services { public class PlayerStatisticsService { private readonly IEnumerable<Player> _players; public PlayerStatisticsService(IEnumerable<Player> players) { _players = players; } public int GetTotalPlayerCount() => _players.Count(); public IEnumerable<Player> GetForwards() => _players.Where(p => p.Position == "Forward"); public IEnumerable<Player> GetPlayersByNationality(string nationality) => _players.Where(p => p.Nationality.Equals(nationality, System.StringComparison.OrdinalIgnoreCase)); public double GetAverageRating() => _players.Any() ? _players.Average(p => p.Rating) : 0; public Player GetTopRatedPlayer() => _players.OrderByDescending(p => p.Rating).FirstOrDefault(); public double GetAverageRatingForClub(string clubName) { var clubPlayers = _players.Where(p => p.ClubName.Equals(clubName, System.StringComparison.OrdinalIgnoreCase)); return clubPlayers.Any() ? clubPlayers.Average(p => p.Rating) : 0; } } }
4. Program.cs
只负责控制台交互、调用各层服务,逻辑简洁:
using System; using ArsenalPlayerTracker.Data; using ArsenalPlayerTracker.Services; using Microsoft.Extensions.Configuration; namespace ArsenalPlayerTracker { internal class Program { static void Main(string[] args) { // 加载配置 var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: false) .Build(); var csvPath = config["CsvFilePath"]; // 初始化各层服务 var dataReader = new PlayerDataReader(csvPath); var players = dataReader.LoadPlayers(); var statsService = new PlayerStatisticsService(players); // 执行统计任务 Console.WriteLine($"Task 2: Number of players: {statsService.GetTotalPlayerCount()}"); Console.WriteLine("\nTask 3: Forwards:"); foreach (var p in statsService.GetForwards()) { Console.WriteLine($"\t{p.Name} - {p.Rating}"); } Console.Write("\nTask 4: Nationality: "); string nat = Console.ReadLine(); var natPlayers = statsService.GetPlayersByNationality(nat); if (!natPlayers.Any()) { Console.WriteLine("\tNo players found with this nationality."); } else { foreach (var p in natPlayers) { Console.WriteLine($"\t{p.Name} - {p.ClubName}"); } } Console.WriteLine($"\nTask 5: Average rating: {statsService.GetAverageRating():F2}"); var topPlayer = statsService.GetTopRatedPlayer(); if (topPlayer != null) { Console.WriteLine($"Task 6: Best player: {topPlayer.Name} ({topPlayer.Rating})"); } Console.WriteLine($"Task 7: Arsenal average rating: {statsService.GetAverageRatingForClub("Arsenal"):F2}"); Console.ReadKey(); } } }
5. appsettings.json
存放可配置参数,避免硬编码:
{ "CsvFilePath": "arsenal_players.csv" }
四、额外规范建议
- 命名空间:用项目名+模块名的结构(如
ArsenalPlayerTracker.Models),避免默认的ConsoleApp1 - 异常处理:在数据读取层加入TryParse而非直接Parse,防止程序因CSV格式错误崩溃
- 依赖注入:如果项目规模扩大,可以引入依赖注入框架(如Microsoft.Extensions.DependencyInjection)来管理服务实例
- 测试友好:业务逻辑层依赖
IEnumerable<Player>,方便编写单元测试时传入模拟数据
内容的提问来源于stack exchange,提问作者Fryback
相关产品推荐
相关产品推荐

