Рефакторинг: замена метода объектом
Представим себе такую ситуацию… Есть у нас метод (функция-член класса), в котором довольно много строк кода. И этот код использует локальные переменные таким образом, что невозможно применить прием группировки кода в отдельную функцию.
Решением этой проблемной ситуации может быть прием, который мы сегодня рассмотрим. Это замена метода объектом. Суть этого приема состоит в том, чтобы исходный метод сделать методом нового класса, а все локальные переменные исходного метода сделать свойствами нового класса. В итоге такой метод в новом классе далее можно декомпозировать на отдельные методы этого же класса.
Чтобы было понятнее, как всегда пример:
// метод класса 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(); }
Этот прием был придуман из-за сложной декомпозиции больших методов, в которых много локальных переменных. Можно конечно использовать прием выделения временной переменной в метод, но он может не дать столь эффективного результата, сколько дает наш сегодняшний прием, который позволяет убрать все локальные переменные в свойства класса и далее уже ничто не мешает нам декомпозировать наш метод так, как мы хотим, используя прием группировки кода в отдельную функцию.
Порядок применения нашего приема следующий:
- Создайте новый класс и назовите в честь метода, который вы хотите декомпозировать.
- Создайте в новом классе свойства, которые будут содержать локальные переменные метода и его параметры.
- Создайте конструктор в новом классе и передайте через него все параметры, которые используются методом, а также экземпляр класса, в котором этот метод находится (в примере выше я передавал $this).
- Дайте имя новому методу в новом классе. Например, «compute».
- Скопируйте тело исходного метода в метод compute. Используйте поле для экземпляра исходного класса в новом классе.
- Замените тело старого метода кодом, который создает экземпляр нового класса и вызывает метод 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; } }
Лично мне этот прием нравится. Думаю, Вам понравится тоже, после того, как его поймете. Но если не поняли - прочитайте этот пост еще раз
Хороший пост, довольно удобный прием реффакторинга, хотя в своей практике пока еще не применял, но все еще впереди