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

Week 03 - 阶段检查

这一页用来确认第三周的内容是否已经连成能力。不要只看结论,要把下面的完整代码至少跑通一遍。

本周学习地图

阶段先掌握什么最终能写什么
建模类、对象、属性、构造函数能把商品、订单、客户写成类型
封装private set、方法保护规则能防止外部绕过业务规则
关系继承、组合能区分“是一种”和“有一个”
抽象接口、抽象类、泛型能让实现可替换、逻辑可复用
行为传递委托、Lambda能把筛选规则传给方法
通知事件能在保存、支付、取消后通知外部

本周自测项目:订单域模型

从空项目开始写一个订单模型,至少包含:

  • ProductOrderLineOrder 三个类。
  • 订单能添加明细、计算总金额。
  • 空订单不能提交。
  • 已取消订单不能支付。
  • 订单状态变化时触发事件或输出记录。

过关标准:能解释哪些规则应该放在 Order 里,哪些规则应该放在服务类里。

上位机阶段验收:可替换通信的设备模型

本周目标: 用类、接口、组合、事件和委托建立上位机核心对象。

必须亲手完成:

  1. 建立 Device、Tag、Reading、Alarm 类
  2. 用 IDeviceReader 替换模拟读取器
  3. 采集值越限时用事件通知报警

过关标准: 你能先定义类型和方法再使用,并解释继承、组合、接口、事件的边界。

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

本周流程图

业务名词 -> 类和属性 -> 构造函数保证初始状态 -> 方法保护业务规则 -> 接口隔离可替换能力 -> 委托传入变化规则 -> 事件通知外部 -> 域模型形成稳定边界

第一部分:本周原理地图

第三周主线是:把代码从“散落的语句”整理成“有职责、有边界、能协作的小系统”。

知识点解决的问题典型写法
类和对象业务数据和行为放在哪里class Product
封装不让外部绕过规则乱改数据private set、私有字段
继承表达“是一种”的关系class Dog : Animal
组合表达“有一个”的关系Order 里有 List<OrderLine>
接口规定能力,让实现可替换interface INotifier
抽象类固定共同流程,留出变化步骤abstract class BaseNotifier
委托和 Lambda把变化规则传给固定流程Func<Order, bool>
事件某件事发生后通知外部event Action<Order>
反射运行时读取类型信息typeof(Product).GetProperties()
特性给代码贴元数据,供框架读取[CsvColumn("name")]

本周必须形成的判断方法

遇到的问题先考虑什么
有业务名词,例如商品、客户、订单
属性不能被外面随便改封装
几个固定状态枚举
多种实现可以互换接口
有共同流程,又有不同细节抽象类
规则经常变化,但流程固定委托 / Lambda
发生后要通知多个地方事件
框架需要自动发现类、属性、方法反射
需要给类、属性、方法补充说明特性

第三周必须会用的写法

主题必须会的写法
类和对象class、构造函数、属性、方法、private set
对象列表List<Product>foreach、LINQ 查询对象
继承: base(...)virtualoverride
组合一个类把另一个类作为属性,或者持有 List<T>
接口interface、实现接口、构造函数依赖接口
抽象类abstract class、抽象方法、普通公共流程
委托FuncActionPredicate
Lambdax => ...、多行 Lambda、闭包
事件event+=-=?.Invoke
标准事件EventHandler<TEventArgs>
订单模型enumIReadOnlyList<T>、业务方法、状态校验

第二部分:阶段检查题

下面每一道都给完整答案。你要做的不是背代码,而是确认自己知道这段代码为什么这样分层。

检查 1:类、对象和构造函数校验

你要会什么:

  1. 把商品建成类。
  2. 用构造函数保证非法商品不能被创建。
  3. 用只读属性保护对象创建后的基本信息。

完整答案

try { Product product = new Product("P001", "键盘", 199m); Console.WriteLine($"{product.Id} | {product.Name} | {product.Price}"); Product invalid = new Product("P002", "鼠标", 0m); Console.WriteLine(invalid.Name); } catch (ArgumentException ex) { Console.WriteLine($"创建失败: {ex.Message}"); } 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; } }

