PHP代码契约与防御式编程
PHP代码契约与防御式编程防御式编程是一种编程习惯假设外部输入都是不可信的所有函数调用都可能失败。代码契约则通过前置条件、后置条件和不变式来定义函数的约定。今天说说PHP中的防御式编程实践。前置条件确保函数调用时参数符合要求。phpdeclare(strict_types1);class Precondition{public static function notNull(mixed $value, string $message 值不能为空): void{if ($value null) {throw new PreconditionException($message);}}public static function notEmpty(mixed $value, string $message 值不能为空): void{if (empty($value)) {throw new PreconditionException($message);}}public static function isInt(mixed $value, string $message 必须是整数): void{if (!is_int($value)) {throw new PreconditionException($message);}}public static function isString(mixed $value, string $message 必须是字符串): void{if (!is_string($value)) {throw new PreconditionException($message);}}public static function inRange(int $value, int $min, int $max, string $message ): void{if ($value $min || $value $max) {throw new PreconditionException($message ?: 值必须在{$min}和{$max}之间);}}public static function matches(string $value, string $pattern, string $message ): void{if (!preg_match($pattern, $value)) {throw new PreconditionException($message ?: 值不匹配模式);}}public static function isEmail(string $value): void{if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {throw new PreconditionException(邮箱格式不正确);}}}class PreconditionException extends InvalidArgumentException {}class UserRegistrationService{public function register(string $name, string $email, int $age): array{Precondition::notEmpty($name, 名称不能为空);Precondition::isEmail($email);Precondition::inRange($age, 1, 150, 年龄必须在1-150之间);return [name $name, email $email, age $age];}}$service new UserRegistrationService();try {$user $service-register(张三, invalid-email, 28);print_r($user);} catch (PreconditionException $e) {echo 参数错误: {$e-getMessage()}\n;}?后置条件确保函数返回的结果符合预期。phpclass Postcondition{public static function check(mixed $result, callable $condition, string $message 后置条件不满足): void{if (!$condition($result)) {throw new PostconditionException($message);}}public static function notNull(mixed $result): void{self::check($result, fn($v) $v ! null, 返回值不能为空);}public static function positiveInt(int $result): void{self::check($result, fn($v) $v 0, 返回值必须是正整数);}}class PostconditionException extends RuntimeException {}class AccountService{public function calculateBalance(int $userId): float{$balance 1000.00;Postcondition::check($balance, fn($v) $v 0, 余额不能为负);return $balance;}}?不变式确保对象的状态始终有效。phpclass BankAccount{private float $balance;private string $currency;private bool $frozen false;public function __construct(float $initialBalance, string $currency CNY){$this-balance $initialBalance;$this-currency $currency;$this-invariant();}private function invariant(): void{if ($this-balance 0) {throw new RuntimeException(余额不能为负);}if (empty($this-currency)) {throw new RuntimeException(货币不能为空);}}public function deposit(float $amount): void{if ($amount 0) {throw new InvalidArgumentException(存款金额必须大于0);}$this-balance $amount;$this-invariant();}public function withdraw(float $amount): void{if ($this-frozen) {throw new RuntimeException(账户已冻结);}if ($amount 0) {throw new InvalidArgumentException(取款金额必须大于0);}if ($amount $this-balance) {throw new RuntimeException(余额不足);}$this-balance - $amount;$this-invariant();}}$account new BankAccount(1000);$account-deposit(500);$account-withdraw(200);echo 余额: {$account-getBalance()}\n;?断言在开发和测试中验证假设。phpclass Assertions{public static function enabled(): bool{return (bool)ini_get(zend.assertions);}public static function true(bool $condition, string $message ): void{if (!self::enabled()) return;assert($condition, $message ?: 断言失败: 条件不为真);}public static function equals(mixed $expected, mixed $actual, string $message ): void{if (!self::enabled()) return;assert($expected $actual, $message ?: 期望值 {$expected}实际值 . json_encode($actual));}public static function throws(callable $fn, string $exceptionClass Throwable::class): void{if (!self::enabled()) return;try {$fn();assert(false, 期望抛出异常 {$exceptionClass});} catch (Throwable $e) {assert($e instanceof $exceptionClass, 期望 {$exceptionClass}实际 . get_class($e));}}}// 生产环境关闭assert// zend.assertions 0$calculator new Calculator();Assertions::equals(5, $calculator-add(2, 3));Assertions::throws(fn() $calculator-divide(10, 0), InvalidArgumentException::class);echo 所有断言通过\n;?防御式编程不是过度防御。在系统边界用户输入、外部服务调用做好验证在内部代码中减少重复检查。前置条件、后置条件和不变式让函数的约定清晰明确调用方和被调用方都清楚各自的责任。