Как различать "не задано" и "не присвоено" для НУЛЕВЫХ значений?
В php в этом примере – но в общем программировании на самом деле, есть ли способ отличить команду "без присвоения" и "неустановленное значение" для значений null
при объединении 2 неизменяемых объектов данных одного и того же типа?
Рассмотрим этот класс php, который является неизменяемым объектом данных. Он принимает строку и целое число в своем конструкторе и предоставляет только методы доступа для значений:
class Data
{
protected $someNumber;
protected $someString;
public function __construct(?int $someNumber, ?string $someString)
{
$this->someNumber = $someNumber;
$this->someString = $someString;
}
public function getSomeNumber(): ?int
{
return $this->someNumber;
}
public function getSomeString(): ?string
{
return $this->someString;
}
}
Значения могут быть либо null
, либо их соответствующими типы данных string
или integer
в любое время. Также конструктор принимает значения null
вместо string
и/или int
: операция ОТМЕНЫ.
Теперь я хочу иметь возможность объединить 2 экземпляра Data
, что-то вроде этого упрощенного фабричного метода, который принимает $first
и $second
, где данные в $second
переопределяют данные в $first
, если они присутствуют.
class DataFactory
{
public function merge(Data $first, Data $second): Data
{
// Uses data from $first if corresponding data from
// $second is (strictly) null
return new Data(
$second->getSomeNumber() ?? $first->getSomeNumber(),
$second->getSomeString() ?? $first->getSomeString(),
}
}
В приведенном выше примере значения null
, возвращаемые средствами доступа $second
, интерпретируются как операция БЕЗ ОБНОВЛЕНИЯ: при обнаружении null
соответствующее значение $first
сохраняется. Проблема в том, что я хочу иметь возможность различать запрос либо на операцию БЕЗ ОБНОВЛЕНИЯ, либо на операцию БЕЗ УСТАНОВКИ внутри merge
.
Строгая типизация в классе Data
запрещает использование какой-либо строковой константы, такой как "DATA_UNSET_FIELD"
, в качестве значения для флага, поэтому реализация этого непосредственно в самих данных кажется невозможной. Более того, даже потому, что передача конструктора null для любого значения определенно должна означать SET NULL.
Я являюсь думая о каком-то объективе обновления, который явно определяет свойства, которые должны быть отключены при объединении, так что значения null
в $second
просто означали бы ОТСУТСТВИЕ ОБНОВЛЕНИЯ (не $first
).
Каким был бы компактный объектно-ориентированный шаблон для решения этой проблемы? Я уже могу представить себе такие проблемы, как взрыв простых схем массивов или взрыв классов стратегий по мере роста данных. Также меня немного беспокоит "мобильность" объектов Data
, поскольку новые объекты должны быть связаны с ними в какой-то момент.
Заранее спасибо!
Редактировать
Я хотел бы иметь возможность различать не переопределяющее текущее значение и отменяющее значение, т.Е. присваивающее null
– при объединении 2 экземпляров Data
, где $first
является базой, а $second
переопределяет данные $first
. Как деталь, слияние приводит к третьему, новому объекту, который является результатом слияния.
Просмотр значений фрагмента DataFactory
null
в $second
в настоящее время интерпретируются как "сохраняйте соответствующее значение $first
". Но как мне перенести другой флаг для каждого поля, указывающий, какие поля должны быть установлены в null
в результирующем объекте, чистым способом и без слишком большого вмешательства в класс данных?
1 answers
PHP не имеет возможности различать неназначенные и нулевые переменные. Это делает практически неизбежным отслеживание того, какие свойства следует перезаписать.
Я вижу, что у вас есть две проблемы:
- Сохранение
Data
неизменным - Поддержание интерфейса
Data
в чистоте (например, применение строгих типов)
Одной из простейших структур данных, которая способна отслеживать "определенные" и "неопределенные" свойства, является объект \stdClass
(но массив идеально тоже хорошо).
Переместив метод merge()
в класс Data
, вы сможете скрыть любые детали реализации, сохранив интерфейс в чистоте.
Реализация может выглядеть примерно так:
final class Data {
/** @var \stdClass */
protected $props;
// Avoid direct instantiation, use ::create() instead
private function __construct()
{
$this->props = new \stdClass();
}
// Fluent interface
public static function create(): Data
{
return new self();
}
// Enforce immutability
public function __clone()
{
$this->props = clone $this->props;
}
public function withSomeNumber(?int $someNumber): Data
{
$d = clone $this;
$d->props->someNumber = $someNumber;
return $d;
}
public function withSomeString(?string $someString): Data
{
$d = clone $this;
$d->props->someString = $someString;
return $d;
}
public function getSomeNumber(): ?int
{
return $this->props->someNumber ?? null;
}
public function getSomeString(): ?string
{
return $this->props->someString ?? null;
}
public static function merge(...$dataObjects): Data
{
$final = new self();
foreach ($dataObjects as $data) {
$final->props = (object) array_merge((array) $final->props, (array) $data->props);
}
return $final;
}
}
$first = Data::create()
->withSomeNumber(42)
->withSomeString('foo');
// Overwrite both someNumber and someString by assigning null
$second = Data::create()
->withSomeNumber(null)
->withSomeString(null);
// Overwrite "someString" only
$third = Data::create()
->withSomeString('bar');
$merged = Data::merge($first, $second, $third); // Only "someString" property is set to "bar"
var_dump($merged->getSomeString()); // "bar"