Chapter 3. Designing function signatures and types
覆盖function signature信息设计、data objects、Unit与Option,把absence、completion、failure和invalid state前移到type boundary。
学习目标
- 能分析function signature泄露的hidden input/output,设计domain value、Option与Either式return使outcome可见
- 能解释Unit与void的value/composition差异,判断何时成功但无业务数据仍需value-level表示
- 能设计Option pipeline和boundary adapter,区分None、external null、invalid input与fallback ownership
机制总览
Chapter 3. Designing function signatures and types:机制路径
- 1
为什么Signature是最便宜的Architecture
Function body可以写得很functional,但若signature接收primitive soup、读取global、通过out parameter或exception偷偷返回结果,caller仍无法组合和推理。Signature是每个consumer必经的architecture s…
- 2
Function signature design
先把function写成arrow: A - B ,再问是否真的只有A输入、B输出。Clock、configuration、repository若藏在object fields/global中也是inputs;mutation、logging、out/ref、exception是额外outputs/…
- 3
Capturing data with data obje…
Data object把相关values作为一个immutable snapshot传递,避免long parameter list和temporal mutation。它应表达construction invariant、value equality需求与serialization boundar…
章级决策实验
Chapter 3. Designing function signatures and types:机制与证据
切换《Chapter 3. Designing function signatures and types》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么Signature是最便宜的Architecture
Function body可以写得很functional,但若signature接收primitive soup、读取global、通过out parameter或exception偷偷返回结果,caller仍无法组合和推理。Signature是每个consumer必经的architecture s…
可核验证据
以确定输入重复运行「为什么Signature是最便宜的Architecture」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
学完《Chapter 3. Designing function signatures and types》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 3. Designing function signatures and types:失效与核验
为什么Signature是最便宜的Architecture
典型失效
若把「为什么Signature是最便宜的Architecture」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「为什么Signature是最便宜的Architecture」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Function signature design
典型失效
若把「Function signature design」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Function signature design」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Capturing data with data obje…
典型失效
若把「Capturing data with data obje…」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Capturing data with data obje…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
为什么Signature是最便宜的Architecture
Function body可以写得很functional,但若signature接收primitive soup、读取global、通过out parameter或exception偷偷返回结果,caller仍无法组合和推理。Signature是每个consumer必经的architecture surface:它决定哪些states能表示、哪些dependencies显式、哪些outcomes必须处理。
先预测:bool TryX(out T)是否和Option<T>表达完全相同;Unit是否等于null;Option是否适合所有failure;把string换成CustomerId是否只是多一个class;nullable reference是否自动替代Option。答案都是否。
Function signature design
先把function写成arrow:A -> B,再问是否真的只有A输入、B输出。Clock、configuration、repository若藏在object fields/global中也是inputs;mutation、logging、out/ref、exception是额外outputs/effects。把稳定dependency部分应用到function,可得到更小的domain operation。
Parameter order影响partial application:先放configuration/dependency,后放变化频繁的data,便于composition root固定前者。多个同type primitives易swap,应提升为validated domain value。Return type区分routine absence、expected validation failure与unexpected dependency fault。
public static Func<Order, Price> PriceWith(
TaxPolicy taxPolicy,
Currency currency) =>
order => Price.Calculate(order, taxPolicy, currency);切换bool/out、nullable、Option与Either
Capturing data with data objects
Data object把相关values作为一个immutable snapshot传递,避免long parameter list和temporal mutation。它应表达construction invariant、value equality需求与serialization boundary。第一版写作时没有C# 9 records,常用constructor + getter-only properties;modern records可减少ceremony,但需单独标版本。
不要用“data object”制造anemic bag后在多个functions重复validate。Boundary先parse/validate成domain value,core接收valid state。需要变化时返回copy/new value;若identity和lifecycle重要,则明确entity与value的差异。
Modern representation boundary
C# 9 records可自动提供value-oriented syntax/equality,但不会自动验证domain invariant,也不会决定serialization compatibility。第一版使用class/getter-only properties表达同一principle;modern替换必须保持constructor/factory validation、external schema和equality contract。
public sealed class Transfer
{
public AccountId From { get; }
public AccountId To { get; }
public Money Amount { get; }
public Transfer(AccountId from, AccountId to, Money amount) =>
(From, To, Amount) = amount.IsPositive
? (from, to, amount)
: throw new ArgumentOutOfRangeException(nameof(amount));
}Modeling the absence of data with Unit
Unit表示只有一个可能value的type。它不是absence,而是“计算成功完成,但没有额外业务value”。C# void不能作为generic type argument或普通value;Unit让Func<Unit>、Either<Error,Unit>等pipeline统一处理完成结果。
Unit不应替代meaningful result。保存订单若需要generated id/etag,应返回它;只在success本身就是全部information时用Unit。Implementation可用singleton/empty struct,重点是equality和single-value semantics。
↡只有一个有效value的type,用value表示successful no-data completion并参与generic composition。切换void、Unit、Action与Func-Unit
Modeling the possible absence of data with Option
Option有两个states:Some(value)和None。它适合routine absence,例如lookup没找到、optional middle name;caller通过Map/Bind/Match/IfNone显式处理,不直接deref null。Option让fallback推迟到最了解context的consumer,而不是repository提前返回empty object。
Option<Customer> customer = repository.Find(id);
string email = customer
.Map(value => value.Email)
.IfNone("missing@example.invalid");External null必须在adapter转换:允许absence则None,不允许则validation error。Some(null)是否允许要由Option implementation明确;通常禁止以避免双重absence。Option也不携带failure reason,validation/dependency error更适合Either/Result或exception boundary。
切换Some、None、external null与fallback
本章回顾:让Type保留Caller必须知道的信息
- Signature显式暴露inputs、outputs、absence与failure,是最早的architecture boundary。
- Domain data objects收敛validation并以immutable snapshot跨function传递。
- Unit表示successful no-data completion,不是null/absence。
- Option表示routine possible absence,fallback由consumer owner决定。
- Type复杂度必须对应真实domain states,不能为functional外观而堆generic wrapper。
练习
问题 1:Customer? Find(id)应何时改成Option?
问题 2:发送Email成功时返回Unit是否合理?
问题 3:怎样重构四个string参数避免swap?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- signature information surface
- domain value boundary
- data object snapshot
- unit completion value
- option absence contract
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
Capturing data with data objects:机制、边界与证据
Chapter 3. Designing function signatures and types中的Capturing data with data objects应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。
Modeling the absence of data with Unit:机制、边界与证据
Chapter 3. Designing function signatures and types中的Modeling the absence of data with Unit应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。