具有接口隔离原则的 TDD
接口是非常有用的,但有时也很容易被一些本不属于接口的功能所污染。我以前经常遇到这种违规行为。我一直在问自己,我是如何不断创建带有待办注释的空方法的,结果几个月或几年后,我发现类中仍然有这些待办注释,方法仍然是空的。
我以前总是先接触我的接口,然后在里面塞满我认为需要的所有方法。然后,当我最终写出具体实现时,这些具体类中的方法大多是空的。
接口应该只包含该接口特有的方法。如果其中有与该接口不完全相关的方法,就需要将其分离到不同的接口中。
让我们看看实际情况。再次,让我们从一个——你猜对了——测试开始:
-
打开代码库
/symfony/tests/Unit/Validator/NameValidatorTest.php
测试类并添加以下内容:/** * @param $data * @param $expected * @dataProvider provideLongNames */ public function testCanValidateNameLength(string $name, bool $expected): void { $validator = new NameValidator(); $isValid = $validator->validateLength($name); $this->assertEquals($expected, $isValid); } /** * @return array */ public function provideLongNames(): array { return [ ['TheQuickBrownFoxJumpsOverTheLazyDog', false], ]; }
我们在测试中引入了一个名为
validateLength
的新函数,这是字符串常用的函数。我们还添加了一个很长的名称,并在数据提供程序中设置了false
。 -
运行以下测试:
/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength
您应该会收到错误消息,因为我们尚未创建新方法。
-
现在,打开
ValidatorInterface.php
接口并添加我们期望在测试中使用的validateLength
方法:codebase/symfony/src/Validator/ValidatorInterface.php<?php namespace App\Validator; interface ValidatorInterface { /** * @param $input * @return bool * @throws ToyCarValidationException */ public function validate($input): bool; /** * @param string $input * @return bool */ public function validateLength(string $input): bool; }
-
太棒了——现在我们有了验证字符串长度的合约。如果我们返回到
NameValidator.php
类,我们将从 IDE 中收到以下错误:Figure 1. Figure 8.13 – Must implement the method显然,我们需要实现
NameValidator.php
类的validateLength
方法,这没有问题,因为我们要验证字符串的长度—但如果我们还想为ToyCar
模型的颜色创建一个验证器,会发生什么情况呢?ToyCar
模型的颜色属性需要一个ToyColor.php
对象,而不是字符串!因此,解决办法是从ValidatorInterface
中删除validateLength
方法。某些类在实现ValidatorInterface
时不需要实现这一逻辑。我们可以做的是创建一个名为StringValidator
接口的新接口,其中可以包含validateLength
方法。 -
重构
codebase/symfony/src/Validator/ValidatorInterface.php
接口并删除我们刚刚添加的validateLength
方法,并创建包含以下内容的以下文件:codebase/symfony/src/Validator/StringValidatorInterface.php<?php namespace App\Validator; interface StringValidatorInterface { /** * @param string $input * @return bool */ public function validateLength(string $input): bool; }
在此阶段,我们已将
validateLength
方法分离到一个单独的接口中,将其从ValidatorInterface.php
接口中删除。 -
现在,打开
NameValidator.php
类,并使用以下内容重构它:codebase/symfony/src/Validator/NameValidator.php<?php namespace App\Validator; class NameValidator implements ValidatorInterface, StringValidatorInterface { const MAX_LENGTH = 10; /** * @param $input * @return bool */ public function validate($input): bool { $isValid = false; if (preg_match("/^([a-zA-Z' ]+)$/", $input)) { $isValid = true; } if ($isValid) { $isValid = $this->validateLength($input); } return $isValid; } /** * @param string $input * @return bool */ public function validateLength(string $input): bool { if (strlen($input) > self::MAX_LENGTH) { return false; } return true; } }
-
我们重构了
NameValidator
类,现在它还可以检查名称的长度。让我们运行测试看看是否通过:/var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength
现在,您应该看到以下结果:
Figure 2. Figure 8.14 – Passing the string length validation test
我们所做的不是将不同的方法合并到 ValidatorInterface
中,而是将它们分离到两个不同的接口中。然后,我们只为需要使用 validateLength
方法的验证器对象实现 StringValidator
接口。这就是 ISP 的基本原理。这只是一个非常基本的示例,但如果你不注意的话,很容易成为这些非常强大的接口的牺牲品。
接下来,我们将回到 ToyCarValidator
类,看看如何使用依赖反转原则(DIP)改进我们之前在 TDD 中使用 开放-封闭原则(Open-Closed Principle) 的示例。