homepage/source/_articles/2020-11-08-nullable-fields.md

178 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Доступ к полям класса с null
description: Заметка о том, лучше организовать доступ к полям класса, которые могут содержать значение null
keywords: [чистый код, php, null, поля класса]
---
Нередкая ситуация, когда в классе есть поле, которое может содержать `null`.
```php
class User
{
private Email $email;
private ?string $name;
}
```
Пользователь может указать имя, а может и не указывать,
ограничившись только почтовым адресом.
А далее мы пишем код, которые работает с полем имени.
```php
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;
}
}
```
И использование этого кода:
```php
/** @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`.
И получается дурацкая ситуация, что формально мы должны дописать
еще одну проверку.
```php
/** @var User $user */
if ($user->hasName()) {
$name = $user->getName();
if ($name !== null) {
do_something_with_name($name);
}
}
function do_something_with_name(string $name) {}
```
Статический анализатор наш друг, он помогает находить ошибки и несоответствия
в коде.
И здесь он нашел такое формальное несоответствие типов.
Статический анализатор прав, а мы, как проектировщики интерфейса, не правы.
На самом деле мы смешали два подхода, когда описывали методы в нашем классе:
1. Получить и проверить
2. Проверить и получить
## Получить и проверить
И сразу начнем с примера использования.
```php
$name = $user->getName();
if ($name !== null) {
do_something_with_name($name);
}
```
Сначала мы получаем значение поля, а потом проверяем, соответствует ли это
значение нашим требованиям. Класс при этом будет построен вот так:
```php
class User
{
private ?string $name;
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
```
Заметьте, здесь уже нет метода `hasName()`, потому что этот метод перестал быть
нужным. Его роль исполняет метод `getName()`.
## Проверить и получить
Второй подход: сначала проверяем значение, а потом работаем с ним:
```php
if ($user->hasName()) {
do_something_with_name($user->getName());
}
```
Структура класса:
```php
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()` позволит обнаружить странное поведение.
- Если в пустом поле нет ничего не обычного, то подход "получить и проверить"
будет удобнее, все равно нужно делать проверку.
В любом случае, нужно смотреть на уместность того или иного подхода в каждом
случае, и не использовать их одновременно.