AI Library · 知识库

# 创建有效对象:把配置校验放在构造边界

学习目标:让成功构造的配置始终携带合法端口,并让调用方明确消费构造失败。

前置知识与范围:函数、字段、整数比较;只需把 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,否则会移除相应断言。异常未被预期分支消费、断言失败或编译失败均不是成功。

# 相关阅读

定义数据类型 · 字符串转端口号 · 方法与封装