# 创建有效对象:把配置校验放在构造边界
学习目标:让成功构造的配置始终携带合法端口,并让调用方明确消费构造失败。
前置知识与范围:函数、字段、整数比较;只需把 Result 理解为成功或失败二选一,把异常理解为退出当前调用并由调用方捕获。本篇接收整数,不重复字符串解析。
# 问题与边界
配置只含一个端口:输入必须是整数,范围 1..65535,两端包含;-1、0、65536 都失败,不创建可供调用方使用的配置,也不改用默认端口。成功后只读访问端口,公开 API 不提供修改。Python 的动态入口额外拒绝布尔值、字符串和小数;这对应静态语言在函数调用处提供的类型门槛,而不是改变数值范围。
选择 Rust 展示模块私有字段和返回结果,Java 展示构造函数异常,Python 展示数据类生成构造函数中的校验钩子。这三者覆盖不同失败和封装模型;Go 的零值、TS 的结构兼容性需要另一套信任边界,不能仅把工厂函数命名为 new 就声称所有实例有效。Kotlin 可在 init 中校验,C++ 也可用构造异常,本篇不重复相同路径。
# 关键差异
| 实现 | 如何阻止绕过 | 失败信号 |
|---|---|---|
| Rust | 模块外无法写私有字段 | Result<Config, &str> |
| Java | 构造时检查,私有 final 字段 | IllegalArgumentException |
| Python | __post_init__ 校验,冻结常规赋值 |
TypeError / ValueError |
构造函数与工厂不是优劣关系。工厂适合显式结果或多个命名入口;构造函数适合“返回实例,否则抛异常”的约定。这里讨论正常公开 API,不把 Python 的冻结机制、反射或不安全代码视为安全沙箱。
# 各语言实现
各程序独立保存到各自目录,不互相拼接;默认只依赖标准库。正常结束、无标准输出表示程序内检查均未报告失败。这是预期行为,不是已经运行通过的声明。
# Rust
Rustmod config {
pub struct Config { port: u16 }
impl Config {
pub fn new(port: i32) -> Result<Self, &'static str> {
if !(1..=65535).contains(&port) { return Err("invalid port"); }
Ok(Self { port: port as u16 })
}
pub fn port(&self) -> u16 { self.port }
}
}
fn main() {
for input in [1, 8080, 65535] {
let c = config::Config::new(input).expect("valid fixture");
assert_eq!(i32::from(c.port()), input);
}
for input in [-1, 0, 65536] {
match config::Config::new(input) {
Ok(_) => panic!("invalid config escaped"),
Err(message) => assert_eq!(message, "invalid port"),
}
}
}
把类型放入子模块很重要:如果私有字段和调用方处于同一模块,调用方仍能直接构造。本例先检查范围再做窄化转换,因此不会靠 as 截断非法值。参见 Rust 可见性与 Result。
# Java
Javafinal class Config {
private final int port;
Config(int port) {
if (port < 1 || port > 65535) throw new IllegalArgumentException("invalid port");
this.port = port;
}
int port() { return port; }
}
public class Main {
public static void main(String[] args) {
for (int input : new int[]{1, 8080, 65535}) {
if (new Config(input).port() != input) throw new AssertionError("valid port");
}
for (int input : new int[]{-1, 0, 65536}) {
try {
new Config(input);
throw new AssertionError("invalid config escaped");
} catch (IllegalArgumentException e) {
if (!e.getMessage().equals("invalid port")) throw new AssertionError(e);
}
}
}
}
检查在字段赋值之前,构造函数不向外泄露 this。final 字段防止正常代码重新赋值;final class 还阻止子类通过重写访问器改变这里的含义。失败是非受检异常,不强制每个调用者写 catch;示例入口选择消费它。参见 JLS 构造函数。
# Python
Pythonfrom dataclasses import dataclass
@dataclass(frozen=True)
class Config:
port: int
def __post_init__(self):
if type(self.port) is not int:
raise TypeError("port must be an integer")
if not 1 <= self.port <= 65535:
raise ValueError("invalid port")
for value in (1, 8080, 65535):
assert Config(value).port == value
for value in (-1, 0, 65536):
try:
Config(value)
except ValueError as error:
assert str(error) == "invalid port"
else:
raise AssertionError("invalid config escaped")
for value in (True, "80", 80.5):
try:
Config(value)
except TypeError as error:
assert str(error) == "port must be an integer"
else:
raise AssertionError("wrong type escaped")
bool 是 int 的子类,因此本例有意使用精确类型检查而非 isinstance(value, int)。dataclass 不替你校验注解,但生成的初始化器会调用 __post_init__。冻结配置只能约束正常使用,不是防恶意调用的边界。参见 数据类初始化后处理和 布尔类型。
# 迁移误区与适用限制
只校验工厂、不隐藏另一条构造入口 → 调用方直接创建端口 0 → 应逐一审视所有公开构造与反序列化入口。Rust 示例以真正的模块边界演示这一点,而不是仅靠方法命名。
先创建“半有效对象”再要求调用方 validate() → 调用方可能提前使用对象 → 若无分阶段初始化需求,应在产生成功结果之前校验。
构造合法不等于网络可用:65535 满足本模型,仍可能被占用或无权监听;连接失败属于使用配置的 I/O 层,不应偷偷塞入这个纯构造器。
# 运行与验证
版本条件统一见示例基线,实际执行情况以验证记录为准。本篇编写阶段未执行示例,不能将静态阅读当成运行结果。每种语言使用独立目录,避免同名入口或编译输出互相覆盖;不需要网络、文件数据或第三方依赖。
程序内检查覆盖:端口下界、上界、普通值和上下越界;所有失败分支都拒绝使用无效对象;Python 还覆盖布尔、文本和小数。。Java 使用显式 AssertionError 而非默认关闭的 assert;Python 不要以 -O 运行,C++ 不要定义 NDEBUG,否则会移除相应断言。异常未被预期分支消费、断言失败或编译失败均不是成功。