Часть кода содержится во всех ветвях условного выражения: очевидное решение состоит в том, чтобы переместить его наружу, чтобы упростить ветви. Может показаться глупым придумать код, дублирующий некоторые ветви, но часто это результат процесса преобразования существующего кода: он не пишется сразу, как это.
Более того, часто дублирование не очень хорошо видно, так как есть другие строки, смешанные с дублированными; или некоторые предположения, которые усложняют одну копию тех же строк.
Зачем устранять дубликаты фрагментов?
Этот рефакторинг является предварительным к возможному решению полиморфизма: чем меньше условные выражения, тем проще их транспортировать в других объектах.
Минимизируя условный код, вы получаете большую помощь в определении того, что должно меняться между различными объектами для создания; просто учтите, что каждая переменная, которую вы исключаете из {} условного блока, является одной ссылкой, которую вам не нужно передавать другому объекту, либо через конструктор, либо через параметр метода.
меры
Идентифицируйте дублированный код: он должен присутствовать во всех ветвях (тогда, иначе или еще, если присутствует). Возможно, вам придется добавить некоторые локальные переменные, чтобы разбить некоторые строки на их дублированные и специфические части.
Тогда альтернативы, представленные Фаулером:
- Дублированный код находится в начале условного блока: переместите его перед условным.
- Дублированный код находится в конце : переместите его после условного.
- Код находится в середине других утверждений . Вы должны попытаться переместить его вперед или назад, если это не приводит к функциональным изменениям результата. Часто позиция заявления не является строгой, поскольку она показывает независимость от соседних заявлений.
Обратите внимание, что весь код, исключенный из условных выражений, может быть извлечен в методе, если он длиннее нескольких строк.
пример
(Я не использую числа с фиксированной точностью, так как это должны быть деньги, но лучше оставить пример коротким)
Счет-фактура: добавьте фиксированную плату за обработку до уплаты налогов или, если есть скидка, добавьте эту плату вместе с ней
Не сразу понятно, что здесь есть некоторый дублирующий код:
<?php class ConsolidateDuplicateConditionalFragments extends PHPUnit_Framework_TestCase { public function testTotalPaymentIncludeTaxesAndProcessingFee() { $invoice = new Invoice(990, 21, false); $this->assertEquals(1210, $invoice->getTotal()); } public function testTotalCanBeDiscountedBeforeTaxes() { $invoice = new Invoice(1250, 21, 20); $this->assertEquals(1210, $invoice->getTotal()); } } class Invoice { private $taxable; private $taxRate; private $discount; const PROCESSING_FEE = 10; public function __construct($taxable, $taxRate, $discount = false) { $this->taxable = $taxable; $this->taxRate = $taxRate; $this->discount = $discount; } public function getTotal() { if ($this->discount) { $total = $this->taxable * (1 - $this->discount / 100); return $total * (1 + $this->taxRate / 100); } else { return ($this->taxable + self::PROCESSING_FEE) * (1 + $this->taxRate / 100); } } }
Но если мы посмотрим на логические шаги, так и должно быть: добавление налога к налогооблагаемой сумме не должно быть связано с наличием скидки. Давайте перепишем это:
class Invoice { private $taxable; private $taxRate; private $discount; const PROCESSING_FEE = 10; public function __construct($taxable, $taxRate, $discount = false) { $this->taxable = $taxable; $this->taxRate = $taxRate; $this->discount = $discount; } public function getTotal() { if ($this->discount) { $total = $this->taxable * (1 - $this->discount / 100); return $total * (1 + $this->taxRate / 100); } else { $total = $this->taxable + self::PROCESSING_FEE; return $total * (1 + $this->taxRate / 100); } } }
Теперь мы видим, что есть дублирование. Если бы мы сейчас должны были реорганизовать полиморфное решение, мы должны принять во внимание и то, как рассчитать налог. Давайте уберем налоговый код из условного.
class Invoice { private $taxable; private $taxRate; private $discount; const PROCESSING_FEE = 10; public function __construct($taxable, $taxRate, $discount = false) { $this->taxable = $taxable; $this->taxRate = $taxRate; $this->discount = $discount; } public function getTotal() { if ($this->discount) { $total = $this->taxable * (1 - $this->discount / 100); } else { $total = $this->taxable + self::PROCESSING_FEE; } return $total * (1 + $this->taxRate / 100); } }
Рефакторинг, рассмотренный в этой статье, завершен. Однако я продолжу показывать простое полиморфное решение.
Мы вводим объект Discount и перемещаем туда все, что связано с условием (все, что не связано, остается в классе curent, так как мы планируем сделать многократную реализацию скидки)
<?php class ConsolidateDuplicateConditionalFragments extends PHPUnit_Framework_TestCase { public function testTotalPaymentIncludeTaxesAndProcessingFee() { $invoice = new Invoice(990, 21, new Discount(false)); $this->assertEquals(1210, $invoice->getTotal()); } public function testTotalCanBeDiscountedBeforeTaxes() { $invoice = new Invoice(1250, 21, new Discount(20)); $this->assertEquals(1210, $invoice->getTotal()); } } class Discount { private $rate; const PROCESSING_FEE = 10; public function __construct($rate) { $this->rate = $rate; } public function discount($amount) { if ($this->rate) { return $amount * (1 - $this->rate / 100); } else { return $amount + self::PROCESSING_FEE; } } } class Invoice { private $taxable; private $taxRate; private $discount; public function __construct($taxable, $taxRate, Discount $discount) { $this->taxable = $taxable; $this->taxRate = $taxRate; $this->discount = $discount; } public function getTotal() { $total = $this->discount->discount($this->taxable); return $total * (1 + $this->taxRate / 100); } }
Теперь у нас есть маленький объект с двумя различными режимами поведения в зависимости от его состояния. Мы должны разделить логику на два класса:
<?php class ConsolidateDuplicateConditionalFragments extends PHPUnit_Framework_TestCase { public function testTotalPaymentIncludeTaxesAndProcessingFee() { $invoice = new Invoice(990, 21, new ProcessingFee); $this->assertEquals(1210, $invoice->getTotal()); } public function testTotalCanBeDiscountedBeforeTaxes() { $invoice = new Invoice(1250, 21, new PercentageDiscount(20)); $this->assertEquals(1210, $invoice->getTotal()); } } interface Discount { public function discount($amount); } class PercentageDiscount implements Discount { private $rate; public function __construct($rate) { $this->rate = $rate; } public function discount($amount) { return $amount * (1 - $this->rate / 100); } } class ProcessingFee implements Discount { const PROCESSING_FEE = 10; public function discount($amount) { return $amount + self::PROCESSING_FEE; } } class Invoice { private $taxable; private $taxRate; private $discount; public function __construct($taxable, $taxRate, Discount $discount) { $this->taxable = $taxable; $this->taxRate = $taxRate; $this->discount = $discount; } public function getTotal() { $total = $this->discount->discount($this->taxable); return $total * (1 + $this->taxRate / 100); } }
Подождите, теперь есть скидка, которая фактически добавляет деньги к сумме, подлежащей выплате. Лучше назвать его по-другому, как для класса, так и для метода:
interface PaymentModifier { public function applyOn($amount); } class PercentageDiscount implements PaymentModifier { private $rate; public function __construct($rate) { $this->rate = $rate; } public function applyOn($amount) { return $amount * (1 - $this->rate / 100); } } class ProcessingFee implements PaymentModifier { const PROCESSING_FEE = 10; public function applyOn($amount) { return $amount + self::PROCESSING_FEE; } } class Invoice { private $taxable; private $taxRate; private $paymentModifier; public function __construct($taxable, $taxRate, PaymentModifier $paymentModifier) { $this->taxable = $taxable; $this->taxRate = $taxRate; $this->paymentModifier = $paymentModifier; } public function getTotal() { $total = $this->paymentModifier->applyOn($this->taxable); return $total * (1 + $this->taxRate / 100); } }