Skip to Content
Week 05📋 Week 05 阶段检查

Week 05 - 阶段检查

这一页用于确认第五周工程化基础是否能落到代码里。不是只背名词,要能写出结构清楚的小功能。

本周学习地图

阶段先掌握什么最终能写什么
结构.sln、多项目、项目引用能把代码拆成 Core、App、Tests
依赖接口、实现、构造函数注入能替换 Repository 实现
配置appsettings.json、配置读取能把路径和开关放到代码外
日志ILogger、不同日志级别能记录关键流程和错误
测试xUnit、断言、测试命名能验证服务层规则
综合工程化小应用能写出可维护的小项目骨架

本周自测项目:工程化商品服务

从空解决方案开始搭一个商品服务,至少包含:

  • Product.Core 放模型、接口、服务。
  • Product.App 调用服务完成新增和查询。
  • Product.Tests 测试商品校验规则。
  • Repository 通过接口注入到 Service。
  • 文件路径从配置读取。
  • 保存失败时写日志。

过关标准:能画出项目引用方向,并能解释为什么 Core 不应该依赖 UI。

上位机阶段验收:分层设备监控解决方案

本周目标: 把模型、业务、通信、配置、日志和测试拆成可维护工程。

必须亲手完成:

  1. Core 不引用界面和通信实现
  2. 依赖注入切换 FakeReader 与真实 Reader
  3. 报警规则至少包含 3 个边界测试

过关标准: 修改通信实现时不改业务服务,错误能从日志定位,测试能一条命令运行。

不要只看答案。新建一个空项目重做一次,运行成功后再故意改坏一处并自己排错。

本周流程图

Solution -> App 项目接收输入 -> Core 项目定义模型、接口、服务 -> Infrastructure 项目实现文件或数据库读写 -> DI 把接口和实现连接起来 -> 配置提供可变参数 -> 日志记录运行过程 -> Tests 验证 Core 规则

第一部分:本周原理地图

第五周主线是:让项目从“能跑”变成“好维护、好替换、好排错、好测试”。

知识点解决的问题
解决方案结构多项目怎么组织
Core业务规则和接口放哪里
Infrastructure文件、数据库、网络实现放哪里
依赖注入对象需要的依赖从外部传入
配置变化值离开代码
日志程序运行过程可追踪
单元测试用代码验证业务规则

本周判断表

场景应该想到什么
商品名称不能为空Core 里的实体或服务
保存商品到文件Infrastructure
Service 需要 Repository构造函数注入
文件路径要可修改配置
保存失败要排查日志
测 Service 不想写文件Fake Repository
多个项目统一管理Solution

第五周必须会用的命令和 API

主题必须会的写法
解决方案dotnet new slndotnet sln add
项目引用dotnet add reference
构建测试dotnet builddotnet test
依赖注入ServiceCollectionAddSingletonAddTransientBuildServiceProvider
取服务GetRequiredService<T>()
配置ConfigurationBuilderAddJsonFileGetValue<T>
日志ILogger<T>LogInformationLogWarningLogError
xUnit[Fact][Theory]Assert.EqualAssert.Throws
分层接口IProductRepositoryProductService

第二部分:阶段检查题

检查 1:解决方案结构

完整答案

ProductApp/ ProductApp.sln src/ ProductApp.Console/ 入口层:读取配置、组装对象、调用服务 ProductApp.Core/ 核心层:实体、接口、业务规则、服务 ProductApp.Infrastructure/ 基础设施层:文件、数据库、网络实现 tests/ ProductApp.Tests/ 测试层:验证 Core 的业务规则

引用方向:

Console -> Core Console -> Infrastructure Infrastructure -> Core Tests -> Core Core 不引用 Console Core 不引用 Infrastructure

检查 2:依赖注入

你要会写:Service 依赖接口,具体实现从外面传进来。

完整答案

IProductRepository repository = new MemoryProductRepository(); ProductService service = new ProductService(repository); service.Create("键盘", 199m); Console.WriteLine($"保存数量: {((MemoryProductRepository)repository).Products.Count}"); interface IProductRepository { void Save(Product product); } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public Product Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); return product; } } class MemoryProductRepository : IProductRepository { public List<Product> Products { get; } = new List<Product>(); public void Save(Product product) { Products.Add(product); } } class Product { public Product(string name, decimal price) { Name = name; Price = price; } public string Name { get; } public decimal Price { get; } }

检查 3:配置管理

你要会写:从 JSON 读取配置对象,并把配置交给服务。

完整答案

using System.Text.Json; await File.WriteAllTextAsync("appsettings.json", """ { "DataFile": "products.txt", "EnableLog": true } """); string json = await File.ReadAllTextAsync("appsettings.json"); AppOptions options = JsonSerializer.Deserialize<AppOptions>(json) ?? new AppOptions(); Console.WriteLine(options.DataFile); Console.WriteLine(options.EnableLog); class AppOptions { public string DataFile { get; set; } = "products.txt"; public bool EnableLog { get; set; } = true; }

