Day 7 - 第三周复盘与综合练习
建议用时:240-300 分钟
你将学会什么
- 如何判断一段代码应该放在类、接口、服务还是事件里
- 如何把类、封装、继承、接口、委托、Lambda、事件串成一条主线
- 如何用一个小项目检查第三周是否真正掌握
- 如何给自己的代码做验收:正常流程、错误流程、可修改性
复盘不是重新抄一遍笔记,而是把知识点变成判断方法:遇到业务名词时建类,遇到可替换能力时用接口,遇到变化规则时用委托,遇到发生后通知时用事件。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“小项目:订单域模型”,今天继续学习“第三周复盘与综合练习”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“第三周复盘与综合练习”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。目标是把第三周的知识点变成“遇到问题时知道该用什么”的判断方法。
1. 第三周的主线
第三周不是散点知识,它其实在解决同一个问题:
如何把代码从一堆语句,整理成可维护的小系统。可维护的小系统至少要做到:
- 数据有归属,不要到处散落。
- 规则有位置,不要到处复制。
- 可替换的部分不要写死。
- 发生后的通知不要强行绑在一起。
- 错误情况要被模型或服务拦住。
2. 遇到业务名词,用类
业务名词通常会变成类:
| 业务名词 | 类 |
|---|---|
| 客户 | Customer |
| 商品 | Product |
| 订单 | Order |
| 订单明细 | OrderLine |
| 通知器 | Notifier |
如果一个名词有多个属性,并且会在程序里反复出现,就不要一直用散变量表示它。
3. 遇到对象自己的规则,用方法和封装
订单自己的规则应该放在订单里。
例如:
| 规则 | 应该放哪里 |
|---|---|
| 没有商品不能提交 | Order.Submit() |
| 只有已提交订单可以支付 | Order.Pay() |
| 提交后不能添加商品 | Order.AddLine() |
| 数量必须大于 0 | OrderLine 构造函数 |
如果规则放在外面,很容易有的入口检查了,有的入口忘记检查。
4. 遇到固定选项,用枚举
订单状态不适合用字符串,因为字符串太自由。
枚举的作用是限制可选值:
OrderStatus.Draft
OrderStatus.Submitted
OrderStatus.Paid
OrderStatus.Cancelled固定选项越明确,代码越不容易写出奇怪状态。
5. 遇到可替换能力,用接口或抽象类
例如通知方式可能有很多种:
| 通知方式 | 实现 |
|---|---|
| 控制台通知 | ConsoleNotifier |
| 邮件通知 | EmailNotifier |
| 短信通知 | SmsNotifier |
它们都能“发送通知”,但具体发送方式不同。
这种情况适合抽出接口:
INotifier接口不关心怎么做,只规定必须能做什么。
6. 遇到变化规则,用委托和 Lambda
筛选订单时,流程是固定的:
- 遍历订单。
- 判断订单是否满足规则。
- 满足就放进结果。
变化的是规则:
| 需求 | 规则 |
|---|---|
| 查已支付订单 | order.Status == OrderStatus.Paid |
| 查大额订单 | order.TotalAmount >= 500 |
| 查某个客户订单 | order.Customer.Id == "C001" |
所以可以把规则写成:
Func<Order, bool>7. 遇到“发生后通知”,用事件
支付完成后可能要做很多事:
- 写日志。
- 发通知。
- 更新统计。
- 刷新界面。
订单服务不应该知道所有后续动作。
它只需要触发事件:
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> | 把规则当参数 |
| Lambda | x => 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
}这个例子把第三周的主线串起来了:
| 知识点 | 在程序里的位置 |
|---|---|
| 类和对象 | Customer、Product、Order |
| 封装 | private readonly List<OrderLine> _lines |
| 构造函数校验 | Product、OrderLine |
| 枚举 | OrderStatus |
| 方法表达业务动作 | Submit()、Pay() |
| 委托和 Lambda | Func<Order, bool> rule |
| 事件 | OrderPaid |
| 服务类 | OrderService |
第三部分:跟着敲代码
从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。
动手前先做这 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; }
}这个例子检查两件事:
- 类把商品数据放在一起。
- 构造函数拦住非法价格。
例子 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:订单查询完整答案
要求:
- 准备 4 个订单。
- 写通用查询方法
Find。 - 分别查已支付订单、大额订单、某个客户的订单。
完整答案
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:通知接口完整答案
要求:
- 定义
INotifier。 - 写
ConsoleNotifier。 - 写
EmailNotifier。 - 用同一个方法发送不同通知。
完整答案
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:支付事件完整答案
要求:
- 写
Order,包含订单号、金额、状态。 - 写
OrderService,支付成功后触发事件。 - 外部订阅事件并输出通知。
完整答案
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:第三周综合完整答案
要求:
- 创建客户、商品、订单。
- 订单可以添加商品明细。
- 订单能提交、支付。
- 支付成功后触发事件。
- 使用 Lambda 查询已支付订单。
- 通知逻辑通过接口实现。
完整答案
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
}综合验收
完成后按下面顺序检查:
- 把商品价格改成
0,确认程序能拦住。 - 把购买数量改成
0,确认程序能拦住。 - 不添加商品就提交订单,确认程序能拦住。
- 不提交订单就支付,确认程序能拦住。
- 再添加一个
EmailNotifier,确认不用改OrderService。 - 再写一个查询规则
item => item.TotalAmount >= 300m,确认不用改OrderQuery.Find。
本页最后要记住
- 类解决“数据和规则放哪里”的问题。
- 接口解决“能力可替换”的问题。
- 委托和 Lambda 解决“规则可传入”的问题。
- 事件解决“发生后通知”的问题。
- 服务类负责组织流程,但不要把所有业务规则都塞进服务类。
- 第三周真正要掌握的是判断方法:遇到什么问题,选择什么结构。