Week 03 - 阶段检查
这一页用来确认第三周的内容是否已经连成能力。不要只看结论,要把下面的完整代码至少跑通一遍。
本周学习地图
| 阶段 | 先掌握什么 | 最终能写什么 |
|---|---|---|
| 建模 | 类、对象、属性、构造函数 | 能把商品、订单、客户写成类型 |
| 封装 | private set、方法保护规则 | 能防止外部绕过业务规则 |
| 关系 | 继承、组合 | 能区分“是一种”和“有一个” |
| 抽象 | 接口、抽象类、泛型 | 能让实现可替换、逻辑可复用 |
| 行为传递 | 委托、Lambda | 能把筛选规则传给方法 |
| 通知 | 事件 | 能在保存、支付、取消后通知外部 |
本周自测项目:订单域模型
从空项目开始写一个订单模型,至少包含:
Product、OrderLine、Order三个类。- 订单能添加明细、计算总金额。
- 空订单不能提交。
- 已取消订单不能支付。
- 订单状态变化时触发事件或输出记录。
过关标准:能解释哪些规则应该放在 Order 里,哪些规则应该放在服务类里。
上位机阶段验收:可替换通信的设备模型
本周目标: 用类、接口、组合、事件和委托建立上位机核心对象。
必须亲手完成:
- 建立 Device、Tag、Reading、Alarm 类
- 用 IDeviceReader 替换模拟读取器
- 采集值越限时用事件通知报警
过关标准: 你能先定义类型和方法再使用,并解释继承、组合、接口、事件的边界。
不要只看答案。新建一个空项目重做一次,运行成功后再故意改坏一处并自己排错。
本周流程图
业务名词
-> 类和属性
-> 构造函数保证初始状态
-> 方法保护业务规则
-> 接口隔离可替换能力
-> 委托传入变化规则
-> 事件通知外部
-> 域模型形成稳定边界第一部分:本周原理地图
第三周主线是:把代码从“散落的语句”整理成“有职责、有边界、能协作的小系统”。
| 知识点 | 解决的问题 | 典型写法 |
|---|---|---|
| 类和对象 | 业务数据和行为放在哪里 | 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(...)、virtual、override |
| 组合 | 一个类把另一个类作为属性,或者持有 List<T> |
| 接口 | interface、实现接口、构造函数依赖接口 |
| 抽象类 | abstract class、抽象方法、普通公共流程 |
| 委托 | Func、Action、Predicate |
| Lambda | x => ...、多行 Lambda、闭包 |
| 事件 | event、+=、-=、?.Invoke |
| 标准事件 | EventHandler<TEventArgs> |
| 订单模型 | enum、IReadOnlyList<T>、业务方法、状态校验 |
第二部分:阶段检查题
下面每一道都给完整答案。你要做的不是背代码,而是确认自己知道这段代码为什么这样分层。
检查 1:类、对象和构造函数校验
你要会什么:
- 把商品建成类。
- 用构造函数保证非法商品不能被创建。
- 用只读属性保护对象创建后的基本信息。
完整答案
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; }
}验收标准:
- 正常商品能输出。
- 价格为
0时能被拦住。 - 能解释为什么校验放在构造函数里。
检查 2:继承和组合怎么区分
你要会什么:
- “是一种”适合继承。
- “有一个”适合组合。
- 不要为了复用几行代码强行继承。
完整答案
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));
}
}验收标准:
PhysicalProduct和DigitalProduct是商品的一种,所以用继承。Order拥有多条OrderLine,所以用组合。- 能说出为什么
Order不应该继承Product。
检查 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}");
}
}验收标准:
NotifyAll这类代码可以依赖INotifier,不依赖具体通知类。- 新增短信通知时,不需要改已有通知器。
[系统通知]这个共同格式只写在抽象类里。
检查 4:委托和 Lambda
你要会什么:
- 固定流程写成方法。
- 变化规则用
Func<T, bool>传进去。 - 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
}验收标准:
Find方法不需要知道具体查询条件。- 新增查询规则时,只加新的 Lambda。
- 能解释
Func<Order, bool>表示输入订单、返回真假。
检查 5:事件机制
你要会什么:
- 发布者声明事件。
- 订阅者用
+=注册处理逻辑。 - 发布者在事情完成后触发事件。
完整答案
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);
}
}验收标准:
- 能说出
PaymentService是发布者。 - 能说出两个 Lambda 是订阅者处理逻辑。
- 能解释为什么触发事件要写
Paid?.Invoke(orderNo)。
检查 6:订单域模型
你要会什么:
- 订单内部持有明细集合。
- 明细金额由单价乘数量得到。
- 订单总金额由所有明细相加得到。
- 状态变化必须经过方法。
完整答案
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
}验收标准:
- 把数量改成
0,程序能拦住。 - 不添加商品就提交,程序能拦住。
- 不提交就支付,程序能拦住。
- 外部不能直接写
order.Status = OrderStatus.Paid。
检查 7:反射基础
你要会什么:
- 反射可以在运行时读取类型信息。
- 不直接访问属性,也能知道一个类有哪些属性。
- 很多框架会用反射自动发现类、方法、属性。
完整答案
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; }
}验收标准:
- 能输出
Name、Price、Stock。 - 能解释
typeof(Product)表示拿到Product类型信息。 - 能解释
GetProperties()表示读取这个类型的公开属性列表。
检查 8:特性基础
你要会什么:
- 特性是贴在类、属性、方法上的元数据。
- 特性本身不会自动执行业务逻辑。
- 框架通常通过反射读取特性,再决定怎么处理。
完整答案
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; }
}验收标准:
- 能输出
Name -> CSV 列名: product_name。 - 能输出
Price -> CSV 列名: product_price。 Stock没有贴特性,所以不会输出。- 能解释特性是元数据,反射负责读取元数据。
第三部分:综合作业完整答案
这道综合题把第三周内容串起来。
要求:
- 创建客户、商品、订单。
- 订单可以添加明细、提交、支付。
- 支付完成后触发事件。
- 通知逻辑通过接口实现。
- 使用 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。
第四部分:进入下一周前的最低通过线
进入下一周前,至少做到:
- 能解释类、接口、委托、事件分别解决什么问题。
- 能独立写出一个带构造函数校验的类。
- 能写一个接口和两个实现类。
- 能写一个接收
Func<T, bool>的查询方法。 - 能写一个事件,并在外部订阅它。
- 能解释
typeof(Product).GetProperties()在做什么。 - 能解释特性为什么需要配合反射读取。
- 能把订单模型从创建跑到支付。
如果上面 8 条能做到,第三周的基础就够进入下一阶段。后面做 Web 或桌面端时,这些结构会反复出现。