Skip to Content
Week 03Day 7 - 复盘与巩固

Day 7 - 第三周复盘与综合练习

建议用时:240-300 分钟

你将学会什么

  • 如何判断一段代码应该放在类、接口、服务还是事件里
  • 如何把类、封装、继承、接口、委托、Lambda、事件串成一条主线
  • 如何用一个小项目检查第三周是否真正掌握
  • 如何给自己的代码做验收:正常流程、错误流程、可修改性

复盘不是重新抄一遍笔记,而是把知识点变成判断方法:遇到业务名词时建类,遇到可替换能力时用接口,遇到变化规则时用委托,遇到发生后通知时用事件。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“小项目:订单域模型”,今天继续学习“第三周复盘与综合练习”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“第三周复盘与综合练习”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。目标是把第三周的知识点变成“遇到问题时知道该用什么”的判断方法。

1. 第三周的主线

第三周不是散点知识,它其实在解决同一个问题:

如何把代码从一堆语句,整理成可维护的小系统。

可维护的小系统至少要做到:

  1. 数据有归属,不要到处散落。
  2. 规则有位置,不要到处复制。
  3. 可替换的部分不要写死。
  4. 发生后的通知不要强行绑在一起。
  5. 错误情况要被模型或服务拦住。

2. 遇到业务名词,用类

业务名词通常会变成类:

业务名词
客户Customer
商品Product
订单Order
订单明细OrderLine
通知器Notifier

如果一个名词有多个属性,并且会在程序里反复出现,就不要一直用散变量表示它。

3. 遇到对象自己的规则,用方法和封装

订单自己的规则应该放在订单里。

例如:

规则应该放哪里
没有商品不能提交Order.Submit()
只有已提交订单可以支付Order.Pay()
提交后不能添加商品Order.AddLine()
数量必须大于 0OrderLine 构造函数

如果规则放在外面,很容易有的入口检查了,有的入口忘记检查。

4. 遇到固定选项,用枚举

订单状态不适合用字符串,因为字符串太自由。

枚举的作用是限制可选值:

OrderStatus.Draft OrderStatus.Submitted OrderStatus.Paid OrderStatus.Cancelled

固定选项越明确,代码越不容易写出奇怪状态。

5. 遇到可替换能力,用接口或抽象类

例如通知方式可能有很多种:

通知方式实现
控制台通知ConsoleNotifier
邮件通知EmailNotifier
短信通知SmsNotifier

它们都能“发送通知”,但具体发送方式不同。

这种情况适合抽出接口:

INotifier

接口不关心怎么做,只规定必须能做什么。

6. 遇到变化规则,用委托和 Lambda

筛选订单时,流程是固定的:

  1. 遍历订单。
  2. 判断订单是否满足规则。
  3. 满足就放进结果。

变化的是规则:

需求规则
查已支付订单order.Status == OrderStatus.Paid
查大额订单order.TotalAmount >= 500
查某个客户订单order.Customer.Id == "C001"

所以可以把规则写成:

Func<Order, bool>

7. 遇到“发生后通知”,用事件

支付完成后可能要做很多事:

  1. 写日志。
  2. 发通知。
  3. 更新统计。
  4. 刷新界面。

订单服务不应该知道所有后续动作。

它只需要触发事件:

OrderPaid?.Invoke(order)

谁关心支付完成,谁自己订阅。

8. 类、接口、委托、事件怎么选择

遇到的问题优先考虑
有一个业务概念
对象内部数据不能随便改封装
固定几个选项枚举
多种实现可以互换接口
小段规则要传进去委托 / Lambda
某件事发生后通知外面事件
多个对象配合完成流程服务类

这张表比背语法更重要。写代码时先判断问题类型,再选择写法。

第三周常用写法总表

