Это вторая часть в рефакторинге из мини- серии кодов типов : коды типов — это скалярные поля, которые могут принимать конечное число значений.
Сегодня предполагается , что код типа влияет на поведение класса : в зависимости от значения поля выполняется другой код. Как правило, код выбирается через if () или другую управляющую структуру, такую как select () или ?: . Любой код, который проверяет значение кода типа, является подозрительным.
На этот раз мы не можем выполнить рефакторинг путем извлечения одного класса, потому что мы просто переместим if в этот класс, который должен был бы охватить все различные случаи.
Поэтому мы пробуем решение наследования: мы создаем разные подклассы исходного, а не извлекаем меньший класс. Поскольку поведение зависит от поля, во время построения мы должны знать, какое значение находится в поле кода типа, и мы можем решить, какой класс создать. Оригинальный класс может стать абстрактным в процессе.
Почему подкласс?
Этот рефакторинг предпочитает полиморфизм структурам управления: он соблюдает принцип единой ответственности , разделяя код, включаемый различными значениями кода типа.
В следующий раз, когда вы добавите новое значение кода типа, вы добавите класс, не касаясь уже существующих ( Open Closed Principle ).
Более того, вы создаете один подкласс для каждого кода типа, выражая концепцию с помощью языковой конструкции, а не с разными значениями строки или целого числа.
Когда вы не можете применить этот рефакторинг
Фаулер приводит некоторые случаи, когда этот рефакторинг не может быть применен (но не отчаивайтесь: есть альтернативы.)
Простой случай, когда код типа меняется очень часто . Тем не менее, я обнаружил, что если он изменяется из-за редкого перехода состояния, вы все равно можете использовать решение наследования, заставляя инкриминируемый метод возвращать новый экземпляр (например, InactiveUser :: activ () возвращает экземпляр ActiveUser). Это сложнее сделать, когда жизненный цикл объектов управляется не только сборкой мусора, но и внешними ресурсами, такими как база данных, доступ к которой осуществляется через ORM (необходимо, чтобы объекты представляли одну и ту же строку / документ / блоб и проверяли висячие ссылки .)
Другой неподходящий сценарий возникает, когда иерархия наследования уже существует : наследование — это решение, которое вы можете использовать для управления только одной осью изменения модели. В этом случае вам придется искать композиционное решение, которое мы увидим в следующей статье этой серии.
меры
- Прежде всего, само кодирование должно выполняться в коде типа , что приводит к защищенному методу getTypeCode ().
- Для каждого значения кода типа должен быть создан подкласс , который переопределяет метод getTypeCode () жестко закодированным.
- Далее мы должны заняться созданием : если код типа передается в конструктор, код создания должен быть перемещен в метод Factory, который может вернуть экземпляр правого подкласса. Единственные операторы if () останутся здесь пока.
- На этом этапе тесты должны быть зелеными. Логика все еще запутана в исходном классе, но различные подклассы решают, какой тип кода указать.
- Теперь мы можем удалить поле кода типа ; getTypeCode () может стать абстрактным, так как все подклассы предоставят его.
- Проверьте набор тестов еще раз.
Теперь вы можете перемещать логику в подклассы каждый раз, когда она специфична для определенного значения кода типа. В идеальном случае вы также сможете удалить метод getTypeCode (), когда закончите.
пример
Мы начинаем с той же пользовательской специализации предыдущего примера; однако на этот раз результат __toString () зависит от значения кода типа.
<?php class ReplaceTypeCodeWithSubclasses extends PHPUnit_Framework_TestCase { public function testAnUserCanBeANewbie() { $user = User::newUser("Giorgio", User::NEWBIE); $this->assertEquals("Giorgio", $user->__toString()); } public function testAnUserCanBeRegardedAsAGuru() { $user = User::newUser("Giorgio", User::GURU); $this->assertEquals("ADMIN: Giorgio", $user->__toString()); } } class User { const NEWBIE = 'N'; const GURU = 'G'; protected $name; public function __construct($name) { $this->name = $name; } public static function newUser($name, $rank) { if ($rank == self::GURU) { return new Guru($name); } return new Newbie($name); } protected function getRank() { return $this->rank; } } class Guru extends User { protected function getRank() { return self::GURU; } public function __toString() { return "ADMIN: $this->name"; } } class Newbie extends User { protected function getRank() { return self::NEWBIE; } public function __toString() { return $this->name; } }
Во-первых, мы самостоятельно инкапсулируем код типа с помощью getRank ():
class User { const NEWBIE = 'N'; const GURU = 'G'; private $name; private $rank; public function __construct($name, $rank) { $this->name = $name; $this->rank = $rank; } protected function getRank() { return $this->rank; } public function __toString() { if ($this->getRank() == self::GURU) { return "ADMIN: $this->name"; } // self::NEWBIE return $this->name; } }
Затем мы добавляем два подкласса Newbie и Guru, которые переопределяют getRank (). Мы должны изменить процесс создания с конструктора на фабричный метод, который будет централизовать ifs в одном месте.
Мы также соответствующим образом модифицируем тестовый код, вызывая метод Factory.
<?php class ReplaceTypeCodeWithSubclasses extends PHPUnit_Framework_TestCase { public function testAnUserCanBeANewbie() { $user = User::newUser("Giorgio", User::NEWBIE); $this->assertEquals("Giorgio", $user->__toString()); } public function testAnUserCanBeRegardedAsAGuru() { $user = User::newUser("Giorgio", User::GURU); $this->assertEquals("ADMIN: Giorgio", $user->__toString()); } } class User { const NEWBIE = 'N'; const GURU = 'G'; protected $name; private $rank; public function __construct($name, $rank) { $this->name = $name; $this->rank = $rank; } public static function newUser($name, $rank) { if ($rank == self::GURU) { return new Guru($name, null); } return new Newbie($name, null); } protected function getRank() { return $this->rank; } public function __toString() { if ($this->getRank() == self::GURU) { return "ADMIN: $this->name"; } // self::NEWBIE return $this->name; } } class Guru extends User { protected function getRank() { return self::GURU; } } class Newbie extends User { protected function getRank() { return self::NEWBIE; } }
Поскольку тест пройден, мы можем удалить код типа и упростить. Мы перемещаем логику, зависящую от старого кода, в два подкласса.
<?php class ReplaceTypeCodeWithSubclasses extends PHPUnit_Framework_TestCase { public function testAnUserCanBeANewbie() { $user = User::newUser("Giorgio", User::NEWBIE); $this->assertEquals("Giorgio", $user->__toString()); } public function testAnUserCanBeRegardedAsAGuru() { $user = User::newUser("Giorgio", User::GURU); $this->assertEquals("ADMIN: Giorgio", $user->__toString()); } } class User { const NEWBIE = 'N'; const GURU = 'G'; protected $name; public function __construct($name) { $this->name = $name; } public static function newUser($name, $rank) { if ($rank == self::GURU) { return new Guru($name); } return new Newbie($name); } protected function getRank() { return $this->rank; } } class Guru extends User { protected function getRank() { return self::GURU; } public function __toString() { return "ADMIN: $this->name"; } } class Newbie extends User { protected function getRank() { return self::NEWBIE; } public function __toString() { return $this->name; } }
Если код не нужен для отображения, мы также можем удалить getRank (), как только вся логика подкласса будет перемещена вниз в иерархии.