Как различать "не задано" и "не присвоено" для НУЛЕВЫХ значений?


В 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 в результирующем объекте, чистым способом и без слишком большого вмешательства в класс данных?

Author: Jeffrey Westerkamp, 2018-08-06

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"
 1
Author: Pete, 2018-08-06 23:33:27