主题常用写法解决什么问题
class Product表达业务对象
构造函数public Product(...)创建时校验
封装private set防止外部乱改
对象集合List<Product>保存多个对象
继承class VipCustomer : Customer表达“是一种”
重写virtual / override子类改父类行为
组合Order 拥有 Customer表达“有一个”
接口IProductRepository替换不同实现
抽象类abstract class公共流程加子类差异
泛型结果Result<T>成功/失败和数据
委托Func<T, TResult>把规则当参数
Lambdax => x.Price > 0简短规则
事件event Action<T>发生后通知外部
标准事件EventHandler<TEventArgs>框架常见模式
枚举enum OrderStatus固定状态
只读列表IReadOnlyList<T>外部只能读取

9. 综合项目的验收标准

第三周代码不能只看“能不能运行”,还要看:

验收点怎么检查
正常流程创建订单、添加商品、提交、支付
错误流程空订单提交、未提交就支付、非法数量
可读性类名、方法名能不能表达业务
封装外部能不能绕过规则直接改状态
可替换通知方式能不能换成另一个类
可扩展新增筛选规则是否不用改原方法

第二部分:把知识组合成完整例子

前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天要把第三周的知识串起来,而不是只复习某一个语法。

先看效果:订单处理中心

OrderService orderService = new OrderService(); ConsoleNotifier notifier = new ConsoleNotifier(); orderService.OrderPaid += notifier.NotifyOrderPaid; Order order = new Order("SO-001", new Customer("C001", "张三")); order.AddLine(new Product("P001", "键盘", 199m), 2); order.AddLine(new Product("P002", "鼠标", 89m), 1); orderService.Submit(order); orderService.Pay(order); List<Order> orders = new List<Order> { order }; List<Order> paidOrders = OrderQuery.Find(orders, item => item.Status == OrderStatus.Paid); Console.WriteLine($"已支付订单数量: {paidOrders.Count}"); Console.WriteLine($"订单总金额: {order.TotalAmount}"); class Customer { public Customer(string id, string name) { Id = id; Name = name; } public string Id { get; } public string Name { get; } } class Product { public Product(string id, string name, decimal price) { if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Id = id; Name = name; Price = price; } public string Id { get; } public string Name { get; } public decimal Price { get; } } class OrderLine { public OrderLine(Product product, int quantity) { if (quantity <= 0) { throw new ArgumentException("数量必须大于 0"); } Product = product; Quantity = quantity; } public Product Product { get; } public int Quantity { get; } public decimal LineAmount => Product.Price * Quantity; } class Order { private readonly List<OrderLine> _lines = new List<OrderLine>(); public Order(string orderNo, Customer customer) { OrderNo = orderNo; Customer = customer; Status = OrderStatus.Draft; } public string OrderNo { get; } public Customer Customer { get; } public OrderStatus Status { get; private set; } public IReadOnlyList<OrderLine> Lines => _lines; public decimal TotalAmount => _lines.Sum(line => line.LineAmount); public void AddLine(Product product, int quantity) { if (Status != OrderStatus.Draft) { throw new InvalidOperationException("只有草稿订单可以添加商品"); } _lines.Add(new OrderLine(product, quantity)); } public void Submit() { if (_lines.Count == 0) { throw new InvalidOperationException("没有商品的订单不能提交"); } Status = OrderStatus.Submitted; } public void Pay() { if (Status != OrderStatus.Submitted) { throw new InvalidOperationException("只有已提交订单可以支付"); } Status = OrderStatus.Paid; } } class OrderService { public event Action<Order>? OrderPaid; public void Submit(Order order) { order.Submit(); Console.WriteLine($"订单已提交: {order.OrderNo}"); } public void Pay(Order order) { order.Pay(); Console.WriteLine($"订单已支付: {order.OrderNo}"); OrderPaid?.Invoke(order); } } class ConsoleNotifier { public void NotifyOrderPaid(Order order) { Console.WriteLine($"通知: 客户 {order.Customer.Name} 的订单 {order.OrderNo} 已支付"); } } static class OrderQuery { public static List<Order> Find(List<Order> orders, Func<Order, bool> rule) { List<Order> result = new List<Order>(); foreach (Order order in orders) { if (rule(order)) { result.Add(order); } } return result; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

这个例子把第三周的主线串起来了:

知识点在程序里的位置
类和对象CustomerProductOrder
封装private readonly List<OrderLine> _lines
构造函数校验ProductOrderLine
枚举OrderStatus
方法表达业务动作Submit()Pay()
委托和 LambdaFunc<Order, bool> rule
事件OrderPaid
服务类OrderService

第三部分:跟着敲代码

从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。

动手前先做这 3 件事

  1. 打开一个控制台项目。
  2. 每次只保留一个例子的代码,运行通过后再换下一个。
  3. 每个例子都要改一次数据再运行,例如改金额、状态、客户编号。

例子 1:类和封装复盘

Product product = new Product("P001", "键盘", 199m); Console.WriteLine(product.Name); Console.WriteLine(product.Price); class Product { public Product(string id, string name, decimal price) { if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Id = id; Name = name; Price = price; } public string Id { get; } public string Name { get; } public decimal Price { get; } }

这个例子检查两件事:

  1. 类把商品数据放在一起。
  2. 构造函数拦住非法价格。

例子 2:订单方法表达业务动作

Order order = new Order("SO-001"); Console.WriteLine(order.Status); order.Submit(); Console.WriteLine(order.Status); class Order { public Order(string orderNo) { OrderNo = orderNo; Status = OrderStatus.Draft; } public string OrderNo { get; } public OrderStatus Status { get; private set; } public void Submit() { if (Status != OrderStatus.Draft) { throw new InvalidOperationException("只有草稿订单可以提交"); } Status = OrderStatus.Submitted; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

不要让外部直接改 Status。应该通过 Submit() 这种业务方法改变状态。

例子 3:接口复盘

INotifier notifier = new ConsoleNotifier(); notifier.Send("订单已支付"); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) { Console.WriteLine($"控制台通知: {message}"); } }

接口让外部依赖“能力”,而不是依赖某个具体类。

例子 4:抽象类复盘

BaseNotifier notifier = new EmailNotifier(); notifier.Send("订单已支付"); abstract class BaseNotifier { public void Send(string message) { string finalMessage = $"[系统通知] {message}"; SendCore(finalMessage); } protected abstract void SendCore(string message); } class EmailNotifier : BaseNotifier { protected override void SendCore(string message) { Console.WriteLine($"邮件发送: {message}"); } }

抽象类适合“有共同流程,但某一步要交给子类实现”的场景。

例子 5:委托和 Lambda 复盘

List<int> scores = new List<int> { 45, 60, 88, 96 }; void PrintMatched(string title, List<int> source, Func<int, bool> rule) { Console.WriteLine(title); foreach (int item in source) { if (rule(item)) { Console.WriteLine(item); } } Console.WriteLine(); } PrintMatched("通过成绩", scores, score => score >= 60); PrintMatched("优秀成绩", scores, score => score >= 90);

PrintMatched 是固定流程,Lambda 是变化规则。

例子 6:事件复盘

PaymentService paymentService = new PaymentService(); paymentService.Paid += orderNo => Console.WriteLine($"日志: 订单 {orderNo} 已支付"); paymentService.Paid += orderNo => Console.WriteLine($"通知: 订单 {orderNo} 已支付"); paymentService.Pay("SO-001"); class PaymentService { public event Action<string>? Paid; public void Pay(string orderNo) { Console.WriteLine($"执行支付: {orderNo}"); Paid?.Invoke(orderNo); } }

事件适合“支付完成后,通知外部做后续动作”。

例子 7:订单查询工具

List<Order> orders = new List<Order> { new Order("SO-001", "C001", 199m, OrderStatus.Paid), new Order("SO-002", "C001", 89m, OrderStatus.Draft), new Order("SO-003", "C002", 1299m, OrderStatus.Paid) }; List<Order> paidOrders = OrderQuery.Find(orders, order => order.Status == OrderStatus.Paid); List<Order> largeOrders = OrderQuery.Find(orders, order => order.Amount >= 500m); List<Order> customerOrders = OrderQuery.Find(orders, order => order.CustomerId == "C001"); Console.WriteLine($"已支付订单: {paidOrders.Count}"); Console.WriteLine($"大额订单: {largeOrders.Count}"); Console.WriteLine($"C001 客户订单: {customerOrders.Count}"); class Order { public Order(string orderNo, string customerId, decimal amount, OrderStatus status) { OrderNo = orderNo; CustomerId = customerId; Amount = amount; Status = status; } public string OrderNo { get; } public string CustomerId { get; } public decimal Amount { get; } public OrderStatus Status { get; } } static class OrderQuery { public static List<Order> Find(List<Order> orders, Func<Order, bool> rule) { List<Order> result = new List<Order>(); foreach (Order order in orders) { if (rule(order)) { result.Add(order); } } return result; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

如果明天要新增“查未支付订单”,只需要传一个新 Lambda,不需要改 Find 方法。

例子 8:综合练习第一版

Order order = new Order("SO-001"); order.AddAmount(199m); order.AddAmount(89m); Console.WriteLine($"总金额: {order.TotalAmount}"); order.Submit(); order.Pay(); Console.WriteLine($"最终状态: {order.Status}"); class Order { private readonly List<decimal> _amounts = new List<decimal>(); public Order(string orderNo) { OrderNo = orderNo; Status = OrderStatus.Draft; } public string OrderNo { get; } public OrderStatus Status { get; private set; } public decimal TotalAmount => _amounts.Sum(); public void AddAmount(decimal amount) { if (amount <= 0) { throw new ArgumentException("金额必须大于 0"); } _amounts.Add(amount); } public void Submit() { if (_amounts.Count == 0) { throw new InvalidOperationException("没有金额不能提交"); } Status = OrderStatus.Submitted; } public void Pay() { if (Status != OrderStatus.Submitted) { throw new InvalidOperationException("只有已提交订单可以支付"); } Status = OrderStatus.Paid; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

这个版本先不追求完整商品模型,只练订单金额、状态和规则。

例子 9:综合练习第二版,加入事件

OrderService service = new OrderService(); service.OrderPaid += order => Console.WriteLine($"订单 {order.OrderNo} 支付成功,金额 {order.TotalAmount}"); Order order = new Order("SO-001"); order.AddAmount(199m); order.AddAmount(89m); service.Submit(order); service.Pay(order); class OrderService { public event Action<Order>? OrderPaid; public void Submit(Order order) { order.Submit(); } public void Pay(Order order) { order.Pay(); OrderPaid?.Invoke(order); } } class Order { private readonly List<decimal> _amounts = new List<decimal>(); public Order(string orderNo) { OrderNo = orderNo; Status = OrderStatus.Draft; } public string OrderNo { get; } public OrderStatus Status { get; private set; } public decimal TotalAmount => _amounts.Sum(); public void AddAmount(decimal amount) { if (amount <= 0) { throw new ArgumentException("金额必须大于 0"); } _amounts.Add(amount); } public void Submit() { if (_amounts.Count == 0) { throw new InvalidOperationException("没有金额不能提交"); } Status = OrderStatus.Submitted; } public void Pay() { if (Status != OrderStatus.Submitted) { throw new InvalidOperationException("只有已提交订单可以支付"); } Status = OrderStatus.Paid; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

加上事件以后,支付流程和支付后的通知就分开了。

常见错误和修法

错误为什么错修法
复盘只背概念不能证明会写代码用商品、订单、仓储重新写最小例子
类太多但职责不清项目结构看起来复杂,实际难维护每个类只回答“我负责什么”
接口没有使用场景为抽象而抽象需要替换实现或测试隔离时再抽接口
事件和委托混在一起不知道谁触发谁响应先画出触发者、事件、订阅者
不做综合练习知识点无法连接写一个订单域模型把类、接口、事件串起来

小白重复敲写训练

复盘日要从空白文件重新组合对象、接口和事件。

训练 1:类加方法

var product = new Product { Name = "Keyboard", Price = 100m }; Console.WriteLine(product.DiscountedPrice(0.8m)); class Product { public string Name { get; set; } = ""; public decimal Price { get; set; } public decimal DiscountedPrice(decimal discount) => Price * discount; }

训练 2:接口切换实现

INotifier notifier = new ConsoleNotifier(); notifier.Send("订单已创建"); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) => Console.WriteLine(message); }

改动任务:再写一个 UpperNotifier

训练 3:对象发出事件

var order = new Order(); order.Paid += () => Console.WriteLine("收到付款通知"); order.Pay(); class Order { public event Action? Paid; public void Pay() => Paid?.Invoke(); }

第三遍不看答案,独立写出三个部分:声明事件、订阅事件、触发事件。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:第三周复盘与综合练习。今天至少要掌握的 3 个点是:

1. 如何判断一段代码应该放在类、接口、服务还是事件里 2. 如何把类、封装、继承、接口、委托、Lambda、事件串成一条主线 3. 如何用一个小项目检查第三周是否真正掌握

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:如何判断一段代码应该放在类、接口、服务还是事件里。

上位机专项练习

本周过关标准是能用类保存设备、用接口隔离读取、用事件通知报警。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:对象保存设备状态

class Device { public string Name { get; } public bool IsOnline { get; private set; } public Device(string name) => Name = name; public void SetOnline(bool value) => IsOnline = value; } class Program { static void Main() { var device = new Device("PLC-01"); device.SetOnline(true); Console.WriteLine(device.IsOnline); } }

运行结果或界面效果:

True

改动任务: 增加 LastSeen 属性。

专项例子 2:接口隔离读取

interface IDeviceReader { double Read(); } class FakeReader : IDeviceReader { public double Read() => 25.6; } class Program { static void Main() { IDeviceReader reader = new FakeReader(); Console.WriteLine(reader.Read()); } }

运行结果或界面效果:

25.6

改动任务: 创建第二个读取实现。

专项例子 3:事件通知报警

class Monitor { public event Action<double>? Alarm; public void Check(double value) { if (value >= 80) Alarm?.Invoke(value); } } class Program { static void Main() { var monitor = new Monitor(); monitor.Alarm += value => Console.WriteLine($"报警: {value}"); monitor.Check(88); } }

运行结果或界面效果:

报警: 88

改动任务: 检查 70,确认没有报警。

第四部分:作业完整答案

这一部分给出当天综合作业的完整答案。建议先照着敲一遍,再按后面的验收点修改和测试。

作业 1:第三周概念判断

要求:把下面场景对应到合适的写法。

场景应该用什么
表达商品、客户、订单
限制订单状态只能是固定几个值枚举
商品价格不能小于等于 0构造函数校验
订单只能通过支付方法变成已支付private set + Pay()
通知方式可以换成邮件、短信、控制台接口
筛选订单的规则经常变委托 / Lambda
订单支付完成后通知外部事件

这道题的目标是形成判断,不是背名词。

作业 2:订单查询完整答案

要求:

  1. 准备 4 个订单。
  2. 写通用查询方法 Find
  3. 分别查已支付订单、大额订单、某个客户的订单。

完整答案

List<Order> orders = new List<Order> { new Order("SO-001", "C001", 199m, OrderStatus.Paid), new Order("SO-002", "C001", 89m, OrderStatus.Draft), new Order("SO-003", "C002", 1299m, OrderStatus.Paid), new Order("SO-004", "C003", 320m, OrderStatus.Submitted) }; void Print(string title, List<Order> result) { Console.WriteLine(title); foreach (Order order in result) { Console.WriteLine($"{order.OrderNo} | {order.CustomerId} | {order.Amount} | {order.Status}"); } Console.WriteLine(); } Print("已支付订单", OrderQuery.Find(orders, order => order.Status == OrderStatus.Paid)); Print("金额大于等于 300 的订单", OrderQuery.Find(orders, order => order.Amount >= 300m)); Print("客户 C001 的订单", OrderQuery.Find(orders, order => order.CustomerId == "C001")); class Order { public Order(string orderNo, string customerId, decimal amount, OrderStatus status) { OrderNo = orderNo; CustomerId = customerId; Amount = amount; Status = status; } public string OrderNo { get; } public string CustomerId { get; } public decimal Amount { get; } public OrderStatus Status { get; } } static class OrderQuery { public static List<Order> Find(List<Order> orders, Func<Order, bool> rule) { List<Order> result = new List<Order>(); foreach (Order order in orders) { if (rule(order)) { result.Add(order); } } return result; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

作业 3:通知接口完整答案

要求:

  1. 定义 INotifier
  2. ConsoleNotifier
  3. EmailNotifier
  4. 用同一个方法发送不同通知。

完整答案

List<INotifier> notifiers = new List<INotifier> { new ConsoleNotifier(), new EmailNotifier() }; void NotifyAll(List<INotifier> notifiers, string message) { foreach (INotifier notifier in notifiers) { notifier.Send(message); } } NotifyAll(notifiers, "订单 SO-001 已支付"); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) { Console.WriteLine($"控制台通知: {message}"); } } class EmailNotifier : INotifier { public void Send(string message) { Console.WriteLine($"邮件通知: {message}"); } }

作业 4:支付事件完整答案

要求:

  1. Order,包含订单号、金额、状态。
  2. OrderService,支付成功后触发事件。
  3. 外部订阅事件并输出通知。

完整答案

OrderService service = new OrderService(); service.OrderPaid += order => Console.WriteLine($"日志: {order.OrderNo} 已支付"); service.OrderPaid += order => Console.WriteLine($"通知: 金额 {order.Amount} 已到账"); Order order = new Order("SO-001", 268m); service.Submit(order); service.Pay(order); class Order { public Order(string orderNo, decimal amount) { if (string.IsNullOrWhiteSpace(orderNo)) { throw new ArgumentException("订单号不能为空"); } if (amount <= 0) { throw new ArgumentException("金额必须大于 0"); } OrderNo = orderNo; Amount = amount; Status = OrderStatus.Draft; } public string OrderNo { get; } public decimal Amount { get; } public OrderStatus Status { get; private set; } public void Submit() { if (Status != OrderStatus.Draft) { throw new InvalidOperationException("只有草稿订单可以提交"); } Status = OrderStatus.Submitted; } public void Pay() { if (Status != OrderStatus.Submitted) { throw new InvalidOperationException("只有已提交订单可以支付"); } Status = OrderStatus.Paid; } } class OrderService { public event Action<Order>? OrderPaid; public void Submit(Order order) { order.Submit(); Console.WriteLine($"订单已提交: {order.OrderNo}"); } public void Pay(Order order) { order.Pay(); Console.WriteLine($"订单已支付: {order.OrderNo}"); OrderPaid?.Invoke(order); } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

作业 5:第三周综合完整答案

要求:

  1. 创建客户、商品、订单。
  2. 订单可以添加商品明细。
  3. 订单能提交、支付。
  4. 支付成功后触发事件。
  5. 使用 Lambda 查询已支付订单。
  6. 通知逻辑通过接口实现。

完整答案

OrderService service = new OrderService(); INotifier notifier = new ConsoleNotifier(); service.OrderPaid += order => notifier.Send($"订单 {order.OrderNo} 已支付,总金额 {order.TotalAmount}"); Customer customer = new Customer("C001", "张三"); Order order = new Order("SO-001", customer); order.AddLine(new Product("P001", "键盘", 199m), 2); order.AddLine(new Product("P002", "鼠标", 89m), 1); service.Submit(order); service.Pay(order); List<Order> orders = new List<Order> { order }; List<Order> paidOrders = OrderQuery.Find(orders, item => item.Status == OrderStatus.Paid); Console.WriteLine($"已支付订单数量: {paidOrders.Count}"); class Customer { public Customer(string id, string name) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentException("客户编号不能为空"); } if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("客户名称不能为空"); } Id = id; Name = name; } public string Id { get; } public string Name { get; } } class Product { public Product(string id, string name, decimal price) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentException("商品编号不能为空"); } if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("商品名称不能为空"); } if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Id = id; Name = name; Price = price; } public string Id { get; } public string Name { get; } public decimal Price { get; } } class OrderLine { public OrderLine(Product product, int quantity) { if (quantity <= 0) { throw new ArgumentException("数量必须大于 0"); } Product = product; Quantity = quantity; } public Product Product { get; } public int Quantity { get; } public decimal LineAmount => Product.Price * Quantity; } class Order { private readonly List<OrderLine> _lines = new List<OrderLine>(); public Order(string orderNo, Customer customer) { if (string.IsNullOrWhiteSpace(orderNo)) { throw new ArgumentException("订单号不能为空"); } OrderNo = orderNo; Customer = customer; Status = OrderStatus.Draft; } public string OrderNo { get; } public Customer Customer { get; } public OrderStatus Status { get; private set; } public IReadOnlyList<OrderLine> Lines => _lines; public decimal TotalAmount => _lines.Sum(line => line.LineAmount); public void AddLine(Product product, int quantity) { if (Status != OrderStatus.Draft) { throw new InvalidOperationException("只有草稿订单可以添加商品"); } _lines.Add(new OrderLine(product, quantity)); } public void Submit() { if (Status != OrderStatus.Draft) { throw new InvalidOperationException("只有草稿订单可以提交"); } if (_lines.Count == 0) { throw new InvalidOperationException("没有商品的订单不能提交"); } Status = OrderStatus.Submitted; } public void Pay() { if (Status != OrderStatus.Submitted) { throw new InvalidOperationException("只有已提交订单可以支付"); } Status = OrderStatus.Paid; } } class OrderService { public event Action<Order>? OrderPaid; public void Submit(Order order) { order.Submit(); Console.WriteLine($"订单已提交: {order.OrderNo}"); } public void Pay(Order order) { order.Pay(); Console.WriteLine($"订单已支付: {order.OrderNo}"); OrderPaid?.Invoke(order); } } interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) { Console.WriteLine($"控制台通知: {message}"); } } static class OrderQuery { public static List<Order> Find(List<Order> orders, Func<Order, bool> rule) { List<Order> result = new List<Order>(); foreach (Order order in orders) { if (rule(order)) { result.Add(order); } } return result; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

综合验收

完成后按下面顺序检查:

  1. 把商品价格改成 0,确认程序能拦住。
  2. 把购买数量改成 0,确认程序能拦住。
  3. 不添加商品就提交订单,确认程序能拦住。
  4. 不提交订单就支付,确认程序能拦住。
  5. 再添加一个 EmailNotifier,确认不用改 OrderService
  6. 再写一个查询规则 item => item.TotalAmount >= 300m,确认不用改 OrderQuery.Find

本页最后要记住

  1. 类解决“数据和规则放哪里”的问题。
  2. 接口解决“能力可替换”的问题。
  3. 委托和 Lambda 解决“规则可传入”的问题。
  4. 事件解决“发生后通知”的问题。
  5. 服务类负责组织流程,但不要把所有业务规则都塞进服务类。
  6. 第三周真正要掌握的是判断方法:遇到什么问题,选择什么结构。