Defer: из Go в PHP

habr.png

В языке 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();
            }
        });

    }
}


Надеюсь, что эта идея поможет вам уменьшить количество багов в коде и создавать более надежные программы.

© Habrahabr.ru