Подсказка возвращаемого типа PHP 7

Уже есть много вопросов по подсказке возвращаемого типа PHP 7 и почему невозможно вернуть null, если класс был определен как возвращаемый тип. Как этот: Правильный способ обработки возвращаемых типов PHP 7 . В ответах обычно говорится, что это не проблема, поскольку если функция должна возвращать класс, то возврат null, вероятно, в любом случае является исключением. Может быть, я что-то упустил, но я не очень понимаю, почему. Например, давайте посмотрим на простой пользовательский класс:

class User
{
    private $username;  // mandatory
    private $password;  // mandatory
    private $realName;  // optional
    private $address;   // optional

    public function getUsername() : string {
        return $this->username;
    }

    public function setUsername(string $username) {
        $this->username = $username;
    }

    public function getPassword() : string {
        return $this->password;
    }

    public function setPassword(string $password) {
        $this->password = $password;
    }

    public function getRealName() : string {
        return $this->realName;
    }

    public function setRealName(string $realName = null) {
        $this->realName = $realName;
    }

    public function getAddress() : Address {
        return $this->address;
    }

    public function setAddress(Address $address = null) {
        $this->address = $address;
    }

}

В нашем приложении абсолютно законно иметь пользователя без реального имени и/или без адреса. Я даже могу установить для полей realName и address значение null, даже если приведенное выше решение не является оптимальным. И на нашей странице профиля я хочу отображать подсказку, если адрес нулевой (пустой).

Но это только 1 пример. Наше приложение имеет более 100 таблиц базы данных и соответствующих классов PHP, и почти все они имеют необязательные поля. Всегда писать блок try catch вместо простой проверки null для обработки необязательных полей кажется мне немного неоптимальным.

Итак, что является хорошим решением для приведенного выше примера?


person Vmxes    schedule 11.04.2016    source источник
comment
Вы можете просто инициализировать переменные пустыми значениями по умолчанию (например, строки для '' и адрес для объекта адреса с пустыми/недействительными значениями по умолчанию. Вы также можете использовать свои классы, вызывая их из оболочки, которая поймает исключение, если вы возвращаете ноль.   -  person Nadir    schedule 11.04.2016
comment
Я не думаю, что getAddress().getCity === '' лучше, чем просто проверить getAddress() == null. Кроме того, проверка может быть сложной. Например, если почтовый индекс является необязательным, новый разработчик может написать код: getAddress().getZip() === '', и он/она думает, что адрес отсутствует, однако отсутствует только почтовый индекс.   -  person Vmxes    schedule 11.04.2016
comment
В вашем примере, каким вы ожидаете поведение вызывающего кода, когда он вызывает getRealName(), когда $this->realName имеет значение null? Нужно ли проверять значение и обрабатывать его, если оно равно нулю? Если это так: ваш класс должен это делать. Если вы используете то, что нулевые строки обрабатываются как пустые строки для многих строковых операций, вам, вероятно, следует по умолчанию использовать пустую строку, а не нуль. Большинство ситуаций можно разрешить таким образом. Тем не менее, я думаю, что PHP неуместно придерживается этого мнения.   -  person Adam Cameron    schedule 11.04.2016
comment
Можно сделать что-то Address::empty(), чтобы указать пустой адрес вместо использования null. Хотя лично я бы не стал указывать необязательные поля.   -  person apokryfos    schedule 11.04.2016
comment
@AdamCameron Класс вызывающей стороны (например, UserPrifile) должен справиться с ситуацией. В этом случае, например, в регистрационной форме присутствуют только поля имени пользователя и пароля. Таким образом, realName должно оставаться нулевым, так как позже на странице профиля я могу показать подсказку, чтобы заполнить настоящее имя. Однако, если пользователь этого не хочет, мы просто сохраним пустую строку, чтобы в следующий раз не беспокоить пользователя. Таким образом, null и пустая строка приводят к другой логике.   -  person Vmxes    schedule 11.04.2016


Ответы (2)


Проблема в том, что ваш объект не соответствует правилам ООП.

Во-первых, геттеры и сеттеры являются злом, потому что раскрывают внутреннюю структуру объекта. Вы просто вводите бесконечную возможность зависеть от внутренней структуры пользовательского объекта для остальной части вашей системы. В то время как целью объекта является скрытие деталей реализации и предоставление интерфейса для управления этими скрытыми данными.

И null — зло (или ошибка на миллиард долларов), потому что код гораздо менее надежен. Он вводит особый случай (нулевое возвращаемое значение), и необходимо обрабатывать этот особый случай в коде с использованием вашего объекта. Хуже всего то, что если вы забудете обработать этот «нулевой» особый случай (или если вы не знаете, что есть особый случай), вы не сразу получите ошибку. Таким образом, вы получите скрытую ошибку, поиск и исправление которой может занять очень много времени.

Практически я вижу следующие варианты (во всех случаях - убрать геттеры):

Вариант 1) Получить снимок данных профиля в какой-то унифицированной форме

