Почему выбрасывать исключения - лучший выбор?
Краткий разбор с простыми и понятными примерами.
Разберём пару примеров, когда исключения применять объективно уместно.
Приведу пример обновления картинки места в проекте Tip2Go.
Давайте начнём писать класс PlaceService
с методом updatePlaceImage
.
class PlaceService {
public function updatePlaceImage(int $place_id, string $image_filename): string
{
$place = $this->getPlaceById($place_id);
...
}
}
С возможностью появления ошибки мы столкнулись уже при получении места по его ID, так как может быть передан несуществующий ID и вместо существующего места вернётся null
.
В случае, если вернулся null
, нам не нужно продолжать выполнение кода и нужно вернуться обратно без обновления картинки. Для этого нам нужно изменить возвращаемые методом updatePlaceImage
типы и добавить возможность возврата null
на случай ошибки.
class PlaceService {
public function updatePlaceImage(int $place_id, string $image_filename): ?string
{
$place = $this->getPlaceById($place_id);
if (!$place instanceof Place) {
return null;
}
if (!file_exists($image_filename)) {
return null;
}
$newImage = ImageHelper::resize($image_filename);
if (!file_exists(self::IMAGES_DIR . '/' . $place->getId())) {
$mkdir = mkdir(self::IMAGES_DIR . '/' . $place->getId());
}
if (!$mkdir) {
return null;
}
}
}
Теперь мы добавили проверку на существование исходного файла и директории для хранения картинок и если её нет, то создаём её, использую ID места.
В данном случае мы можем столкнуться сразу с двумя ошибками: отсутствие исходно файла и невозможность создания новой директории. В случае ошибок, мы так же возвращаем null
и прекращаем установку новой картинки.
Хорошо, наш метод уже в трёх местах может прервать своё выполнение из-за ошибок. Дальше можно добавить ещё проверки на ошибки, например, при перемещении временного файла в новую директорию и уже получится четыре точки, в которых может прерваться выполнение метода.
И как бы нам понять, что именно случилось? Вариант первый и самый наглядный, добавить в метод updatePlaceImage
возможность возвращать int
и в нём получать код ошибки.
class PlaceService {
public function updatePlaceImage(int $place_id, string $image_filename): string|int
{
$place = $this->getPlaceById($place_id);
if (!$place instanceof Place) {
return -1;
}
if (!file_exists($image_filename)) {
return -2;
}
$newImage = ImageHelper::resize($image_filename);
if (!file_exists(self::IMAGES_DIR . '/' . $place->getId())) {
$mkdir = mkdir(self::IMAGES_DIR . '/' . $place->getId());
}
if (!$mkdir) {
return -3;
}
}
}
В этом случае, в вызывающем методе, нам нужно будет проверять все варианты возвращённых значений.
$placeService = new PlaceService();
$result = $placeService->updatePlaceImage(1, 'filename.jpg');
if (is_string($result)) {
return 'Картинка обновлена';
} else {
switch($result) {
case -1:
return 'Место с таким ID не найдено';
break;
case -2:
return 'Исходная картинка не найдена';
break;
case -3:
return 'Невозможно создать директорию с ID места';
break;
}
}
А теперь представьте, как будет разрастаться блок switch
при увеличении количества проверок? А если сам метод должен возвращать int
, то ещё что-то придумывать с возвращаемым типом ошибок? В общем, такой код быстро превратится в нечитаемые спагетти.
А теперь давайте попробуем сделать те же самые проверки, но с использованием исключений.
class PlaceService {
public function updatePlaceImage(int $place_id, string $image_filename): string
{
$place = $this->getPlaceById($place_id);
if (!$place instanceof Place) {
throw new Exception('Место с таким ID не найдено');
}
if (!file_exists($image_filename)) {
throw new Exception('Исходная картинка не найдена');
}
$newImage = ImageHelper::resize($image_filename);
if (!file_exists(self::IMAGES_DIR . '/' . $place->getId())) {
$mkdir = mkdir(self::IMAGES_DIR . '/' . $place->getId());
}
if (!$mkdir) {
throw new Exception('Невозможно создать директорию с ID места');
}
}
}
В таком случае, при вызове метода, мы будем делать всего одну проверку на исключение.
$placeService = new PlaceService();
try {
$placeService->updatePlaceImage(1, 'filename.jpg');
} catch (Exception $exception) {
return $exception->getMessage();
}
return 'Картинка обновлена';
Так как мы при выбрасывании исключения уже указали текст ошибки, нам не нужно его повторно писать, он возвращается при вызове $exception->getMessage()
.
Вот так легко и непринуждённо мы сделали код более читаемым и обезопасили себя от возможных ошибок при проверке возвращаемого значения.
Конечно, у исключений есть свои минуты. Например, при наступлении события исключения, создаётся соответствующий объект, который наполняется нужными данными.
Да, в современных реалиях это занимает незначительное время и этим можно пренебречь, но в чувствительных системах это нужно учитывать.
Исключения использовать нужно, но с умом!