You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

测试驱动开发(TDD)中类的输入验证时机选择问题

推荐方案

方案1:构造函数注入校验器(无静态,完全符合你的要求)

这是最贴合你需求的实现,不需要静态方法,不会漏校验,校验逻辑可单独测试:

interface ValidatorInterface
{
    public function validate(mixed $value) : bool;
}

class MachineIdValidator implements ValidatorInterface
{
    public function validate($uid) : bool
    {
        // 你的正则校验逻辑
        return preg_match('/^[a-f0-9]{32}$/', $uid) === 1;
    }
}

class MachineId
{
    private string $uid;
    
    // 给校验器加默认参数,日常使用不需要手动传
    public function __construct(string $uid, ValidatorInterface $validator = new MachineIdValidator())
    {
        if(!$validator->validate($uid)) {
            throw new \InvalidArgumentException('非法的MachineId格式');
        }
        $this->uid = $uid;
    }

    // 其他业务方法
    public function getUid(): string
    {
        return $this->uid;
    }
}

// 日常使用方式,和你期望的完全一致,自动完成校验
$machineId = new MachineId($uid);

这个方案的优势:

  • 完全不用静态方法
  • 不会遗漏校验:构造函数自动执行,只要创建出来的实例一定是合法的,不存在中间无效状态
  • 校验逻辑可测:单独的MachineIdValidator类可以直接写单元测试,不需要反射等黑科技
  • 扩展性强:后续如果校验规则变化,只需要替换校验器实现即可,不需要修改MachineId的核心逻辑

方案2:构造函数内置私有校验+反射测试(代码更内聚)

如果你不想额外新增校验类,也可以直接把校验逻辑放在MachineId类的私有方法中,用反射测试私有方法即可,主流PHP单元测试框架(比如PHPUnit)都原生支持这种操作:

class MachineId
{
    private string $uid;
    
    public function __construct(string $uid)
    {
        $this->uid = $uid;
        if(!$this->validate()) {
            throw new \InvalidArgumentException('非法的MachineId格式');
        }
    }
    
    private function validate() : bool
    {
        return preg_match('/^[a-f0-9]{32}$/', $this->uid) === 1;
    }
}

私有方法的单元测试示例(PHPUnit):

public function testValidate()
{
    $reflection = new ReflectionClass(MachineId::class);
    $validateMethod = $reflection->getMethod('validate');
    $validateMethod->setAccessible(true);

    // 测试合法值
    $validInstance = new MachineId('合法的uid');
    $this->assertTrue($validateMethod->invoke($validInstance));

    // 测试非法值,构造时已经抛异常,可以单独传参数测试
    $invalidInstance = $reflection->newInstanceWithoutConstructor();
    $reflection->getProperty('uid')->setValue($invalidInstance, '非法的uid');
    $this->assertFalse($validateMethod->invoke($invalidInstance));
}

可接受的备选方案:私有构造+静态创建器

如果可以接受少量静态方法,这个方案代码更简洁,不需要额外依赖:

class MachineId
{
    private string $uid;

    // 构造函数私有,禁止外部直接实例化
    private function __construct(string $uid)
    {
        $this->uid = $uid;
    }

    // 对外唯一的实例创建入口
    public static function create(string $uid): self
    {
        if (!self::validate($uid)) {
            throw new \InvalidArgumentException('非法的MachineId格式');
        }
        return new self($uid);
    }

    // 公共静态校验方法,可直接测试
    public static function validate(string $uid): bool
    {
        return preg_match('/^[a-f0-9]{32}$/', $uid) === 1;
    }
}

// 使用方式
$machineId = MachineId::create($uid);

不推荐的方案

  • 实例化后校验、先创建空实例再调用setUid:都存在校验逻辑被遗漏的风险,会导致非法对象在系统中流转,排查问题成本极高。
  • 要求业务代码实例化前手动调用校验:校验逻辑和对象创建解耦,同样容易被遗漏,不符合封装原则。

内容的提问来源于stack exchange,提问作者Frank Mohaupt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 20:57:03