Я хотел бы объединить 2 объекта посредством композиции с одним необязательным объектом и сохранить общий и чистый 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, который у меня есть, и сейчас использую методы.
Есть ли что-нибудь, что можно сделать здесь, чтобы это исправить, или я должен просто пойти с упомянутой цепочкой методов решения?
Я думаю, вы имеете в виду что-то вроде нуль-безопасный оператор.
В отличие от мотыга, PHP не имеет этого оператора (соответствующий RFC), так что вам нужно явно проверить значения для нуля. Также вы можете использовать что-то вроде Тип варианта. По крайней мере, вы можете динамически создавать версии зависимых объектов по умолчанию:
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.
}
}
У вас есть два варианта: либо всегда проверять, установлен ли объект, прежде чем что-либо возвращать из него, либо использовать «фиктивный» объект вместо предоставленного пользователем, если он не был предоставлен.
Первый может стать довольно грязным, так как вы должны добавить охрану вокруг всего, поэтому я лично предпочел бы последний.
Одна вещь, которая сделает реализацию последнего намного проще, — это использование интерфейса для определения спецификаций ожидаемого объекта и использование вашего конструктора для создания фиктивного объекта, если не предоставлен реальный объект.
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-> logger, потому что он всегда установлен.