Иногда перехват исключения может быть преобразован в предварительную проверку, которая в первую очередь исключает возникновение исключения. Затем код вызывается только в обычном случае, тогда как ранее исключительные просто завершают текущий метод. Так что речь идет не об автоматизированном тестировании, а об условно вставленном в нужном месте.
Почему избегать исключения?
В прошлой статье мы видели, что исключения, как правило, являются более гибким решением в отношении кодов ошибок. Тем не менее, они не являются более чистым решением в отношении всего остального . Например, большая часть управления потоком должна управляться с помощью обычного кода, поскольку это легче понять, чем последовательность блоков try / catch. Исключения должны быть зарезервированы для реальных ошибок .
Таким образом, когда исключение можно легко избежать с помощью предварительной проверки, предпочтительно выполнять этот тест вместо вызова метода, в то время как мы уже можем точно знать, что вызов завершится неудачей.
Первоначально этот рефакторинг также был связан с медленными реализациями исключений. На самом деле, это просто простота: зачем создавать и перехватывать объекты для связи, когда выделенный вызов метода может сделать то же самое?
меры
В исходной ситуации клиентский код вызывает метод на объекте сервера.
- Вставьте условие для ошибочного случая: возможно, ему придется делегировать объекту сервера некоторую часть своей логики.
- Скопируйте код из блока catch в оператор if . Обычно это генерирует новое исключение или возвращает определенное значение.
- Теперь улов должен генерировать базовый объект Exception, чтобы проверить, что он не выполняется ни в одном из рассматриваемых случаев. Запустите тестовый набор.
- Удалите защелку и даже структуру try, если нет других исключений для управления.
пример
Этот рефакторинг редко встречается в PHP-функциях, которые все еще являются процедурными. Тем не менее, такие библиотеки, как Zend Framework, Symfony и Doctrine, имеют богатую иерархию исключений и могут побудить разработчика попытаться и поймать, а не пытаться полностью понять их API и поведение.
Однако мы будем использовать одну из немногих базовых объектно-ориентированных библиотек PHP, PDO, для переносимости этого примера.
Этот объект предназначен для того, чтобы скрыть PDO от остальной части приложения: как таковой, он также генерирует свое собственное исключение вместо того, чтобы заставить клиентский код зависеть от объектов PDOException и работать с ними. Я моделирую только фазу создания, опуская все методы, не относящиеся к этому примеру:
<?php class ReplaceExceptionWithTest extends PHPUnit_Framework_TestCase { /** * @expectedException DatabaseException */ public function testTheConnectionIsNotCreatedIfTheDriverIsNotSupported() { $database = new Database('somecloneofsqlite', ':memory:'); } } class Database { private $connection; public function __construct($driver, $restOfDsn) { try { $dsn = $driver . ':' . $restOfDsn; $this->connection = new PDO($dsn); } catch (PDOException $e) { throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn')."); } } } class DatabaseException extends Exception {}
Мы копируем содержимое catch, которое выбрасывает другое исключение, в предложение guard .
class Database { private $connection; private $supportedDrivers = array('sqlite', 'mysql'); public function __construct($driver, $restOfDsn) { $dsn = $driver . ':' . $restOfDsn; if (!in_array($driver, $this->supportedDrivers)) { throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn')."); } try { $this->connection = new PDO($dsn); } catch (PDOException $e) { throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn')."); } } }
Чтобы убедиться, что улов больше не достигнут, мы добавляем в него общее исключение. PHPUnit всегда будет сигнализировать об общем исключении как об ошибке, даже если мы добавим его в @expectedException.
public function __construct($driver, $restOfDsn) { $dsn = $driver . ':' . $restOfDsn; if (!in_array($driver, $this->supportedDrivers)) { throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn')."); } try { $this->connection = new PDO($dsn); } catch (PDOException $e) { throw Exception(); } }
Выполнение тестов подтверждает, что код catch является недоступным (конечно, для случаев, охватываемых нашим набором тестов):
[17:15:37][giorgio@Galen:~/Dropbox/practical-php-refactoring]$ phpunit ReplaceExceptionWithTest.php PHPUnit 3.6.4 by Sebastian Bergmann. . Time: 1 second, Memory: 2.50Mb OK (1 test, 1 assertion)
Теперь мы можем удалить блок try / catch, чтобы окончательно очистить код:
class Database { private $connection; private $supportedDrivers = array('sqlite', 'mysql'); public function __construct($driver, $restOfDsn) { $dsn = $driver . ':' . $restOfDsn; if (!in_array($driver, $this->supportedDrivers)) { throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn')."); } $this->connection = new PDO($dsn); } }
Другие ошибки в конфигурации могут вызвать исключение PDOException. Но полезно обнаружить, что некоторые объекты PDOException всплывают и покрыть их дополнительным тестом: альтернатива — продолжать вызывать DatabaseException для всех случаев, содержащих либо очень общее, либо неправильное сообщение об ошибке.