Организация доступа к nullable полям класса
Нередкая ситуация, когда в классе есть поле, которое может содержать null
.
class User
{
private Email $email;
private ?string $name;
}
Пользователь может указать имя, а может и не указывать, ограничившись только почтовым адресом.
А далее мы пишем код, которые работает с полем имени.
class User
{
private ?string $name;
public function hasName(): bool
{
return $this->name !== null;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
И использование этого кода:
/** @var User $user */
if ($user->hasName()) {
do_something_with_name($user->getName());
}
function do_something_with_name(string $name) {}
Выглядит хорошо. Сначала убедились, что имя установлено, а потом использовали его.
Но статический анализатор нам обязательно припомнит, что мы пытаемся передать в функцию
do_something_with_name
значение типа string|null
, хотя функция ожидает значение
типа string
. И получается дурацкая ситуация, что формально мы должны дописать еще одну проверку.
/** @var User $user */
if ($user->hasName()) {
$name = $user->getName();
if ($name !== null) {
do_something_with_name($name);
}
}
function do_something_with_name(string $name) {}
Статический анализатор наш друг, он помогает находить ошибки и несоответствия в коде. И здесь он нашел такое формальное несоответствие типов.
Статический анализатор прав, а мы, как проектировщики интерфейса, не правы. На самом деле мы смешали два подхода, когда описывали методы в нашем классе:
- Получить и проверить
- Проверить и получить
Получить и проверить
И сразу начнем с примера использования.
$name = $user->getName();
if ($name !== null) {
do_something_with_name($name);
}
Сначала мы получаем значение поля, а потом проверяем, соответствует ли это значение нашим требованиям. Класс при этом будет построен вот так:
class User
{
private ?string $name;
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
Заметьте, здесь уже нет метода hasName()
, потому что этот метод перестал быть нужным. Его роль
исполняет метод getName()
.
Проверить и получить
Второй подход: сначала проверяем значение, а потом работаем с ним:
if ($user->hasName()) {
do_something_with_name($user->getName());
}
Структура класса:
class User
{
private ?string $name;
public function hasName(): bool
{
return $this->name !== null;
}
public function getName(): string
{
if ($this->name === null) {
throw new \LogicException('Name is not set');
}
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
Смотрите отличия. Метод hasName()
остается. А вот метод getName()
теперь возвращает
значение типа string
. Он выбросит исключение, если мы попытаемся получить значение, которое не
установлено.
Использование
Теперь встает вопрос, когда и какой подход следует использовать.
- Если ситуация, когда поле не установлено, скорее исключительная, нежели обычная, то можно использовать
второй подход, а проверку опустить. Исключение в методе
getName()
позволит обнаружить странное поведение. - Если в пустом поле нет ничего не обычного, то подход "получить и проверить" будет удобнее, все равно нужно делать проверку.
В любом случае, нужно смотреть на уместность того или иного подхода в каждом случае, и не использовать их одновременно.
08.11.2020, anton@vakhrushev.me