验收标准:

  1. 正常商品能输出。
  2. 价格为 0 时能被拦住。
  3. 能解释为什么校验放在构造函数里。

检查 2:继承和组合怎么区分

你要会什么:

  1. “是一种”适合继承。
  2. “有一个”适合组合。
  3. 不要为了复用几行代码强行继承。

完整答案

Order order = new Order("SO-001", new Customer("C001", "张三")); order.AddLine(new PhysicalProduct("P001", "键盘", 199m, 0.8m), 2); order.AddLine(new DigitalProduct("P002", "课程视频", 99m, "https://example.com/course"), 1); Console.WriteLine($"订单号: {order.OrderNo}"); Console.WriteLine($"客户: {order.Customer.Name}"); Console.WriteLine($"总金额: {order.TotalAmount}"); foreach (OrderLine line in order.Lines) { Console.WriteLine($"{line.Product.Name} x {line.Quantity} = {line.LineAmount}"); } abstract class Product { protected Product(string id, string name, decimal price) { Id = id; Name = name; Price = price; } public string Id { get; } public string Name { get; } public decimal Price { get; } } class PhysicalProduct : Product { public PhysicalProduct(string id, string name, decimal price, decimal weight) : base(id, name, price) { Weight = weight; } public decimal Weight { get; } } class DigitalProduct : Product { public DigitalProduct(string id, string name, decimal price, string downloadUrl) : base(id, name, price) { DownloadUrl = downloadUrl; } public string DownloadUrl { get; } } class Customer { public Customer(string id, string name) { Id = id; Name = name; } public string Id { get; } public string Name { get; } } class OrderLine { public OrderLine(Product product, int quantity) { 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; } public string OrderNo { get; } public Customer Customer { get; } public IReadOnlyList<OrderLine> Lines => _lines; public decimal TotalAmount => _lines.Sum(line => line.LineAmount); public void AddLine(Product product, int quantity) { _lines.Add(new OrderLine(product, quantity)); } }

验收标准:

  1. PhysicalProductDigitalProduct 是商品的一种,所以用继承。
  2. Order 拥有多条 OrderLine,所以用组合。
  3. 能说出为什么 Order 不应该继承 Product

检查 3:接口和抽象类

你要会什么:

  1. 接口规定能力。
  2. 多个通知器可以互相替换。
  3. 抽象类可以固定共同流程。

完整答案

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

验收标准:

  1. NotifyAll 这类代码可以依赖 INotifier,不依赖具体通知类。
  2. 新增短信通知时,不需要改已有通知器。
  3. [系统通知] 这个共同格式只写在抽象类里。

检查 4:委托和 Lambda

你要会什么:

  1. 固定流程写成方法。
  2. 变化规则用 Func<T, bool> 传进去。
  3. Lambda 是临时写出来的小规则。

完整答案

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) }; Print("已支付订单", Find(orders, order => order.Status == OrderStatus.Paid)); Print("大额订单", Find(orders, order => order.Amount >= 500m)); Print("客户 C001 的订单", Find(orders, order => order.CustomerId == "C001")); List<Order> Find(List<Order> source, Func<Order, bool> rule) { List<Order> result = new List<Order>(); foreach (Order order in source) { if (rule(order)) { result.Add(order); } } return result; } void Print(string title, List<Order> orders) { Console.WriteLine(title); foreach (Order order in orders) { Console.WriteLine($"{order.OrderNo} | {order.CustomerId} | {order.Amount} | {order.Status}"); } Console.WriteLine(); } 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; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

验收标准:

  1. Find 方法不需要知道具体查询条件。
  2. 新增查询规则时,只加新的 Lambda。
  3. 能解释 Func<Order, bool> 表示输入订单、返回真假。

检查 5:事件机制

你要会什么:

  1. 发布者声明事件。
  2. 订阅者用 += 注册处理逻辑。
  3. 发布者在事情完成后触发事件。

完整答案

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

验收标准:

  1. 能说出 PaymentService 是发布者。
  2. 能说出两个 Lambda 是订阅者处理逻辑。
  3. 能解释为什么触发事件要写 Paid?.Invoke(orderNo)

