Мы согласны с тем, что полиморфизм исключает множество случаев, когда необходимы операторы 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;
}
}