具有依赖倒置原则的 TDD

就提高类的可测试性而言,DIP 可能是我认为最重要的原则。DIP 建议细节应依赖于抽象。在我看来,这意味着程序中并不真正属于某个类的细节应该被抽象出来。作为开发者,DIP 允许我们移除例程或程序的具体实现,并将其置于另一个对象中。这样,我们就可以随时使用 DIP 注入我们需要的对象。我们可以在构造函数中注入所需的对象,也可以在类实例化时将其作为参数传递,或者直接暴露一个突变函数。

让我们重温一下本章前面创建的 ToyCarValidator 类,看看如何实现 DIP

这在我们的代码中是什么样子的?

回到 ToyCarValidator.php 类,你会发现在 __constructor 方法中,我们实例化了两个类:

image 2023 10 24 14 16 24 677
Figure 1. Figure 8.15 – Hardcoded dependencies

如何改进?好了,这个程序可以工作了—​正如你所看到的,我们通过了 ToyCarValidatorTest。唯一的问题是,我们的 ToyCarValidator 类现在被硬编码为它的依赖类-- YearValidatorNameValidator 类。如果我们想替换这些类,或者想添加更多验证器,该怎么办?我们可以从类的内部移除依赖关系。请按照以下步骤操作:

  1. 重构以下测试类,并将 testCanValidate 方法替换为以下内容:

    codebase/symfony/tests/Unit/Validator/ToyCarValidatorTest.php
    /**
    * @param ToyCar $toyCar
    * @param array $expected
    * @dataProvider provideToyCarModel
    */
    public function testCanValidate(ToyCar $toyCar, array $expected): void
    {
        $validators = [
            'year' => new YearValidator(),
            'name' => new NameValidator(),
        ];
    
        // Inject the validators
        $validator = new ToyCarValidator();
        $validator->setValidators($validators);
    
        $result = $validator->validate($toyCar);
    
        $this->assertEquals($expected['is_valid'],$result->isValid());
        $this->assertEquals($expected['name'], $result->getReport()['name']['is_valid']);
        $this->assertEquals($expected['year'], $result->getReport()['year']['is_valid']);
    }
    php

    你会注意到,ToyCarValidator 依赖的对象现在是在 ToyCarValidator 类之外实例化的,然后我们使用 setValidators 变量来设置验证器。

  2. 现在,从 ToyCarValidator 的构造函数中删除硬编码的验证器实例:

    codebase/symfony/src/Validator/ToyCarValidator.php
    <?php
    
    namespace App\Validator;
    
    use App\Model\ToyCar;
    use App\Model\ValidationModel as ValidationResult;
    
    class ToyCarValidator implements ToyCarValidatorInterface
    {
        /**
        * @var array
        */
        private $validators = [];
    
        /**
        * @param ToyCar $toyCar
        * @return ValidationResult
        */
        public function validate(ToyCar $toyCar): ValidationResult
        {
            $result = new ValidationResult();
            $allValid = true;
            $results = [];
    
            foreach ($this->getValidators() as $key => $validator) {
                $accessor = 'get' . ucfirst(strtolower($key));
                $value = $toyCar->$accessor();
                $isValid = false;
    
                try {
                    $isValid = $validator->validate($value);
                    $results[$key]['message'] = '';
                } catch (ToyCarValidationException $ex) {
                    $results[$key]['message'] = $ex->getMessage();
                } finally {
                    $results[$key]['is_valid'] = $isValid;
                }
                if (!$isValid) {
                    $allValid = false;
                }
            }
    
            $result->setValid($allValid);
            $result->setReport($results);
    
            return $result;
        }
    
        /**
        * @return array
        */
        public function getValidators(): array
        {
            return $this->validators;
        }
    
        /**
        * @param array $validators
        */
        public function setValidators(array $validators): void
        {
            $this->validators = $validators;
        }
    }
    php
  3. 我们不再有硬编码的验证器实例化——现在,让我们运行测试并看看测试是否仍然通过:

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

    运行命令后,您应该看到测试仍然通过。此时,我们可以继续创建新的验证器,并将它们添加到我们想要注入 ToyCarValidator.php 类的验证器数组中。

  4. 现在,打开我们在本章前面创建的 ToyCarCreator.php 类,你会发现它已经准备好接受来自外部的依赖。我们还可以重构该类,以便在实例化过程中自动注入所需的依赖项。

  5. 打开以下测试类并使用以下内容重构它:

    codebase/symfony/tests/Integration/Processor/ToyCarCreatorTest.php
    <?php
    
    namespace App\Tests\Integration\Repository;
    
    use App\DAL\Writer\WriterInterface;
    use App\Model\CarManufacturer;
    use App\Model\ToyCar;
    use App\Model\ToyColor;
    use App\Model\ValidationModel;
    use App\Processor\ToyCarCreator;
    use App\Validator\ToyCarValidatorInterface;
    use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
    
    class ToyCarCreatorTest extends KernelTestCase
    {
        /**
        * @param ToyCar $toyCarModel
        * @throws \App\Validator\ToyCarValidationException
        * @dataProvider provideToyCarModel
        */
        public function testCanCreate(ToyCar $toyCarModel): void
        {
            $validationResultStub = $this->createMock(ValidationModel::class);
            $validationResultStub
                ->method('isValid')
                ->willReturn(true);
    
            // Mock 1: Validator
            $validatorStub = $this->createMock(ToyCarValidatorInterface::class);
            $validatorStub
                ->method(‘validate’)
                ->willReturn($validationResultStub);
    
            // Mock 2: Data writer
            $toyWriterStub = $this->createMock(WriterInterface::class);
            $toyWriterStub
                ->method(‘write’)
                ->willReturn(true);
    
            // Processor Class
            $processor = new ToyCarCreator($validatorStub,$toyWriterStub);
    
            // Execute
            $result = $processor->create($toyCarModel);
    
            $this->assertTrue($result);
        }
    
        public function provideToyCarModel(): array
        {
            // Toy Car Color
            $toyColor = new ToyColor();
            $toyColor->setName(‘Black’);
    
            // Car Manufacturer
            $carManufacturer = new CarManufacturer();
            $carManufacturer->setName(‘Ford’);
    
            // Toy Car
            $toyCarModel = new ToyCar();
            $toyCarModel->setName(‘Mustang’);
            $toyCarModel->setColour($toyColor);
            $toyCarModel->setManufacturer($carManufacturer);
            $toyCarModel->setYear(1968);
    
            return [
                [$toyCarModel],
            ];
        }
    }
    php

    如你所见,我们实例化了 ToyCarCreator.php 类的依赖项,然后在实例化 ToyCarCreator($validatorStub, $toyWriterStub); 中的类时,将它们作为参数注入。

  6. 然后,打开 ToyCarCreator.php 解决方案类并使用以下内容重构它:

    codebase/symfony/src/Processor/ToyCarCreator.php
    <?php
    
    namespace App\Processor;
    
    use App\DAL\Writer\WriterInterface;
    use App\Model\ToyCar;
    use App\Validator\ToyCarValidationException;
    use App\Validator\ToyCarValidatorInterface;
    
    class ToyCarCreator
    {
        /**
        * @var ToyCarValidatorInterface
        */
        private $validator;
    
        /**
        * @var WriterInterface
        */
        private $dataWriter;
    
        public function __construct(ToyCarValidatorInterface $validator, WriterInterface $dataWriter)
        {
            $this->setValidator($validator);
            $this->setDataWriter($dataWriter);
        }
    
        /**
        * @param ToyCar $toyCar
        * @return bool
        * @throws ToyCarValidationException
        */
        public function create(ToyCar $toyCar): bool
        {
            // Do some validation here and so on...
            $this->getValidator()->validate($toyCar);
    
            // Write the data
            $result = $this->getDataWriter()->write($toyCar);
    
            // Do other stuff.
    
            return $result;
        }
    
        /**
        * @return WriterInterface
        */
        public function getDataWriter(): WriterInterface
        {
            return $this->dataWriter;
        }
    
        /**
        * @param WriterInterface $dataWriter
        */
        public function setDataWriter(WriterInterface $dataWriter): void
        {
            $this->dataWriter = $dataWriter;
        }
    
        /**
        * @return ToyCarValidatorInterface
        */
        public function getValidator(): ToyCarValidatorInterface
        {
            return $this->validator;
        }
    
        /**
        * @param ToyCarValidatorInterface $validator
        */
        public function setValidator(ToyCarValidatorInterface $validator): void
        {
            $this->validator = $validator;
        }
    }
    php

