Чому викидання винятків – найкращий вибір?
Короткий розбір з простими та зрозумілими прикладами.
Розглянемо кілька прикладів, коли винятки застосовувати об'єктивно доречно.
Наведу приклад оновлення зображення місця в проєкті 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().
Ось так легко і невимушено ми зробили код більш читабельним і убезпечили себе від можливих помилок при перевірці значення, що повертається.
Звичайно, у винятків є свої мінуси. Наприклад, при настанні події винятку, створюється відповідний об'єкт, який наповнюється потрібними даними.
Так, у сучасних реаліях це займає незначний час і цим можна знехтувати, але в чутливих системах це потрібно враховувати.
Винятки використовувати потрібно, але з розумом!