具有接口隔离原则的 TDD

接口是非常有用的,但有时也很容易被一些本不属于接口的功能所污染。我以前经常遇到这种违规行为。我一直在问自己,我是如何不断创建带有待办注释的空方法的,结果几个月或几年后,我发现类中仍然有这些待办注释,方法仍然是空的。

我以前总是先接触我的接口,然后在里面塞满我认为需要的所有方法。然后,当我最终写出具体实现时,这些具体类中的方法大多是空的。

接口应该只包含该接口特有的方法。如果其中有与该接口不完全相关的方法,就需要将其分离到不同的接口中。

让我们看看实际情况。再次,让我们从一个——你猜对了——测试开始:

  1. 打开代码库 /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

  2. 运行以下测试:

    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength

    您应该会收到错误消息,因为我们尚未创建新方法。

  3. 现在,打开 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;
    }
  4. 太棒了——现在我们有了验证字符串长度的合约。如果我们返回到 NameValidator.php 类,我们将从 IDE 中收到以下错误:

    image 2023 10 24 13 28 23 038
    Figure 1. Figure 8.13 – Must implement the method

    显然,我们需要实现 NameValidator.php 类的 validateLength 方法,这没有问题,因为我们要验证字符串的长度—​但如果我们还想为 ToyCar 模型的颜色创建一个验证器,会发生什么情况呢?ToyCar 模型的颜色属性需要一个 ToyColor.php 对象,而不是字符串!因此,解决办法是从 ValidatorInterface 中删除 validateLength 方法。某些类在实现 ValidatorInterface 时不需要实现这一逻辑。我们可以做的是创建一个名为 StringValidator 接口的新接口,其中可以包含 validateLength 方法。

  5. 重构 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 接口中删除。

  6. 现在,打开 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;
        }
    }
  7. 我们重构了 NameValidator 类,现在它还可以检查名称的长度。让我们运行测试看看是否通过:

    /var/www/html/symfony# ./runDebug.sh --testsuite=Unit --filter testCanValidateNameLength

    现在,您应该看到以下结果:

    image 2023 10 24 13 34 21 414
    Figure 2. Figure 8.14 – Passing the string length validation test

我们所做的不是将不同的方法合并到 ValidatorInterface 中,而是将它们分离到两个不同的接口中。然后,我们只为需要使用 validateLength 方法的验证器对象实现 StringValidator 接口。这就是 ISP 的基本原理。这只是一个非常基本的示例,但如果你不注意的话,很容易成为这些非常强大的接口的牺牲品。

接下来,我们将回到 ToyCarValidator 类,看看如何使用依赖反转原则(DIP)改进我们之前在 TDD 中使用 开放-封闭原则(Open-Closed Principle) 的示例。