实例化后,验证器和写入器依赖项均通过构造函数设置。

如果我们运行测试,它仍然应该通过:

/var/www/html/symfony# ./runDebug.sh --testsuite=Integration --filter ToyCarCreatorTest
bash

运行该命令后,您应该仍能看到测试通过。

使用这种方法最明显的一点是,你必须自己管理所有的依赖关系,然后将它们注入到需要它们的对象中。幸运的是,我们并不是第一个遇到这种头疼问题的人。有很多服务容器可以帮助管理应用程序所需的依赖关系,但在为 PHP 选择服务容器时,最重要的是它应遵循 PSR-11 标准。有关 PSR-11 的更多信息,请访问 https://www.php-fig.org/psr/psr-11/

总结

在本章中,我们逐一介绍了 SOLID 原则。我们利用测试来启动解决方案代码的开发,以便在实际生活中将它们作为实现 SOLID 原则的示例。

我们介绍了 SRP,它帮助我们使 PHP 类的责任或能力更加集中。OCP 帮助我们避免了在某些情况下,当我们想改变一个类的行为时,需要接触或修改它。LSP 帮助我们更严格地规定了接口的行为,使我们可以更容易地切换实现该接口的具体对象,而不会破坏父类的行为。ISP 帮助我们使接口的责任更加明确—​实现该接口的类不会再因为接口声明了空方法而出现空方法。DIP 帮助我们快速测试 ToyCarCreator 类,即使不创建其依赖项的具体实现,如 ToyCarValidator 类。

在实际项目中,有些原则很难严格遵守,有时界限也很模糊。再加上现实生活中最后期限的压力,情况就会变得更加有趣。有一点可以肯定,使用 BDDTDD 会让你对正在开发的功能更有信心,尤其是当你已经深入项目几个月的时候。在此基础上添加 SOLID 原则,会让你的解决方案更加完美!

在下一章中,我们将尝试利用自动化测试来帮助我们确保团队中任何开发人员推送到代码库中的任何代码更改都不会破坏软件的预期行为。我们将尝试使用持续集成来自动化这一过程。