Week 05 - 阶段检查
这一页用于确认第五周工程化基础是否能落到代码里。不是只背名词,要能写出结构清楚的小功能。
本周学习地图
| 阶段 | 先掌握什么 | 最终能写什么 |
|---|---|---|
| 结构 | .sln、多项目、项目引用 | 能把代码拆成 Core、App、Tests |
| 依赖 | 接口、实现、构造函数注入 | 能替换 Repository 实现 |
| 配置 | appsettings.json、配置读取 | 能把路径和开关放到代码外 |
| 日志 | ILogger、不同日志级别 | 能记录关键流程和错误 |
| 测试 | xUnit、断言、测试命名 | 能验证服务层规则 |
| 综合 | 工程化小应用 | 能写出可维护的小项目骨架 |
本周自测项目:工程化商品服务
从空解决方案开始搭一个商品服务,至少包含:
Product.Core放模型、接口、服务。Product.App调用服务完成新增和查询。Product.Tests测试商品校验规则。- Repository 通过接口注入到 Service。
- 文件路径从配置读取。
- 保存失败时写日志。
过关标准:能画出项目引用方向,并能解释为什么 Core 不应该依赖 UI。
上位机阶段验收:分层设备监控解决方案
本周目标: 把模型、业务、通信、配置、日志和测试拆成可维护工程。
必须亲手完成:
- Core 不引用界面和通信实现
- 依赖注入切换 FakeReader 与真实 Reader
- 报警规则至少包含 3 个边界测试
过关标准: 修改通信实现时不改业务服务,错误能从日志定位,测试能一条命令运行。
不要只看答案。新建一个空项目重做一次,运行成功后再故意改坏一处并自己排错。
本周流程图
Solution
-> App 项目接收输入
-> Core 项目定义模型、接口、服务
-> Infrastructure 项目实现文件或数据库读写
-> DI 把接口和实现连接起来
-> 配置提供可变参数
-> 日志记录运行过程
-> Tests 验证 Core 规则第一部分:本周原理地图
第五周主线是:让项目从“能跑”变成“好维护、好替换、好排错、好测试”。
| 知识点 | 解决的问题 |
|---|---|
| 解决方案结构 | 多项目怎么组织 |
| Core | 业务规则和接口放哪里 |
| Infrastructure | 文件、数据库、网络实现放哪里 |
| 依赖注入 | 对象需要的依赖从外部传入 |
| 配置 | 变化值离开代码 |
| 日志 | 程序运行过程可追踪 |
| 单元测试 | 用代码验证业务规则 |
本周判断表
| 场景 | 应该想到什么 |
|---|---|
| 商品名称不能为空 | Core 里的实体或服务 |
| 保存商品到文件 | Infrastructure |
| Service 需要 Repository | 构造函数注入 |
| 文件路径要可修改 | 配置 |
| 保存失败要排查 | 日志 |
| 测 Service 不想写文件 | Fake Repository |
| 多个项目统一管理 | Solution |
第五周必须会用的命令和 API
| 主题 | 必须会的写法 |
|---|---|
| 解决方案 | dotnet new sln、dotnet sln add |
| 项目引用 | dotnet add reference |
| 构建测试 | dotnet build、dotnet test |
| 依赖注入 | ServiceCollection、AddSingleton、AddTransient、BuildServiceProvider |
| 取服务 | GetRequiredService<T>() |
| 配置 | ConfigurationBuilder、AddJsonFile、GetValue<T> |
| 日志 | ILogger<T>、LogInformation、LogWarning、LogError |
| xUnit | [Fact]、[Theory]、Assert.Equal、Assert.Throws |
| 分层接口 | IProductRepository、ProductService |
第二部分:阶段检查题
检查 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; }
}第三部分:综合作业完整答案
要求:
- 配置里有文件路径和日志开关。
- Service 通过接口依赖 Repository 和 Logger。
- Product 校验名称和价格。
- Repository 保存到文件。
- 正常和异常流程都有输出。
完整答案
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; }
}第四部分:进入下一周前的最低通过线
进入下一周前,至少做到:
- 能画出 Console / Core / Infrastructure / Tests 的职责。
- 能写构造函数注入。
- 能用配置对象保存文件路径。
- 能写一个简单日志接口。
- 能用假的 Repository 验证 Service。
- 能说明为什么 Core 不应该依赖 Infrastructure。
- 能把商品新增功能从入口到保存完整跑通。
如果这 7 条能做到,第五周的工程化基础就达标了。后面换成桌面或 Web 项目时,这套结构仍然适用。