Day 6 - 小项目:订单域模型
建议用时:260-320 分钟
你将学会什么
- 如何从业务名词找出类:客户、商品、订单、订单明细
- 为什么业务规则不要散落在控制台输入、按钮事件或页面代码里
- 如何用属性、方法、构造函数、私有字段保护对象内部状态
- 如何计算订单金额、限制非法数量、限制非法状态变化
- 如何把前面学过的集合、异常、枚举、类、方法组合成一个小项目
今天不是学一个新语法,而是把前面几天的知识串起来。项目能力不是从大项目开始的,而是从一个能跑、规则清楚、边界清楚的小模型开始的。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“事件机制”,今天继续学习“小项目:订单域模型”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“小项目:订单域模型”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。今天的目标不是背概念,而是知道为什么要这样组织代码。
1. 什么是域模型
“域”就是业务范围。
如果你写的是订单系统,业务范围里会有:
| 业务名词 | 代码里可能变成 |
|---|---|
| 客户 | Customer |
| 商品 | Product |
| 订单 | Order |
| 订单明细 | OrderLine |
| 支付 | Pay() |
| 取消 | Cancel() |
| 订单状态 | OrderStatus |
域模型就是用类、属性、方法把这些业务概念表达出来。
一句话:
域模型 = 用代码表达业务里的名词、动作和规则2. 为什么不要只写一堆变量
如果不用类,订单可能写成这样:
string orderNo
string customerName
List<string> productNames
List<decimal> prices
List<int> quantities
string status问题很快就出现:
| 问题 | 后果 |
|---|---|
| 商品名、价格、数量分散在不同列表 | 很容易下标对不上 |
| 状态是字符串 | 写成 Payed、paid、已支付 都可能混进来 |
| 金额到处算 | 有的地方含折扣,有的地方不含折扣 |
| 谁都能改数据 | 已支付订单也可能被随便改 |
类的价值不是“看起来更正式”,而是把相关数据和规则放在一起。
3. 实体是什么
实体是有身份的对象。
例如两个客户都叫“张三”,但客户编号不同,它们就是两个不同客户。
Customer Id = C001, Name = 张三
Customer Id = C002, Name = 张三判断客户是不是同一个,主要看 Id,不是只看名字。
订单也是实体:
OrderNo = SO-001
OrderNo = SO-002订单号不同,就是不同订单。
4. 值对象是什么
值对象不靠身份区分,主要看值本身。
例如订单明细:
商品 + 数量如果商品和数量都一样,这一行表达的含义就一样。
这门课程里不需要把值对象讲得很深,只要先记住:
实体重身份,值对象重内容5. 业务规则是什么
业务规则就是业务允许什么、不允许什么。
订单里常见规则:
| 规则 | 代码里应该出现在哪里 |
|---|---|
| 商品价格必须大于 0 | Product 构造函数 |
| 购买数量必须大于 0 | OrderLine 构造函数 |
| 没有商品不能提交订单 | Order.Submit() |
| 只有已提交订单可以支付 | Order.Pay() |
| 已支付订单不能再添加商品 | Order.AddLine() |
如果规则不写进模型,就会散落在很多地方。以后改规则时,很难知道到底该改哪里。
6. 状态为什么要用枚举
订单状态如果用字符串:
"Draft"
"draft"
"草稿"
"DRAFT"这些写法都可能出现,程序很难控制。
用枚举以后:
OrderStatus.Draft
OrderStatus.Submitted
OrderStatus.Paid
OrderStatus.Cancelled状态只能从这几个固定值里选,写错时编译器会提醒。
7. 为什么要把 set 关起来
如果属性写成这样:
public OrderStatus Status { get; set; }外部代码就能随便改:
order.Status = OrderStatus.Paid这会绕过 Pay() 方法里的规则检查。
更好的写法是:
public OrderStatus Status { get; private set; }这样外部可以看状态,但不能直接改状态。状态变化必须通过 Submit()、Pay()、Cancel() 这类方法完成。
8. 为什么订单明细列表不要直接暴露
如果写成:
public List<OrderLine> Lines { get; set; }外部就可以绕开 AddLine():
order.Lines.Add(...)
order.Lines.Clear()这会破坏订单规则。
更好的写法是内部用 List,外部只读:
private readonly List<OrderLine> _lines = new List<OrderLine>();
public IReadOnlyList<OrderLine> Lines => _lines;外部能查看订单明细,但不能直接操作内部列表。
9. 模型方法应该表达业务动作
不要只写 SetStatus() 这种机械方法。
更好的方法名应该表达业务动作:
| 不够清晰 | 更清晰 |
|---|---|
SetStatus(OrderStatus.Paid) | Pay() |
SetStatus(OrderStatus.Cancelled) | Cancel() |
Add(...) | AddLine(...) |
Calc() | TotalAmount |
好的方法名能让代码像业务流程一样读出来。
10. 今天要形成的建模顺序
做一个小项目时,不要一开始就写很多类。
按这个顺序:
- 先列业务名词。
- 把最核心的名词变成类。
- 给类加必要属性。
- 把业务动作变成方法。
- 把不允许发生的情况写成校验。
- 用一组正常数据和一组错误数据验证。
域模型常用写法速查
| 需求 | 写法 | 说明 |
|---|---|---|
| 业务实体 | class Order | 有身份、有生命周期 |
| 值对象 | record Money(decimal Amount) | 只关心值 |
| 固定状态 | enum OrderStatus | 待支付、已支付、已取消 |
| 关闭外部 set | private set | 外部不能绕过规则 |
| 只读列表 | IReadOnlyList<T> | 外部只能看,不能直接改 |
| 内部列表 | private readonly List<T> | 类内部维护明细 |
| 业务动作 | Submit()、Pay()、Cancel() | 用方法表达状态变化 |
| 规则失败 | throw new InvalidOperationException(...) | 阻止错误状态 |
| 计算属性 | TotalAmount => ... | 根据明细计算 |
订单模型常用模板:
class Order
{
private readonly List<OrderLine> lines = new();
public IReadOnlyList<OrderLine> Lines => lines;
public OrderStatus Status { get; private set; } = OrderStatus.Draft;
public decimal TotalAmount => lines.Sum(line => line.Amount);
public void AddLine(OrderLine line)
{
if (Status != OrderStatus.Draft)
{
throw new InvalidOperationException("只有草稿订单可以添加明细");
}
lines.Add(line);
}
}第二部分:把知识组合成完整例子
这一部分先看结果,不要求马上理解每个类。今天学完后,要能写出一个能管理订单状态和金额的小模型。
先看效果:一个订单从创建到支付
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}");
foreach (OrderLine line in order.Lines)
{
Console.WriteLine($"{line.Product.Name} x {line.Quantity} = {line.LineAmount}");
}
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 (_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
}这个程序里有四个核心类:
| 类 | 负责什么 |
|---|---|
Customer | 表示客户 |
Product | 表示商品 |
OrderLine | 表示订单中的一行商品 |
Order | 表示订单整体和订单规则 |
最重要的是:金额计算、数量检查、状态限制都放在模型里,而不是散落在外面的代码里。
第三部分:跟着敲代码
从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。
动手前先做这 3 件事
- 打开一个控制台项目。
- 每次只保留一个例子的代码,运行通过后再换下一个。
- 每个例子运行后,改一组数据,比如价格、数量、状态,再运行观察结果。
例子 1:先定义商品
Product product = new Product("P001", "键盘", 199m);
Console.WriteLine($"商品编号: {product.Id}");
Console.WriteLine($"商品名称: {product.Name}");
Console.WriteLine($"商品价格: {product.Price}");
class Product
{
public 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; }
}这一步只做一件事:把商品相关的数据放进 Product 类。
例子 2:给商品加校验
try
{
Product product = new Product("P001", "键盘", -1m);
Console.WriteLine(product.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; }
}构造函数是保护对象入口的第一道门。无效商品不应该被创建出来。
例子 3:定义客户
Customer customer = new Customer("C001", "张三");
Console.WriteLine($"客户编号: {customer.Id}");
Console.WriteLine($"客户名称: {customer.Name}");
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; }
}客户和商品都属于业务名词,所以都应该有清晰的类。
例子 4:定义订单明细
Product product = new Product("P001", "键盘", 199m);
OrderLine line = new OrderLine(product, 2);
Console.WriteLine($"商品: {line.Product.Name}");
Console.WriteLine($"数量: {line.Quantity}");
Console.WriteLine($"小计: {line.LineAmount}");
class Product
{
public 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 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;
}OrderLine 不只是保存数量,它还知道这一行的小计怎么算。
例子 5:订单持有多条明细
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);
Console.WriteLine($"订单号: {order.OrderNo}");
Console.WriteLine($"客户: {order.Customer.Name}");
foreach (OrderLine line in order.Lines)
{
Console.WriteLine($"{line.Product.Name} x {line.Quantity} = {line.LineAmount}");
}
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)
{
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)
{
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 void AddLine(Product product, int quantity)
{
_lines.Add(new OrderLine(product, quantity));
}
}这里开始出现“整体和部分”的关系:
Order 包含多条 OrderLine例子 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);
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)
{
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)
{
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));
}
}总金额属于订单,所以放在 Order 里。外面不应该每次都自己重新算。
例子 7:用枚举表达订单状态
OrderStatus status = OrderStatus.Draft;
Console.WriteLine($"当前状态: {status}");
status = OrderStatus.Submitted;
Console.WriteLine($"提交后状态: {status}");
status = OrderStatus.Paid;
Console.WriteLine($"支付后状态: {status}");
enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}枚举让状态只能从固定选项里选,减少字符串写错造成的问题。
例子 8:订单状态只能由方法改变
Customer customer = new Customer("C001", "张三");
Order order = new Order("SO-001", customer);
Console.WriteLine($"初始状态: {order.Status}");
order.Submit();
Console.WriteLine($"提交后: {order.Status}");
order.Pay();
Console.WriteLine($"支付后: {order.Status}");
class Customer
{
public Customer(string id, string name)
{
Id = id;
Name = name;
}
public string Id { get; }
public string Name { get; }
}
class Order
{
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 void Submit()
{
Status = OrderStatus.Submitted;
}
public void Pay()
{
Status = OrderStatus.Paid;
}
}
enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}Status 是 private set,外部只能看,不能直接改。
例子 9:限制错误状态变化
Customer customer = new Customer("C001", "张三");
Order order = new Order("SO-001", customer);
try
{
order.Pay();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"支付失败: {ex.Message}");
}
order.Submit();
order.Pay();
Console.WriteLine($"最终状态: {order.Status}");
class Customer
{
public Customer(string id, string name)
{
Id = id;
Name = name;
}
public string Id { get; }
public string Name { get; }
}
class Order
{
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 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;
}
}
enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}这里故意先支付,程序会拦住错误操作。然后按正确顺序提交、支付。
例子 10:没有商品的订单不能提交
Customer customer = new Customer("C001", "张三");
Order order = new Order("SO-001", customer);
try
{
order.Submit();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"提交失败: {ex.Message}");
}
order.AddLine(new Product("P001", "键盘", 199m), 1);
order.Submit();
Console.WriteLine($"提交成功,状态: {order.Status}");
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)
{
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)
{
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 void AddLine(Product product, int quantity)
{
_lines.Add(new OrderLine(product, quantity));
}
public void Submit()
{
if (_lines.Count == 0)
{
throw new InvalidOperationException("没有商品的订单不能提交");
}
Status = OrderStatus.Submitted;
}
}
enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}规则应该写在动作里。提交订单时检查是否有商品,比外面到处检查更可靠。
例子 11:提交后不能继续添加商品
Customer customer = new Customer("C001", "张三");
Order order = new Order("SO-001", customer);
order.AddLine(new Product("P001", "键盘", 199m), 1);
order.Submit();
try
{
order.AddLine(new Product("P002", "鼠标", 89m), 1);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"添加失败: {ex.Message}");
}
Console.WriteLine($"订单状态: {order.Status}");
Console.WriteLine($"明细数量: {order.Lines.Count}");
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)
{
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)
{
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 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;
}
}
enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}这就是模型保护内部状态:外部想做不合法操作时,模型自己拦住。
例子 12:给订单增加取消规则
Customer customer = new Customer("C001", "张三");
Order order = new Order("SO-001", customer);
order.AddLine(new Product("P001", "键盘", 199m), 1);
order.Submit();
order.Cancel("客户不想买了");
Console.WriteLine($"订单状态: {order.Status}");
Console.WriteLine($"取消原因: {order.CancelReason}");
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)
{
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)
{
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 string CancelReason { get; private set; } = "";
public IReadOnlyList<OrderLine> Lines => _lines;
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 Cancel(string reason)
{
if (Status == OrderStatus.Paid)
{
throw new InvalidOperationException("已支付订单不能直接取消");
}
if (string.IsNullOrWhiteSpace(reason))
{
throw new ArgumentException("取消原因不能为空");
}
CancelReason = reason;
Status = OrderStatus.Cancelled;
}
}
enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}取消订单不只是改状态,还要记录原因。这就是“业务动作”比“直接改属性”更清晰的地方。
例子 13:最终完整订单模型
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);
void PrintOrder(Order order)
{
Console.WriteLine($"订单号: {order.OrderNo}");
Console.WriteLine($"客户: {order.Customer.Name}");
Console.WriteLine($"状态: {order.Status}");
Console.WriteLine($"总金额: {order.TotalAmount}");
foreach (OrderLine line in order.Lines)
{
Console.WriteLine($"{line.Product.Name} x {line.Quantity} = {line.LineAmount}");
}
Console.WriteLine();
}
PrintOrder(order);
order.Submit();
order.Pay();
PrintOrder(order);
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;
}
}
enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}这一版已经具备小项目的基本结构:
- 有业务名词对应的类。
- 有订单明细集合。
- 有金额计算。
- 有状态变化。
- 有业务规则校验。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 订单规则散落在外面 | 后期很难保证状态一致 | 把下单、支付、取消放进订单模型或服务 |
| 金额每次临时算 | 容易漏掉数量或折扣 | 封装 CalculateTotal() |
| 状态用普通字符串 | 拼错后不容易发现 | 用枚举表示订单状态 |
| 明细为空也允许提交 | 业务数据无意义 | 提交前检查至少有一条明细 |
| 修改状态没有限制 | 已取消订单可能继续支付 | 每个状态转换都写明确判断 |
小白重复敲写训练
订单模型先从“一条订单明细”开始,不要直接敲完整项目。
训练 1:计算一条明细小计
var line = new OrderLine
{
ProductName = "Keyboard",
Price = 100m,
Quantity = 2
};
Console.WriteLine(line.Subtotal());
class OrderLine
{
public string ProductName { get; set; } = "";
public decimal Price { get; set; }
public int Quantity { get; set; }
public decimal Subtotal() => Price * Quantity;
}改动任务:换成鼠标,单价 50,数量 3。
训练 2:订单包含多条明细
var lines = new List<OrderLine>
{
new() { Price = 100m, Quantity = 2 },
new() { Price = 50m, Quantity = 3 }
};
decimal total = lines.Sum(line => line.Price * line.Quantity);
Console.WriteLine(total);
class OrderLine
{
public decimal Price { get; set; }
public int Quantity { get; set; }
}第二遍再增加一条明细,先手算总额。
训练 3:把规则放进对象
var order = new Order { Status = "Created" };
order.Pay();
Console.WriteLine(order.Status);
class Order
{
public string Status { get; set; } = "";
public void Pay()
{
if (Status == "Created")
{
Status = "Paid";
}
}
}改动任务:重复调用两次 Pay(),确认状态不会乱跳。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:小项目:订单域模型。今天至少要掌握的 3 个点是:
1. 如何从业务名词找出类:客户、商品、订单、订单明细
2. 为什么业务规则不要散落在控制台输入、按钮事件或页面代码里
3. 如何用属性、方法、构造函数、私有字段保护对象内部状态答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:如何从业务名词找出类:客户、商品、订单、订单明细。
上位机专项练习
把设备、点位和采集记录组合成小型领域模型。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:设备包含多个点位
class Tag
{
public string Name { get; }
public Tag(string name) { Name = name; }
}
class Device
{
public string Name { get; }
public List<Tag> Tags { get; } = new();
public Device(string name) { Name = name; }
}
class Program
{
static void Main()
{
var device = new Device("PLC-01");
device.Tags.Add(new Tag("温度"));
Console.WriteLine($"{device.Name}: {device.Tags.Count} 个点位");
}
}运行结果或界面效果:
PLC-01: 1 个点位改动任务: 增加压力点位。
专项例子 2:采集记录保存时间和值
record Reading(string DeviceId, string TagName, double Value, DateTime Time);
class Program
{
static void Main()
{
var reading = new Reading("PLC-01", "温度", 25.6, DateTime.Now);
Console.WriteLine($"{reading.DeviceId}/{reading.TagName}: {reading.Value}");
}
}运行结果或界面效果:
PLC-01/温度: 25.6改动任务: 增加 Unit 字段。
专项例子 3:服务判断报警
record Reading(double Value);
class DeviceService
{
public bool IsAlarm(Reading reading, double limit) => reading.Value >= limit;
}
class Program
{
static void Main()
{
var service = new DeviceService();
Console.WriteLine(service.IsAlarm(new Reading(85), 80));
}
}运行结果或界面效果:
True改动任务: 测试 75 和 80。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。建议先照着敲一遍,再修改价格、数量、状态顺序,看规则是否生效。
作业 1:商品和客户模型
要求:
- 写
Product,包含编号、名称、价格。 - 写
Customer,包含编号、名称。 - 构造函数里检查空编号、空名称、非法价格。
完整答案
try
{
Product product = new Product("P001", "键盘", 199m);
Customer customer = new Customer("C001", "张三");
Console.WriteLine($"{customer.Name} 准备购买 {product.Name},单价 {product.Price}");
}
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; }
}
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; }
}作业 2:订单明细金额
要求:
- 写
OrderLine。 - 传入商品和数量。
- 数量必须大于 0。
LineAmount返回单价乘数量。
完整答案
Product product = new Product("P001", "键盘", 199m);
OrderLine line = new OrderLine(product, 3);
Console.WriteLine($"{line.Product.Name} x {line.Quantity}");
Console.WriteLine($"小计: {line.LineAmount}");
class Product
{
public 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 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;
}作业 3:订单总金额
要求:
- 写
Order。 - 内部保存多条订单明细。
- 外部通过
AddLine添加商品。 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);
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)
{
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;
}
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));
}
}作业 4:订单状态规则
要求:
- 订单初始状态是
Draft。 - 只有草稿订单可以提交。
- 只有已提交订单可以支付。
- 已支付订单不能直接取消。
完整答案
Customer customer = new Customer("C001", "张三");
Order order = new Order("SO-001", customer);
order.AddLine(new Product("P001", "键盘", 199m), 1);
Console.WriteLine($"初始状态: {order.Status}");
order.Submit();
Console.WriteLine($"提交后: {order.Status}");
order.Pay();
Console.WriteLine($"支付后: {order.Status}");
try
{
order.Cancel("不想买了");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"取消失败: {ex.Message}");
}
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)
{
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)
{
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 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;
}
public void Cancel(string reason)
{
if (Status == OrderStatus.Paid)
{
throw new InvalidOperationException("已支付订单不能直接取消");
}
if (string.IsNullOrWhiteSpace(reason))
{
throw new ArgumentException("取消原因不能为空");
}
Status = OrderStatus.Cancelled;
}
}
enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}作业 5:完整订单打印
要求:
- 创建客户。
- 创建订单。
- 添加两条商品明细。
- 打印订单号、客户、状态、每条明细和总金额。
- 提交并支付后再次打印。
完整答案
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);
void PrintOrder(Order order)
{
Console.WriteLine($"订单号: {order.OrderNo}");
Console.WriteLine($"客户: {order.Customer.Name}");
Console.WriteLine($"状态: {order.Status}");
foreach (OrderLine line in order.Lines)
{
Console.WriteLine($"{line.Product.Name} x {line.Quantity} = {line.LineAmount}");
}
Console.WriteLine($"总金额: {order.TotalAmount}");
Console.WriteLine();
}
PrintOrder(order);
order.Submit();
order.Pay();
PrintOrder(order);
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)
{
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)
{
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
}本页最后要记住
- 域模型不是一堆类名,而是业务名词、动作和规则的组合。
- 商品价格、购买数量、订单状态这些规则应该放进模型里。
private set可以阻止外部绕过业务方法直接改状态。- 内部
List加外部IReadOnlyList可以保护订单明细。 - 金额计算应该放在订单或订单明细里,而不是让外面到处算。
- 小项目先做最小闭环,再逐步补规则和边界。