В языке Go есть полезная конструкция defer. Обычно она используется для освобождения ресурсов и работает следующим образом: в качестве аргумента defer передается функция, которая помещается в список функций. Этот список функций выполняется при выходе из объемлющей функции.
У defer есть несколько очевидных и не очень достоинств:
- улучшает понимание кода — при создании ресурса сразу виден код, ответственный за его освобождение. Не нужно искать try {} finally {} или все точки выхода из функции
- позволяет избежать частых ошибок, связанных с освобождением ресурсов, например, с необработанными исключениями, или в случае открытия нескольких ресурсов.
К примеру, такой код:
class Utils { public function copyFile(string $sourceName, string $destName): void { $readHandle = fopen($sourceName, "r"); if ($readHandle === false) { throw new \Exception(); } $writeHandle = fopen($destName, "w"); if ($writeHandle === false) { fclose($readHandle); throw new \Exception(); } while (($buffer = fgets($readHandle)) !== false) { $wasFailure = fwrite($writeHandle, $buffer); if ($wasFailure) { fclose($readHandle); fclose($writeHandle); throw new \Exception(); } } if (!feof($readHandle)) { fclose($readHandle); fclose($writeHandle); throw new \Exception(); } fclose($readHandle); fclose($writeHandle); } }
Можно было бы превратить в такой:
class Utils { public function copyFile(string $sourceName, string $destName): void { $readHandle = fopen($sourceName, "r"); if ($readHandle === false) { throw new \Exception(); } defer fclose($readHandle); $writeHandle = fopen($destName, "w"); if ($writeHandle === false) { throw new \Exception(); } defer fclose($writeHandle); while (($buffer = fgets($readHandle)) !== false) { $wasFailure = fwrite($writeHandle, $buffer); if ($wasFailure) { throw new \Exception(); } } if (!feof($readHandle)) { throw new \Exception(); } } }
Во втором случае работать с закрытием файлов гораздо проще — каждый файл нужно закрыть только один раз. Снижается вероятность, что кто-то забудет закрыть файл, особенно, если их будет не 2, а больше.
Но, к сожалению, в PHP нет defer. Зато можно написать свою реализацию. Она выглядит следующим образом:
class DeferredContext { protected $deferredActions = []; public function defer(callable $deferAction) { $this->deferredActions[] = $deferAction; } public function executeDeferredActions() { $actionsCount = count($this->deferredActions); if ($actionsCount > 0) { for ($i = $actionsCount - 1; $i >= 0; $i--) { $action = $this->deferredActions[$i]; try { $action(); } catch (\Exception $e) { } unset($this->deferredActions[$i]); } } $this->deferredActions = []; } } trait DeferredTrait { private function deferred(callable $callback) { $context = new DeferredContext(); try { $callback($context); } finally { $context->executeDeferredActions(); } } }
DeferredContext — класс, в котором накапливаются функции-обработчики. При выходе из функции необходимо вызвать метод executeDeferredActions(), который выполнит все обработчики. Для того, чтобы не создавать вручную DeferredContext, можно использовать трейт DeferredTrait, который инкапсулирует в себе логику работы с DeferredContext.
С использованием данного подхода код из примера выше будет выглядеть так:
class Utils { use DeferredTrait; public function copyFile(string $sourceName, string $destName): void { $this->deferred(function(DeferredContext $context) use ($destName, $sourceName) { $readHandle = fopen($sourceName, "r"); if ($readHandle === false) { throw new \Exception(); } $context->defer(function() use ($readHandle) { fclose($readHandle); }); $writeHandle = fopen($destName, "w"); if ($writeHandle === false) { throw new \Exception(); } $context->defer(function() use ($writeHandle) { fclose($writeHandle); }); while (($buffer = fgets($readHandle)) !== false) { $wasFailure = fwrite($writeHandle, $buffer); if ($wasFailure) { throw new \Exception(); } } if (!feof($readHandle)) { throw new \Exception(); } }); } }
Надеюсь, что эта идея поможет вам уменьшить количество багов в коде и создавать более надежные программы.