检查 4:日志体系

你要会写:日志接口和控制台实现。

完整答案

IAppLogger logger = new ConsoleAppLogger(); logger.Information("开始保存商品"); logger.Error("保存失败", new InvalidOperationException("磁盘空间不足")); interface IAppLogger { void Information(string message); void Error(string message, Exception? exception = null); } class ConsoleAppLogger : IAppLogger { public void Information(string message) { Console.WriteLine($"{DateTime.Now:HH:mm:ss} [INFO] {message}"); } public void Error(string message, Exception? exception = null) { Console.WriteLine($"{DateTime.Now:HH:mm:ss} [ERROR] {message}"); if (exception is not null) { Console.WriteLine($"{exception.GetType().Name}: {exception.Message}"); } } }

检查 5:测试替身

你要会写:用假的 Repository 验证 Service 行为。

完整答案

FakeProductRepository repository = new FakeProductRepository(); ProductService service = new ProductService(repository); Product product = service.Create("键盘", 199m); Console.WriteLine(product.Name == "键盘" ? "名称正确" : "名称错误"); Console.WriteLine(repository.Saved.Count == 1 ? "保存正确" : "保存错误"); interface IProductRepository { void Save(Product product); } class FakeProductRepository : IProductRepository { public List<Product> Saved { get; } = new List<Product>(); public void Save(Product product) { Saved.Add(product); } } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public Product Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); return product; } } class Product { public Product(string name, decimal price) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("商品名称不能为空"); } if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Name = name.Trim(); Price = price; } public string Name { get; } public decimal Price { get; } }

第三部分:综合作业完整答案

要求:

  1. 配置里有文件路径和日志开关。
  2. Service 通过接口依赖 Repository 和 Logger。
  3. Product 校验名称和价格。
  4. Repository 保存到文件。
  5. 正常和异常流程都有输出。

完整答案

using System.Text.Json; await File.WriteAllTextAsync("appsettings.json", """ { "DataFile": "products.txt", "EnableLog": true } """); string json = await File.ReadAllTextAsync("appsettings.json"); AppOptions options = JsonSerializer.Deserialize<AppOptions>(json) ?? new AppOptions(); IAppLogger logger = new ConsoleAppLogger(options.EnableLog); IProductRepository repository = new FileProductRepository(options.DataFile); ProductService service = new ProductService(repository, logger); try { Product product = service.Create("显示器", 1299m); Console.WriteLine($"创建成功: {product.Name}"); } catch (ArgumentException ex) { logger.Error("创建商品失败", ex); } class AppOptions { public string DataFile { get; set; } = "products.txt"; public bool EnableLog { get; set; } = true; } interface IAppLogger { void Information(string message); void Error(string message, Exception? exception = null); } class ConsoleAppLogger : IAppLogger { private readonly bool _enabled; public ConsoleAppLogger(bool enabled) { _enabled = enabled; } public void Information(string message) { if (_enabled) { Console.WriteLine($"{DateTime.Now:HH:mm:ss} [INFO] {message}"); } } public void Error(string message, Exception? exception = null) { Console.WriteLine($"{DateTime.Now:HH:mm:ss} [ERROR] {message}"); if (exception is not null) { Console.WriteLine($"{exception.GetType().Name}: {exception.Message}"); } } } interface IProductRepository { void Save(Product product); } class FileProductRepository : IProductRepository { private readonly string _path; public FileProductRepository(string path) { _path = path; } public void Save(Product product) { File.AppendAllText(_path, $"{product.Name},{product.Price}" + Environment.NewLine); } } class ProductService { private readonly IProductRepository _repository; private readonly IAppLogger _logger; public ProductService(IProductRepository repository, IAppLogger logger) { _repository = repository; _logger = logger; } public Product Create(string name, decimal price) { _logger.Information($"开始创建商品: {name}"); Product product = new Product(name, price); _repository.Save(product); _logger.Information($"商品创建成功: {product.Name}"); return product; } } class Product { public Product(string name, decimal price) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("商品名称不能为空"); } if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Name = name.Trim(); Price = price; } public string Name { get; } public decimal Price { get; } }

第四部分:进入下一周前的最低通过线

进入下一周前,至少做到:

  1. 能画出 Console / Core / Infrastructure / Tests 的职责。
  2. 能写构造函数注入。
  3. 能用配置对象保存文件路径。
  4. 能写一个简单日志接口。
  5. 能用假的 Repository 验证 Service。
  6. 能说明为什么 Core 不应该依赖 Infrastructure。
  7. 能把商品新增功能从入口到保存完整跑通。

如果这 7 条能做到,第五周的工程化基础就达标了。后面换成桌面或 Web 项目时,这套结构仍然适用。