Мы согласны с тем, что полиморфизм исключает множество случаев, когда необходимы операторы if (). Однако, если у нас запутанная условная зависимость, лучше максимально упростить ее с помощью локальных вмешательств, прежде чем пытаться извлечь новые объекты .
Пример запутанных условных выражений на самом деле представляет собой группу вложенных:
if ($user->isAdmin()) { if ($this->closed) { $displayed = "Closed (not for you)"; } else if ($this->inEvidence) { $displayed = "Open and in evidence"; } else { $displayed = ""; } } else { if ($this->closed) { $displayed = "Closed"; } else { $displayed = ""; } }
Как код получает это плохо? Одна строка в то время. Это классический пример кода, где каждая строка добавляется в другой день, и никто не пытается ее улучшить. Если вы бросите лягушку в кипящую воду, она отскочит; если вы положите его в пресную воду и медленно нагреете, вы получите лягушачий суп .
Альтернатива
Альтернатива заключается в локальном извлечении угловых дел в форме охранных положений. В условных выражениях такого типа одна нога — это необычный случай (тогда блок), а другая — основной (который выполняет простое возвращение, предполагая, что условное выражение выделено в методе).
Охранные предложения лучше масштабируются для длинных условий , так как вы можете поместить много защитных предложений, которые покрывают много угловых случаев в начале, а затем продолжить как обычно.
Однако их распространение вызывает проблемы, как и для других условных выражений, но если вы посвящаете тестирование каждому угловому случаю, вы покрываете все пункты охраны. Задача пунктов охраны состоит в том, чтобы упростить проблемный код, в котором джунгли if / else / elseif будут общим результатом ; если они хранятся в листовых классах, они не могут дублироваться в разных местах кодовой базы.
Рефакторинг для защиты пунктов является еще одним примером упадка одной записи, одного правила выхода .
меры
Предварительный шаг требует, чтобы условное выражение было выделено в методе, возвращаемое значение которого должно быть единственным, что имеет значение для условного выполнения. Устраните побочные эффекты или другие назначения, где это возможно. Затем начните итеративный процесс.
- Для каждого угла случае , поставить пункт охраны в начале метода , который выполняет раннее возвращение с правильным результатом. Тесты все равно должны пройти.
- Проверьте тесты . Цель состоит в том, чтобы продолжать проводить «зеленый» тест, исключая при этом части кода для рефакторинга; пункты охраны делают это так, чтобы проблемные случаи не достигли основной части условных джунглей.
Последний случай с крышкой является основной , который должен быть из условных.
пример
В исходном состоянии у нас есть небольшие джунгли кода.
<?php class ReplaceNestedConditionalWithGuardClauses extends PHPUnit_Framework_TestCase { public function testTheOpenTopicTitleIsDisplayedNormally() { $topic = new Topic("Hello"); $this->assertEquals("Hello", $topic->__toString()); } public function testTheClosedTopicTitleIsDisplayedWithACorrespondingIndication() { $topic = new Topic("Hello", true); $this->assertEquals("Closed: Hello", $topic->__toString()); } public function testTheClosedTopicTitleIsDisplayedNormallyToAdmins() { $topic = new Topic("Hello", true, true); $this->assertEquals("Closed (not for you): Hello", $topic->__toString()); } } class Topic { private $title; private $isClosed; private $isAdminViewing; public function __construct($title, $isClosed = false, $isAdminViewing = false) { $this->title = $title; $this->isClosed = $isClosed; $this->isAdminViewing = $isAdminViewing; } public function __toString() { if (!$this->isClosed) { $displayed = $this->title; } else { if ($this->isAdminViewing) { $displayed = "Closed (not for you): $this->title"; } else { $displayed = "Closed: $this->title"; } } return $displayed; } }
Мы добавляем первый пункт охраны.
public function __toString() { if ($this->isClosed && $this->isAdminViewing) { return "Closed (not for you): $this->title"; } if (!$this->isClosed) { $displayed = $this->title; } else { if ($this->isAdminViewing) { } else { $displayed = "Closed: $this->title"; } } return $displayed; }
Мы добавляем второй и последний пункт охраны. Вот и все, поскольку у нас ровно на два теста больше основного.
public function __toString() { if ($this->isClosed && $this->isAdminViewing) { return "Closed (not for you): $this->title"; } if ($this->isClosed) { return "Closed: $this->title"; } if (!$this->isClosed) { $displayed = $this->title; } else { if ($this->isAdminViewing) { } else { } } return $displayed; }
Мы добавляем основной случай в качестве безусловного оператора возврата.
public function __toString() { if ($this->isClosed && $this->isAdminViewing) { return "Closed (not for you): $this->title"; } if ($this->isClosed) { return "Closed: $this->title"; } return $this->title; if (!$this->isClosed) { $displayed = $this->title; } else { if ($this->isAdminViewing) { } else { } } return $displayed; }
Теперь мы можем упростить код, так как его часть недоступна.
public function __toString() { if ($this->isClosed && $this->isAdminViewing) { return "Closed (not for you): $this->title"; } if ($this->isClosed) { return "Closed: $this->title"; } return $this->title; }
Теперь мы продолжим и попробуем полиморфное решение: чтобы исключить одну из двух осей изменения (закрытая или открытая тема), мы вводим объект TopicState.
Обратите внимание, что код разбит на разные меньшие объекты. Я реализую один из них с предложением guard, последнее if (), которое остается.
<?php class ReplaceNestedConditionalWithGuardClauses extends PHPUnit_Framework_TestCase { public function testTheOpenTopicTitleIsDisplayedNormally() { $topic = new Topic("Hello", new OpenTopicState); $this->assertEquals("Hello", $topic->__toString()); } public function testTheClosedTopicTitleIsDisplayedWithACorrespondingIndication() { $topic = new Topic("Hello", new ClosedTopicState); $this->assertEquals("Closed: Hello", $topic->__toString()); } public function testTheClosedTopicTitleIsDisplayedNormallyToAdmins() { $topic = new Topic("Hello", new ClosedTopicState, true); $this->assertEquals("Closed (not for you): Hello", $topic->__toString()); } } class Topic { private $title; private $isClosed; private $isAdminViewing; public function __construct($title, TopicState $isClosed, $isAdminViewing = false) { $this->title = $title; $this->isClosed = $isClosed; $this->isAdminViewing = $isAdminViewing; } public function __toString() { return $this->isClosed->topicCaption($this->isAdminViewing) . $this->title; } } interface TopicState { /** * @return string */ function topicCaption($isAdminViewing); } class OpenTopicState implements TopicState { function topicCaption($isAdminViewing) { return ''; } } class ClosedTopicState implements TopicState { function topicCaption($isAdminViewing) { if ($isAdminViewing) { return 'Closed (not for you): '; } return 'Closed: '; } }
Мы также переименовываем поле, чтобы отразить имя класса, чтобы завершить операцию.
class Topic { private $title; private $topicState; private $isAdminViewing; public function __construct($title, TopicState $topicState, $isAdminViewing = false) { $this->title = $title; $this->topicState = $topicState; $this->isAdminViewing = $isAdminViewing; } public function __toString() { return $this->topicState->topicCaption($this->isAdminViewing) . $this->title; } }