Day 6 - 小项目:工程化小应用
建议用时:280-340 分钟
你将学会什么
- 如何把解决方案结构、依赖注入、配置、日志、测试思想组合起来
- 如何让入口层只负责组装和调用
- 如何让 Service 负责业务规则
- 如何让 Repository 负责数据保存
- 如何用配置控制文件路径和日志开关
- 如何用假的 Repository 验证服务逻辑
今天的小项目重点不是功能多,而是结构清楚。一个商品新增功能,也可以写出入口、配置、日志、Service、Repository、测试替身这些工程化骨架。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“单元测试”,今天继续学习“小项目:工程化小应用”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“小项目:工程化小应用”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。工程化不是把代码拆很多文件,而是让每块代码都有明确职责。
1. 小项目也要有结构
哪怕只有一个“新增商品”功能,也能拆成:
| 职责 | 对应代码 |
|---|---|
| 入口 | 读取配置、创建对象、调用服务 |
| 配置 | AppOptions |
| 日志 | IAppLogger |
| 业务规则 | Product、ProductService |
| 数据保存 | IProductRepository |
| 文件实现 | FileProductRepository |
| 测试替身 | FakeProductRepository |
结构不是为了复杂,而是为了以后能改。
2. 入口层负责组装
入口层做这些事:
- 读取配置。
- 创建日志对象。
- 创建 Repository。
- 创建 Service。
- 调用 Service。
- 处理最终错误提示。
入口层不应该写商品价格校验规则。
3. Service 负责业务流程
ProductService 负责:
- 记录开始日志。
- 创建商品。
- 调用 Repository 保存。
- 记录成功日志。
它不负责:
- 配置文件怎么读取。
- 商品保存到哪个文件。
- 日志最终输出到哪里。
4. 实体负责自己的基本规则
Product 负责:
- 名称不能为空。
- 价格必须大于 0。
- 名称需要 Trim。
这些规则属于商品自己,不应该散落在入口层。
5. Repository 负责数据保存
Repository 只负责保存和读取数据。
例如:
FileProductRepository 保存到文件
MemoryProductRepository 保存到内存
SqlProductRepository 保存到数据库Service 依赖接口,所以可以替换实现。
6. 配置负责变化项
本项目中变化项是:
- 商品文件路径。
- 是否启用日志。
以后还可以加:
- 日志级别。
- 接口地址。
- 超时时间。
- 数据库连接字符串。
7. 测试为什么需要假的 Repository
测试 ProductService 时,不需要真的写文件。
可以用:
MemoryProductRepository它把商品保存到列表里,测试就能检查列表数量。
工程化小应用常用操作速查
| 需求 | 常用写法 |
|---|---|
| Core 放业务规则 | Product、ProductService |
| Infrastructure 放实现 | JsonProductRepository |
| Cli 放入口 | Program.cs |
| Tests 放测试 | ProductServiceTests |
| 服务依赖接口 | ProductService(IProductRepository repo) |
| 程序注册服务 | services.AddSingleton<IProductRepository, ...>() |
| 读取配置 | IConfiguration |
| 记录日志 | ILogger<T> |
| 验证规则 | xUnit Assert |
工程化最小闭环:
CLI 读取输入
-> 调用 Service
-> Service 校验业务规则
-> Repository 保存数据
-> Logger 记录关键过程
-> Tests 验证 Service第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要能写出一个结构清楚的小应用。
先看效果:配置 + 日志 + Service + 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();
options.Validate();
IAppLogger logger = new ConsoleAppLogger(options.EnableLog);
IProductRepository repository = new FileProductRepository(options.DataFile);
ProductService service = new ProductService(repository, logger);
try
{
Product product = service.Create(" 键盘 ", 199m);
Console.WriteLine($"创建成功: {product.Name} | {product.Price}");
}
catch (ArgumentException ex)
{
logger.Error("创建商品失败", ex);
}
Console.WriteLine("文件内容:");
Console.WriteLine(await File.ReadAllTextAsync(options.DataFile));
class AppOptions
{
public string DataFile { get; set; } = "products.txt";
public bool EnableLog { get; set; } = true;
public void Validate()
{
if (string.IsNullOrWhiteSpace(DataFile))
{
throw new InvalidOperationException("DataFile 不能为空");
}
}
}
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={name}");
Product product = new Product(name, price);
_repository.Save(product);
_logger.Information($"商品创建成功,Name={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; }
}这个小项目已经包含 Week 5 的主线:
| 能力 | 代码位置 |
|---|---|
| 配置 | AppOptions |
| 日志 | IAppLogger、ConsoleAppLogger |
| 依赖注入 | new ProductService(repository, logger) |
| Repository | IProductRepository、FileProductRepository |
| Service | ProductService |
| 业务规则 | Product 构造函数 |
| 错误处理 | try/catch |
第三部分:跟着敲代码
从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。
动手前先做这 3 件事
- 先跑通最小新增商品。
- 每次只加一个结构:配置、日志、Repository、测试替身。
- 每一步都运行一次。
例子 1:最小商品创建
Product product = new Product(" 键盘 ", 199m);
Console.WriteLine($"{product.Name} | {product.Price}");
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; }
}先把业务规则放进实体。
例子 2:加入 Repository 接口
IProductRepository repository = new MemoryProductRepository();
Product product = new Product("键盘", 199m);
repository.Save(product);
Console.WriteLine($"保存数量: {((MemoryProductRepository)repository).Products.Count}");
interface IProductRepository
{
void Save(Product 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:加入 Service
MemoryProductRepository repository = new MemoryProductRepository();
ProductService service = new ProductService(repository);
service.Create("键盘", 199m);
Console.WriteLine($"保存数量: {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)
{
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 负责保存。
例子 4:加入配置
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;
}配置先能读取,再传给后面的对象使用。
例子 5:加入日志接口
IAppLogger logger = new ConsoleAppLogger(enabled: true);
logger.Information("程序启动");
logger.Error("模拟错误", new InvalidOperationException("测试错误"));
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}");
}
}
}日志也用接口,未来可以换成文件日志或框架日志。
例子 6:完整工程化小应用
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();
options.Validate();
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;
public void Validate()
{
if (string.IsNullOrWhiteSpace(DataFile))
{
throw new InvalidOperationException("DataFile 不能为空");
}
}
}
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; }
}例子 7:用假的 Repository 验证 Service
FakeProductRepository repository = new FakeProductRepository();
NullLogger logger = new NullLogger();
ProductService service = new ProductService(repository, logger);
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);
}
}
interface IAppLogger
{
void Information(string message);
void Error(string message, Exception? exception = null);
}
class NullLogger : IAppLogger
{
public void Information(string message)
{
}
public void Error(string message, Exception? exception = null)
{
}
}
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("开始创建商品");
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; }
}这个例子就是测试思路:不写文件,只验证服务规则和保存调用。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 小项目只堆功能 | 后面难维护 | 按 Core、Services、UI 或 Console 分层 |
| 没有日志 | 出错后无法定位 | 关键流程记录输入、结果和异常 |
| 没有配置 | 路径、开关、参数都写死 | 把可变内容放配置文件 |
| 没有测试核心规则 | 改动后容易破坏旧逻辑 | 给服务层和校验规则补测试 |
| README 不写运行命令 | 接手者不知道怎么启动 | 写清 restore、build、run |
小白重复敲写训练
工程化小项目先分别验证服务、配置和日志,再组合。
训练 1:仓储接口和内存实现
IProductRepository repository = new MemoryProductRepository();
repository.Add("Keyboard");
Console.WriteLine(string.Join(", ", repository.GetAll()));
interface IProductRepository
{
void Add(string name);
IReadOnlyList<string> GetAll();
}
class MemoryProductRepository : IProductRepository
{
private readonly List<string> _items = new();
public void Add(string name) => _items.Add(name);
public IReadOnlyList<string> GetAll() => _items;
}训练 2:服务依赖仓储
class ProductService(IProductRepository repository)
{
public void Create(string name)
{
if (string.IsNullOrWhiteSpace(name)) return;
repository.Add(name.Trim());
}
}改动任务:增加重复名称检查。
训练 3:给服务写测试
[Fact]
public void Create_EmptyName_DoesNotAdd()
{
var repository = new MemoryProductRepository();
var service = new ProductService(repository);
service.Create(" ");
Assert.Empty(repository.GetAll());
}第三遍再写一个成功添加的测试。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:小项目:工程化小应用。今天至少要掌握的 3 个点是:
1. 如何把解决方案结构、依赖注入、配置、日志、测试思想组合起来
2. 如何让入口层只负责组装和调用
3. 如何让 Service 负责业务规则答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:如何把解决方案结构、依赖注入、配置、日志、测试思想组合起来。
上位机专项练习
把分层、配置、依赖注入、日志和测试组合成可维护的设备监控控制台。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:业务服务只依赖接口
class DeviceService
{
private readonly IReader reader;
public DeviceService(IReader reader) => this.reader = reader;
public double Collect() => reader.Read();
}运行结果或界面效果:
DeviceService 不知道底层是串口还是模拟器改动任务: 增加 IsAlarm 方法。
专项例子 2:启动时组合对象
IReader reader = new FakeReader();
var service = new DeviceService(reader);
double value = service.Collect();
Console.WriteLine($"采集值: {value}");运行结果或界面效果:
采集值: 模拟读取值改动任务: 把 FakeReader 换成第二个实现。
专项例子 3:输出运行摘要
var summary = new
{
DeviceCount = 3,
OnlineCount = 2,
AlarmCount = 1
};
Console.WriteLine($"设备 {summary.DeviceCount} / 在线 {summary.OnlineCount} / 报警 {summary.AlarmCount}");运行结果或界面效果:
设备 3 / 在线 2 / 报警 1改动任务: 增加 OfflineCount。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。建议先照着敲一遍,再改配置值和错误输入验证。
作业 1:完整工程化小应用
要求:
- 从
appsettings.json读取DataFile和EnableLog。 ProductService通过接口依赖 Repository 和 Logger。- 商品名不能为空,价格必须大于 0。
- 保存到配置指定的文件。
- 正常流程有日志,异常流程有错误日志。
完整答案
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();
options.Validate();
IAppLogger logger = new ConsoleAppLogger(options.EnableLog);
IProductRepository repository = new FileProductRepository(options.DataFile);
ProductService service = new ProductService(repository, logger);
try
{
Product product = service.Create("键盘", 199m);
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;
public void Validate()
{
if (string.IsNullOrWhiteSpace(DataFile))
{
throw new InvalidOperationException("DataFile 不能为空");
}
}
}
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; }
}作业 2:测试替身完整答案
FakeProductRepository repository = new FakeProductRepository();
NullLogger logger = new NullLogger();
ProductService service = new ProductService(repository, logger);
service.Create("鼠标", 88m);
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);
}
}
interface IAppLogger
{
void Information(string message);
void Error(string message, Exception? exception = null);
}
class NullLogger : IAppLogger
{
public void Information(string message)
{
}
public void Error(string message, Exception? exception = null)
{
}
}
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("开始创建商品");
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 里。
- Service 不直接 new Repository。
- 日志通过接口注入。
- 文件保存只在 Repository 里。
- 商品规则在 Product 或 ProductService 里。
- 可以用 Fake Repository 验证 Service。
本页最后要记住
- 工程化不是把代码拆散,而是让职责清楚。
- 入口层负责组装和调用。
- 配置负责变化参数。
- 日志负责运行线索。
- Service 负责业务流程。
- Repository 负责数据读写。
- 接口让真实实现和测试替身都能替换。