PHP结合了2个类,其中1可以是组合的可选项

时间:2017-10-24 09:32:08

标签: php class inheritance interface composition

我想通过合成将两个对象与一个可选对象组合在一起,并保留一个通用且干净的API。举个例子:

class Composition implements Object1Interface, Object2Interface
{
    private $object1;

    private $object2;

    public function __construct($object1, $object2 = null)
    {
        $this->$object1 = $object1;
        $this->$object2 = $object2;
    }

    // This will always work since the object is mandatory.
    public function getId() {
        return $this->object1->getId();
    }

    // This may fail because the object2 can be null.
    public function getName() {
        return $this->object2->getName();
    }
}

你可以看到,这开始很快失败,因为我可以在这里代理一个空对象。什么是解决这个问题的好方法?

如果我要强制$object2而我可能没有任何构造函数数据一直填写$object2,我需要使构造函数的所有参数都是可选的。这似乎是一个很大的禁忌,因为它不再需要论证。

一个选项是创建一个返回$object2的方法,但这需要用户链接,如下所示:

$composition->getObject2()->getName()

这可以接受,但我正在破坏我所拥有的干净API,现在正在链接方法。

这里有什么可以解决的问题,或者我应该简单地使用上面提到的方法解决方案吗?

2 个答案:

答案 0 :(得分:1)

我猜你的意思是null-safe operator

Hack不同,PHP没有此运算符(相关的RFC),因此您需要explicitly check null的值。您也可以使用Option Type之类的内容。至少你可以动态创建"默认"依赖对象的版本:

class Composition implements Object1Interface, Object2Interface
{
    private $object1;

    // ...

    public function __construct(Object1Interface $object1 = null, Object2Interface $object2 = null)
    {
        if ($object1 !== null) {
            $this->object1 = $object1;
        } else {
            $this->object1 = new class implements Object1Interface
            {
                public function getId()
                {
                    return null;
                }
            };
        }

        // ...
    }

    public function getId() {
        return $this->object1->getId(); // Always works.
    }
}

答案 1 :(得分:1)

您有两种选择:在从对象返回之前始终检查对象是否已设置,或者使用" dummy"如果没有提供,则代替用户提供的对象。

前者可能变得非常混乱,因为你必须在所有东西周围添加警卫,所以我个人更喜欢后者。

使实现后者更简单的一件事是使用接口来指定期望对象的规范,并让构造函数在没有提供真实对象的情况下实例化虚拟对象。

interface AThingThatDoesSomething
{
    public function getValue() : integer
}

class RealClass implements AThingThatDoesSomething
{
    public function getValue() : integer
    {
        return mt_rand();
    }
}

class DummyClass implements AThingThatDoesSomething
{
    public function getValue() : integer
    {
        // You could return a dummy value here, or throw an exception, or whatever you deem to be the correct behaviour for the dummy class.  This implementation just returns 0 for simplicity
        return 0;
    }
}

class ConsumingClass
{
    private $requiredInstance = null;
    private $optionalInstance = null;

    public function __construct(AThingThatDoesSomething $requiredInstance, AThingThatDoesSomething $optionalInstance = null)
    {
        if (null === $optionalInstance)
        {
            $optionalInstance = new DummyClass();
        }

        $this->requiredInstance = $requiredInstance;
        $this->optionalInstance = $optionalInstance;
    }

    public function getRequiredVal() : integer
    {
        return $this->requiredInstance->getValue();
    }

    // You don't need to worry if the user supplied an optional instance here because if they didn't then the dummy instance will have been instantiated instead
    public function getOptionalVal() : integer
    {
        return $this->optionalInstance->getValue();
    }
}

这似乎是一个人为的例子,当然你是对的,但它也展示了一种名为Design By Contract的模式的好处之一。只要对象承诺满足某些条件(在这种情况下通过实现接口),您就可以替换满足这些条件的任何对象,即使该对象实际上没有做任何事情。

在现实生活中,我将它用于需要记录的类。我使用psr \ log包并在构造函数中设置一个新的NullLogger。如果我需要实际记录,那么我使用setLogger()来传递记录器,但如果我不这样做,我就不必担心$ this->记录器失败,因为它始终是组。