检查 6:订单域模型

你要会什么:

  1. 订单内部持有明细集合。
  2. 明细金额由单价乘数量得到。
  3. 订单总金额由所有明细相加得到。
  4. 状态变化必须经过方法。

完整答案

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); order.Submit(); order.Pay(); Console.WriteLine($"订单号: {order.OrderNo}"); Console.WriteLine($"客户: {order.Customer.Name}"); Console.WriteLine($"状态: {order.Status}"); 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; } } enum OrderStatus { Draft, Submitted, Paid, Cancelled }

验收标准:

  1. 把数量改成 0,程序能拦住。
  2. 不添加商品就提交,程序能拦住。
  3. 不提交就支付,程序能拦住。
  4. 外部不能直接写 order.Status = OrderStatus.Paid

检查 7:反射基础

你要会什么:

  1. 反射可以在运行时读取类型信息。
  2. 不直接访问属性,也能知道一个类有哪些属性。
  3. 很多框架会用反射自动发现类、方法、属性。

完整答案

using System.Reflection; Product product = new Product { Name = "键盘", Price = 199m, Stock = 8 }; PropertyInfo[] properties = typeof(Product).GetProperties(); foreach (PropertyInfo property in properties) { object? value = property.GetValue(product); Console.WriteLine($"{property.Name} = {value}"); } class Product { public string Name { get; set; } = ""; public decimal Price { get; set; } public int Stock { get; set; } }

验收标准:

  1. 能输出 NamePriceStock
  2. 能解释 typeof(Product) 表示拿到 Product 类型信息。
  3. 能解释 GetProperties() 表示读取这个类型的公开属性列表。

检查 8:特性基础

你要会什么:

  1. 特性是贴在类、属性、方法上的元数据。
  2. 特性本身不会自动执行业务逻辑。
  3. 框架通常通过反射读取特性,再决定怎么处理。

完整答案

using System.Reflection; foreach (PropertyInfo property in typeof(Product).GetProperties()) { CsvColumnAttribute? attribute = property.GetCustomAttribute<CsvColumnAttribute>(); if (attribute is not null) { Console.WriteLine($"{property.Name} -> CSV 列名: {attribute.Name}"); } } class Product { [CsvColumn("product_name")] public string Name { get; set; } = ""; [CsvColumn("product_price")] public decimal Price { get; set; } public int Stock { get; set; } } [AttributeUsage(AttributeTargets.Property)] class CsvColumnAttribute : Attribute { public CsvColumnAttribute(string name) { Name = name; } public string Name { get; } }

验收标准:

  1. 能输出 Name -> CSV 列名: product_name
  2. 能输出 Price -> CSV 列名: product_price
  3. Stock 没有贴特性,所以不会输出。
  4. 能解释特性是元数据,反射负责读取元数据。

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

这道综合题把第三周内容串起来。

要求:

  1. 创建客户、商品、订单。
  2. 订单可以添加明细、提交、支付。
  3. 支付完成后触发事件。
  4. 通知逻辑通过接口实现。
  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 }

综合验收标准

完成后必须逐条检查:

  1. 正常创建订单、添加商品、提交、支付能跑通。
  2. 商品价格改成 0,程序能拦住。
  3. 购买数量改成 0,程序能拦住。
  4. 不添加商品就提交,程序能拦住。
  5. 不提交就支付,程序能拦住。
  6. 新增 EmailNotifier 时,不需要改 OrderService
  7. 新增查询条件 item => item.TotalAmount >= 300m 时,不需要改 OrderQuery.Find

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

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

  1. 能解释类、接口、委托、事件分别解决什么问题。
  2. 能独立写出一个带构造函数校验的类。
  3. 能写一个接口和两个实现类。
  4. 能写一个接收 Func<T, bool> 的查询方法。
  5. 能写一个事件,并在外部订阅它。
  6. 能解释 typeof(Product).GetProperties() 在做什么。
  7. 能解释特性为什么需要配合反射读取。
  8. 能把订单模型从创建跑到支付。

如果上面 8 条能做到,第三周的基础就够进入下一阶段。后面做 Web 或桌面端时,这些结构会反复出现。