$user->getProfileData() {
    return [
        'username': $this->username,
        'realName': $this->realName ? $this->realName : '',
        'address': $this->address ? $this->address : 'Not specified'
        ...
    ];
}

Это все еще своего рода геттер, но вы приводите все данные в унифицированный вид (строки). Даже если в вашей базе данных есть целочисленные поля с плавающей запятой (например, возраст или рост), вы все равно вернете здесь строки, поэтому остальная часть вашей системы не зависит от фактического внутреннего устройства объекта.

Пустая строка (или специальное значение, например «не указано») для необязательных полей также действует как своего рода «нулевой объект». Также можно обернуть возвращаемые значения в небольшие поля-объекты и фактически использовать шаблон нулевого объекта для необязательных полей.

В коде, который использует класс, вы не должны иметь дело со специальным «нулевым» случаем, вы просто перебираете поля и показываете строки (в том числе пустые).

Вариант 2) Сделать сам объект ответственным за презентацию

$user->showProfile($profileView) {
    $profileView->addLabel('First Name');
    $profileView->addString($this->username);
    ...
}

Здесь вы сохраняете внутренние детали внутри объекта, но теперь он делает слишком много, поэтому кто-то может сказать, что теперь он нарушает Рекомендуемая розничная цена.

Вариант 3) Создать специальный объект, отвечающий за представление

$userPresentation = $user->createPresentation()
// internally it will return new UserPresentation($this->username, this->realName, $this->address, ...);
// now display it - generate the template and insert it into the view
<? echo $userPresentation->getHtml(); ?>

Здесь вы выносите логику представления в отдельный объект. Пользовательский объект и его представление тесно связаны, но остальная часть системы теперь ничего не знает (и не имеет возможности узнать) о внутреннем устройстве пользовательского объекта.

person Boris Serebrov    schedule 13.04.2016
comment
Очень подробный ответ, однако я думаю, что в случае простого класса сущностей все 3 варианта нарушают SRP. Я не хочу всегда создавать новый геттер и новый класс для подмножества свойств пользователя в зависимости от того, что требуется вызывающему классу. И, конечно же, мне не нужна логика представления в моем классе сущностей. - person Vmxes; 14.04.2016
comment
@Vmxes проблема с вашей текущей реализацией заключается в том, что она нарушает один из основных принципов ООП - инкапсуляцию. Хотя вы определяете свои поля как private, вы затем открываете эти поля миру с помощью геттеров и сеттеров. И вы возвращаете нули, поэтому вам нужно проверять особые случаи по всему коду. Как только вы начнете думать, как избавиться от этих недостатков и использовать другие подходы, вы увидите, что остальная часть вашего кода также станет более простой и унифицированной. Мои примеры — это лишь некоторые варианты, которые пришли вам на ум, проверьте также статьи, на которые я ссылаюсь. - person Boris Serebrov; 14.04.2016

Недавно я принял идею о том, что null означает наличие проблемы, поэтому я знаю, что, когда я вижу это, это означает, что что-то пошло не так.

В таких ситуациях я предпочитаю использовать шаблон проектирования Null Object.
Итак, для обработки адрес, вы бы создали что-то вроде:

class NullAddress implements AddressInterface { }

Класс Address также должен реализовать AddressInterface.
Тогда getAddress() будет выглядеть так:

public function getAddress(): AddressInterface {
    return $this->address ?? new NullAddress();
}

Затем функция, вызывающая getAddress(), может проверить нулевое значение следующим образом:

if ($user->getAddress() instanceof NullAddress) {
    // Implement result of empty address here.
}

Для обработки строк до сих пор мне хорошо служила возвращаемая пустая строка. Я просто использую empty(), чтобы проверить это.

person Batandwa    schedule 07.12.2016