Статьи

Практический рефакторинг PHP: объединение дублирующихся условных фрагментов

Часть кода содержится во всех ветвях условного выражения: очевидное решение состоит в том, чтобы переместить его наружу, чтобы упростить ветви. Может показаться глупым придумать код, дублирующий некоторые ветви, но часто это результат процесса преобразования существующего кода: он не пишется сразу, как это.

Более того, часто дублирование не очень хорошо видно, так как есть другие строки, смешанные с дублированными; или некоторые предположения, которые усложняют одну копию тех же строк.

Зачем устранять дубликаты фрагментов?

Этот рефакторинг является предварительным к возможному решению полиморфизма: чем меньше условные выражения, тем проще их транспортировать в других объектах.

Минимизируя условный код, вы получаете большую помощь в определении того, что должно меняться между различными объектами для создания; просто учтите, что каждая переменная, которую вы исключаете из {} условного блока, является одной ссылкой, которую вам не нужно передавать другому объекту, либо через конструктор, либо через параметр метода.

меры

Идентифицируйте дублированный код: он должен присутствовать во всех ветвях (тогда, иначе или еще, если присутствует). Возможно, вам придется добавить некоторые локальные переменные, чтобы разбить некоторые строки на их дублированные и специфические части.

Тогда альтернативы, представленные Фаулером:

  • Дублированный код находится в начале условного блока: переместите его перед условным.
  • Дублированный код находится в конце : переместите его после условного.
  • Код находится в середине других утверждений . Вы должны попытаться переместить его вперед или назад, если это не приводит к функциональным изменениям результата. Часто позиция заявления не является строгой, поскольку она показывает независимость от соседних заявлений.

Обратите внимание, что весь код, исключенный из условных выражений, может быть извлечен в методе, если он длиннее нескольких строк.

пример

(Я не использую числа с фиксированной точностью, так как это должны быть деньги, но лучше оставить пример коротким)
Счет-фактура: добавьте фиксированную плату за обработку до уплаты налогов или, если есть скидка, добавьте эту плату вместе с ней

Не сразу понятно, что здесь есть некоторый дублирующий код:

<?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);
    }
}