在php中为子类强制执行变量声明

时间:2016-04-28 12:37:55

标签: php oop design-patterns phpunit

我有一个像这样的抽象类

<?php
    abstract class AbastractCreationCommand extends AbstactCommand {
        protected $repository;
        function handle($payload) {
            $this->repository->create($payload);
        }
    }

    class TagCreationCmd extends AbstractCreationCommand {
        function __constructor() {
            $this->repository = new TagRepository();
        }
    }
?>

问题

  • 有没有办法在AbstractCreationCommand的子类中强制执行存储库类的定义?
  • 我是否需要为每个子类创建一个测试并调用handle方法或者是另一种测试我所有代码的方法?

4 个答案:

答案 0 :(得分:1)

1.是。当我需要强制实例化一些依赖时,我也会这样做。这种方式是支持GRASP: Creator principle

使用依赖注入实现此目的的另一种方法,但这种方式破坏了Creator原则:

class TagCreationCmd extends AbstractCreationCommand {
    function __constructor(TagRepositoryInterface $tagRepository) {
        $this->repository = $tagRepository;
   }
}
  1. 如果按照3 rules of TDD,您应该为每行代码编写测试。所以答案是肯定的。

答案 1 :(得分:1)

回答1:您不能在抽象类中强制创建子类中的任何内容。至少在实例化期间不会。子类中的所有内容都是可选的。

但是,抽象类中的代码可以在执行需要它的代码部分时检查是否已定义必要的对象,如下所示:

abstract class AbastractCreationCommand extends AbstactCommand {
    protected $repository;
    function handle($payload) {
        if (!$this->repository instanceof TagRepository) {
            throw new \InvalidArgumentException('Need a TagRepository');
        }
        $this->repository->create($payload);
    }
}

然而,很可能是抱怨太晚了。原因可能是因为你使用的是继承而不是组合,或者是继承了错误的东西。

首先,你没有进行依赖注入。您的子类不应直接实例化TagRepository。这会导致测试抽象类的代码以及子类代码时出现问题,因为您无法提供模拟对象。这严重限制了单独测试代码的能力。

此外,如果没有非常具体地知道如何继承抽象类而不是实现任何抽象函数,那么子类就无法工作。如果抽象和子类都来自你作为作者,我会认为可以正确地对你做所有事情。但是如果你希望其他开发人员继承那个抽象类(并且你的问题听起来像这可能是背景问题),那么你根本就不应该这样做。

抽象类确实通过继承为一组子类提供了一些通用函数。但是,如果将所有代码放入非抽象类并将此类注入独立的ex-sub类,则可以实现同样的目的。他们将这些常用函数称为公共方法而不是私有或受保护,并且公共代码的测试也更容易,因为这些方法是公开的。

另请注意,您已经有三个级别的继承,接近令人不安的级别:AbstractCommand - &gt; AbstractCreationCommand - &gt; TagCreationCmd。

问题是您在AbstractCommand中更改的所有内容都必须考虑到两个级别的继承对象。您不能简单地更改受保护变量的名称。您不能简单地添加受保护(或公共)变量,而不检查是否有任何子类已经具有相同名称的变量 - 除非您打算共享它。

维护继承代码的问题不在于继承链末尾的类,而是在顶部有这些类。只要想想有多少类可能会受到不同用法上下文的影响:如果你有AbstractCreationCommands,你将拥有AbstractDeletionCommands和AbstractChangeCommands以及AbstractDoNothingCommands,以及所有这些类型的大量具体命令可以做很多不同的事情。只需在每个级别上成像,你就有四个类 - 这使得你必须维护一个基类,四个继承类和四个四个具体类 - 总共21个类,所有这些都必须进行测试,可能没有人从instanceof AbstractCommand获得任何好处。

回答2:是的,您必须测试所有子类 - 这些是实例化和使用的子类。您还应该单独测试抽象类的代码。 PHPUnit提供了使用模拟框架实例化抽象类,因此任何抽象方法都将被模拟并可以进行配置。但是,当我使用mock作为真正的测试对象时,我总是有一种不好的感觉,因为我不是真的测试纯代码,而是模拟代码和真实代码的某种组合。

一种可能的出路是创建一个测试类,除了扩展抽象类之外几乎不做什么,并使用它。

答案 2 :(得分:0)

  

有没有办法在AbstractCreationCommand的子类中强制执行存储库类的定义?

我没有看到必要性。如果您的AbstractCreationCommand需要repo才能工作,请将其添加为构造函数参数。这并没有强制执行要注入的repo,因为子类型可以覆盖构造函数,但是应该非常清楚AbstractCreationCommand子类型需要某种类型的repo,例如< / p>

abstract class AbstractCreationCommand extends AbstractCommand 
{
    private $repository;

    public function __construct(Repository $repository) 
    {
        $this->repository = $repository
    }

    protected function getRepository(): Repository 
    {
        return $this->repository;
    }

    // …

您还可以使用模板方法模式来指示任何子类型将通过为repo添加抽象getter来使用repo。那么子类型必须实现该方法。然后由开发人员决定实施:

abstract class AbstractCreationCommand extends AbstractCommand
{
    public function handle()
    {
        $this->getRepository()->create();
    }

    abstract function getRepository(): Repository;

    // …

如果你真的必须在创建级别强制执行,你可以将抽象类型的构造函数设置为final protected并在静态工厂方法中创建任何子类型,例如< / p>

abstract class AbstractCreationCommand extends AbstractCommand
{
    private $repository;

    final protected function __construct(Repository $repository)
    {
        $this->repository = $repository;
    }

    // …

现在可以阻止通过new直接实例化任何子类型。尝试new子类型将导致PHP致命错误。

相反,必须像这样创建子类型:

class TagCreationCommand extends AbstractCreationCommand
{
    private $foo;

    public static function create(Repository $repository, Foo $foo)
    {
        $command = new static ($repository);
        $command->setFoo($foo);

        return $command;
    }

    protected function setFoo(Foo $foo)
    {
        $this->foo = $foo;
    }

    // …

然后您调用TagCreationCommand::create(new TagRepository, new Foo);来获取新实例。由于您无法覆盖构造函数并且必须从静态create方法中调用父构造函数,因此您现在可以有效地强制执行Repository。我添加了Foo的东西只是为了说明你如何使用其他依赖项。

正如你所希望的那样,这需要相当多的体操,而前两种方法要轻得多,基本上会产生相同的结果。毕竟,如果没有repo,代码将失败。由于您正在使用测试,因此会引起注意。那么为什么要这么麻烦呢?

  

我是否需要为每个子类创建一个测试并调用handle方法或者是另一种测试我所有代码的方法?

如果要覆盖handle方法,则应在该子类型的具体测试类中测试该行为。

如果您的子类型覆盖handle方法,则可以创建AbstractCreationCommandTest并在其中对handle方法进行测试。但是,如果是这种情况,我想知道为什么你需要AbstractCreationCommand首先是抽象的,因为这听起来你只需要一个CreationCommand

答案 3 :(得分:-1)

根据Yan Burtovoy的建议,我会更进一步,实际执行DI container

<?php
abstract class AbastractCreationCommand extends AbstactCommand {
    protected $repository;
    function __constructor(\DI\Container $container) {
        $this->repository = $container->get('TagRepository');
    }

    function handle($payload) {
        $this->repository->create($payload);
    }
}

您应该为您的图书馆用户(即您的应用程序)公开的所有内容创建测试 所以,如果你有一个依赖于handle()被调用的子类,那么你应该为此编写一个测试。原因是在6个月内有人可能会更改继承或覆盖handle()方法并更改初始预期行为。