Skip to Content
Week 03Day 6 - 小项目:订单域模型

Day 6 - 小项目:订单域模型

建议用时:260-320 分钟

你将学会什么

  • 如何从业务名词找出类:客户、商品、订单、订单明细
  • 为什么业务规则不要散落在控制台输入、按钮事件或页面代码里
  • 如何用属性、方法、构造函数、私有字段保护对象内部状态
  • 如何计算订单金额、限制非法数量、限制非法状态变化
  • 如何把前面学过的集合、异常、枚举、类、方法组合成一个小项目

今天不是学一个新语法,而是把前面几天的知识串起来。项目能力不是从大项目开始的,而是从一个能跑、规则清楚、边界清楚的小模型开始的。

本页固定顺序

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

学习衔接

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

今天的最低通过线

第一次学习不要求背完整页。完成下面 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

问题很快就出现:

问题后果
商品名、价格、数量分散在不同列表很容易下标对不上
状态是字符串写成 Payedpaid已支付 都可能混进来
金额到处算有的地方含折扣,有的地方不含折扣
谁都能改数据已支付订单也可能被随便改

类的价值不是“看起来更正式”,而是把相关数据和规则放在一起。

3. 实体是什么

实体是有身份的对象。

例如两个客户都叫“张三”,但客户编号不同,它们就是两个不同客户。

Customer Id = C001, Name = 张三 Customer Id = C002, Name = 张三

判断客户是不是同一个,主要看 Id,不是只看名字。

订单也是实体:

OrderNo = SO-001 OrderNo = SO-002

订单号不同,就是不同订单。

4. 值对象是什么

值对象不靠身份区分,主要看值本身。

例如订单明细:

商品 + 数量

如果商品和数量都一样,这一行表达的含义就一样。

这门课程里不需要把值对象讲得很深,只要先记住:

实体重身份,值对象重内容

5. 业务规则是什么

业务规则就是业务允许什么、不允许什么。

订单里常见规则:

规则代码里应该出现在哪里
商品价格必须大于 0Product 构造函数
购买数量必须大于 0OrderLine 构造函数
没有商品不能提交订单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. 今天要形成的建模顺序

做一个小项目时,不要一开始就写很多类。

按这个顺序:

  1. 先列业务名词。
  2. 把最核心的名词变成类。
  3. 给类加必要属性。
  4. 把业务动作变成方法。
  5. 把不允许发生的情况写成校验。
  6. 用一组正常数据和一组错误数据验证。

域模型常用写法速查

需求写法说明
业务实体class Order有身份、有生命周期
值对象record Money(decimal Amount)只关心值
固定状态enum OrderStatus待支付、已支付、已取消
关闭外部 setprivate 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. 打开一个控制台项目。
  2. 每次只保留一个例子的代码,运行通过后再换下一个。
  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 }

Statusprivate 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 }

这一版已经具备小项目的基本结构:

  1. 有业务名词对应的类。
  2. 有订单明细集合。
  3. 有金额计算。
  4. 有状态变化。
  5. 有业务规则校验。

常见错误和修法

错误为什么错修法
订单规则散落在外面后期很难保证状态一致把下单、支付、取消放进订单模型或服务
金额每次临时算容易漏掉数量或折扣封装 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:商品和客户模型

要求:

  1. Product,包含编号、名称、价格。
  2. Customer,包含编号、名称。
  3. 构造函数里检查空编号、空名称、非法价格。

完整答案

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:订单明细金额

要求:

  1. OrderLine
  2. 传入商品和数量。
  3. 数量必须大于 0。
  4. 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:订单总金额

要求:

  1. Order
  2. 内部保存多条订单明细。
  3. 外部通过 AddLine 添加商品。
  4. 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:订单状态规则

要求:

  1. 订单初始状态是 Draft
  2. 只有草稿订单可以提交。
  3. 只有已提交订单可以支付。
  4. 已支付订单不能直接取消。

完整答案

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:完整订单打印

要求:

  1. 创建客户。
  2. 创建订单。
  3. 添加两条商品明细。
  4. 打印订单号、客户、状态、每条明细和总金额。
  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 }

本页最后要记住

  1. 域模型不是一堆类名,而是业务名词、动作和规则的组合。
  2. 商品价格、购买数量、订单状态这些规则应该放进模型里。
  3. private set 可以阻止外部绕过业务方法直接改状态。
  4. 内部 List 加外部 IReadOnlyList 可以保护订单明细。
  5. 金额计算应该放在订单或订单明细里,而不是让外面到处算。
  6. 小项目先做最小闭环,再逐步补规则和边界。