Рефакторинг: замена метода объектом

Представим себе такую ситуацию… Есть у нас метод (функция-член класса), в котором довольно много строк кода. И этот код использует локальные переменные таким образом, что невозможно применить прием группировки кода в отдельную функцию.

Решением этой проблемной ситуации может быть прием, который мы сегодня рассмотрим. Это замена метода объектом. Суть этого приема состоит в том, чтобы исходный метод сделать методом нового класса, а все локальные переменные исходного метода сделать свойствами нового класса. В итоге такой метод в новом классе далее можно декомпозировать на отдельные методы этого же класса.

Чтобы было понятнее, как всегда пример:

// метод класса Order...
function price() {
    $primaryBasePrice   = 0.0;
    $secondaryBasePrice = 0.0;
    $tertiaryBasePrice  = 0.0;
    // какие-то вычисления ...
}

Чтобы нам декомпозировать эту функцию, применим сначала наш прием:

class PriceCalculator {
    var $primaryBasePrice;
    var $secondaryBasePrice;
    var $tertiaryBasePrice;
    function compute() {
        // код исходного метода price…
    }
}

А в методе Order::price сделаем просто вызов нового метода:

function price() {
    $pc = new PriceCalculator($this);
    return $pc->compute();
}

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

Порядок применения нашего приема следующий:

  1. Создайте новый класс и назовите в честь метода, который вы хотите декомпозировать.
  2. Создайте в новом классе свойства, которые будут содержать локальные переменные метода и его параметры.
  3. Создайте конструктор в новом классе и передайте через него все параметры, которые используются методом, а также экземпляр класса, в котором этот метод находится (в примере выше я передавал $this).
  4. Дайте имя новому методу в новом классе. Например, «compute».
  5. Скопируйте тело исходного метода в метод compute. Используйте поле для экземпляра исходного класса в новом классе.
  6. Замените тело старого метода кодом, который создает экземпляр нового класса и вызывает метод compute этого нового класса.

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

Пример

Рассмотрим какой-то метод, который что-то делает, но что он делает, не важно. Главное - с помощью него показать принцип применения рассматриваемого сегодня приема рефакторинга.

// метод gamma класса Account
function gamma ($inputVal, $quantity, $yearToDate) {
    $importantValue1 = ($inputVal * $quantity) + $this->delta();
    $importantValue2 = ($inputVal * $yearToDate) + 100;
    if (($yearToDate - $importantValue1) > 100) {
        $importantValue2 -= 20;
    }
    $importantValue3 = $importantValue2 * 7;
    // что-то еще делается ...
    return $importantValue3 - 2 * $importantValue1;
}

Чтобы выделить этот метод в отдельный объект, я создаю новый класс, который называю Gamma. В новом классе я создаю для каждой локальной переменной и каждого параметра метода Account::gamma собственное свойство (поле), а также создаю поле для экземпляра класса Account:

class Gamma {
    var $_account; // поле для объекта Account
    var $inputVal;
    var $quantity;
    var $yearToDate;
    var $importantValue1;
    var $importantValue2;
    var $importantValue3;
    // ...
}

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

function Gamma ($source, $inputValArg, $quantityArg, $yearToDateArg) {
    $this->_account   = $source;
    $this->inputVal   = $inputValArg;
    $this->quantity   = $quantityArg;
    $this->yearToDate = $yearToDateArg;
}

Теперь мы можем переместить тело нашего исходного метода Account::gamma в новый - Gamma::compute:

function compute () {
    $this->importantValue1 = ($this->inputVal * $this->quantity) + $this->_account->delta();
    $this->importantValue2 = ($this->inputVal * $this->yearToDate) + 100;
    if (($this->yearToDate - $this->importantValue1) > 100) {
        $this->importantValue2 -= 20;
    }
    $this->importantValue3 = $this->importantValue2 * 7;
    // что-то еще делается ...
    return $this->importantValue3 - 2 * $this->importantValue1;
}

Заметьте, что мы здесь использовали поле _account, чтобы обратиться к классу Account исходного метода и вызвать метод delta.

Теперь заменяем код исходного метода Account::gamma таким образом, чтобы он создавал новый экземпляр класса и вызывал метод compute:

function gamma ($inputVal, $quantity, $yearToDate) {
    $gamma = new Gamma($this, $inputVal, $quantity, $yearToDate);
    return $gamma->compute();
}

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

function compute () {
    $this->importantValue1 = ($this->inputVal * $this->quantity) + $this->_account->delta();
    $this->importantValue2 = ($this->inputVal * $this->yearToDate) + 100;
    $this->importantThing();
    $this->importantValue3 = $this->importantValue2 * 7;
    // что-то еще делается ...
    return $this->importantValue3 - 2 * $this->importantValue1;
}
function importantThing() {
    if (($this->yearToDate - $this->importantValue1) > 100) {
        $this->importantValue2 -= 20;
    }
}

Лично мне этот прием нравится. Думаю, Вам понравится тоже, после того, как его поймете. Но если не поняли - прочитайте этот пост еще раз :)





Читайте также:



Один ответ на “Рефакторинг: замена метода объектом”

  1. Игорь

    Хороший пост, довольно удобный прием реффакторинга, хотя в своей практике пока еще не применял, но все еще впереди :)


© Copyright. . I-Novice. All Rights Reserved. Terms | Site Map