测试驱动开发(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
相关产品推荐
相关产品推荐

