Статьи

Практический рефакторинг PHP: замените параметр явными методами

Рефакторинг сегодня разбивает метод на несколько: он разбивает метод на части в соответствии с кодом, который выполняется в ответ на один из параметров. Этот параметр может принимать только несколько значений или несколько эквивалентных классов значений; каждый выбор приводит к новой версии метода.

Как правило, параметр является логическим , и два результата могут быть предоставлены согласно его значению. Также Value Objects, обертывающие целые числа и фиксированные строки, попадают в одну категорию (конечный набор значений)

Этот рефакторинг является обратным к параметризации метода , который принимает несколько версий метода и объединяет их, добавляя отличительный параметр к вызовам.

Зачем делить метод на несколько версий?

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

Преимущества нескольких методов очевидны, когда в исходном методе найдены switch () или if () для параметра . Вы можете избежать условного поведения, которое выполняет сильно отличающийся код в зависимости от значения параметра ключа.

Интерфейс объекта становится также ясным , как у вас есть набор открытых методов , которые можно назвать хорошо, а не догоняющего всякого имени , как процесс ($ ActionType, …).
Наконец, этот рефакторинг сокращает список параметров (удаляя один из аргументов) и тело метода, выбирая только одну ветвь условных операторов для каждого нового метода.

Когда его не использовать?

Фаулер указывает в своей книге « Рефакторинг», что вам не следует вводить явные методы, когда вы думаете, что параметры изменятся новыми, неожиданными способами . Несколько реализаций объекта параметра легко добавить, в то время как несколько версий метода могут нарушить API исходного объекта и вынудить grep весь клиентский код отрегулировать вызовы.

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

меры

  1. Создайте явные методы для каждого из путей кода, на которые вы хотите разбить метод. Например, если у вас есть переключатель с 3 случаями, вы создадите 3 явных метода.
  2. Делегируйте эти методы изнутри старого монолитного. Этот шаг сокращает исходный метод и гарантирует, что вы ничего не забудете.
  3. Измените вызовы кода клиента, чтобы использовать новые методы.
  4. Удалите оригинальный метод, когда закончите.

Тесты должны проходить между каждым шагом: нет причин нарушать код при применении этого рефакторинга.

пример

Пример основан на контексте форума: потоки являются объектами домена и не связаны с многопоточностью.

В объекте Thread у нас есть установщик для логического параметра. Также есть добытчик. Открытое поле имеет значение false или true в зависимости от состояния потока.
Сеттер обновляет вычисляемое поле по-разному, основываясь на булевом параметре: мы хотим устранить условное условие и предоставить клиентскому коду более точный API-интерфейс.

<?php
class ReplaceParameterWithExplicitMethods extends PHPUnit_Framework_TestCase
{
    public function testThreadCanBeClosed()
    {
        $thread = new Thread('Ubuntu on EEE Pc');
        $thread->setOpen(false);
        $this->assertFalse($thread->getOpen());
        $this->assertEquals('[closed] Ubuntu on EEE Pc', $thread->__toString());
    }

    public function testThreadCanBeOpened()
    {
        $thread = new Thread('Ubuntu on EEE Pc');
        $thread->setOpen(true);
        $this->assertTrue($thread->getOpen());
        $this->assertEquals('Ubuntu on EEE Pc', $thread->__toString());
    }
}

class Thread
{
    private $title;
    private $open;

    public function __construct($title)
    {
        $this->title = $title;
    }

    public function setOpen($state)
    {
        $this->open = $state;
        if (!$this->open) {
            $this->label = '[closed] ' . $this->title;
        } else {
            $this->label = $this->title;
        }
    }

    public function getOpen()
    {
        return $this->open;
    }

    public function __toString()
    {
        return $this->label;
    }
}

Мы преобразуем setter: close () и open () — лучшие имена для методов, которые делают больше, чем простое присваивание полю. Геттер не содержит разных ветвей исполнения.

class Thread
{
    private $title;
    private $open;

    public function __construct($title)
    {
        $this->title = $title;
    }

    public function setOpen($state)
    {
        $this->open = $state;
        if (!$this->open) {
            $this->label = '[closed] ' . $this->title;
        } else {
            $this->label = $this->title;
        }
    }

    public function open()
    {
        $this->open = true;
        $this->label = $this->title;
    }

    public function close()
    {
        $this->open = false;
        $this->label = '[closed] ' . $this->title;
    }

    public function getOpen()
    {
        return $this->open;
    }

    public function __toString()
    {
        return $this->label;
    }
}

Мы называем новые методы изнутри условным. Старый метод действительно некрасиво видеть сейчас, но он исчезнет через мгновение.

class Thread
{
    private $title;
    private $open;

    public function __construct($title)
    {
        $this->title = $title;
    }

    public function setOpen($open)
    {
        if (!$open) {
            $this->close();
        } else {
            $this->open();
        }
    }

    public function open()
    {
        $this->open = true;
        $this->label = $this->title;
    }

    public function close()
    {
        $this->open = false;
        $this->label = '[closed] ' . $this->title;
    }

    public function getOpen()
    {
        return $this->open;
    }

    public function __toString()
    {
        return $this->label;
    }
}

Тесты все еще проходят.

[giorgio@Desmond:practical-php-refactoring]$ phpunit ReplaceParameterWithExplicitMethods.php
PHPUnit 3.5.15 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 3.50Mb

OK (2 tests, 4 assertions)

Теперь мы можем изменить вызовы из клиентского кода (только тесты в этом примере).

<?php
class ReplaceParameterWithExplicitMethods extends PHPUnit_Framework_TestCase
{
    public function testThreadCanBeClosed()
    {
        $thread = new Thread('Ubuntu on EEE Pc');
        $thread->close();
        $this->assertFalse($thread->getOpen());
        $this->assertEquals('[closed] Ubuntu on EEE Pc', $thread->__toString());
    }

    public function testThreadCanBeOpened()
    {
        $thread = new Thread('Ubuntu on EEE Pc');
        $thread->open();
        $this->assertTrue($thread->getOpen());
        $this->assertEquals('Ubuntu on EEE Pc', $thread->__toString());
    }
}

Наконец, мы можем удалить старую версию метода, которая больше не достигается ни одним вызовом.

class Thread
{
    private $title;
    private $open;

    public function __construct($title)
    {
        $this->title = $title;
    }

    public function open()
    {
        $this->open = true;
        $this->label = $this->title;
    }

    public function close()
    {
        $this->open = false;
        $this->label = '[closed] ' . $this->title;
    }

    public function getOpen()
    {
        return $this->open;
    }

    public function __toString()
    {
        return $this->label;
    }
}