Рефакторинг: перемещение метода
В прошлой статье из цикла рефакторинга про встраивание класса я писал, что расскажу о таких приемах, как «Перемещение метода» и «Перемещение поля». Сегодня время пришло написать об одном из них - о первом.
Иногда метод чаще использует функции другого класса (или наоборот - используется ими), чем своего собственного. Выход из такой ситуации здесь состоит в том, чтобы создать аналогичный метод в том классе, функции которого этот метод чаще использует.
Но этот прием не всегда удается провести. Если нет уверенности в его необходимости - то лучше не стоит, особенно в крупных проектах. Почему? Сами знаете
Порядок применения приема состоит в следующем.
Сначала нужно изучить все функции, которые используются исходным методом в исходном классе. Если какая-то функция используется только исходным методом, то ее тоже лучше переместить, а если она используется и другими методами, то нужно также проверить, нельзя ли переместить и их. Просто иногда проще переместить целую группу методов, чем перемещать их по одному.
Затем необходимо проверить, нет ли в родительских классах и подклассах исходного класса других объявлений исходного метода. Если есть, то перемещение становится невозможным из-за полиморфизма. Если отразить полиморфизм в целевом классе, то перемещение становится возможным.
Теперь нужно объявить перемещаемый метод в целевом классе. Можно даже выбрать для метода другое имя, которое будет лучше соответствовать целевому классу.
Затем копируем код исходного метода в целевой, после чего приспосабливаем скопированный код под новое место. Например, перемещаемому методу может потребоваться его исходный объект. В этом случае нужно как-то передать этот объект (например, через параметр). Если метод содержит обработчики исключительных ситуаций, то следует определить, какому из классов - целевому или исходному - логичнее будет обрабатывать exceptions.
Теперь можно сделать из исходного метода делегирующий, т.е. который не будет содержать код, а просто будет вызывать целевой метод. Можно оставить метод как делегирующий в исходном классе, а можно его удалить, предварительно заменив все обращения к нему обращениями к созданному в другом классе методу.
Пример
В качестве примера представим себе, что у нас есть программа управления банковскими счетами. Пусть она начисляет дополнительную плату владельцу счета за превышение срока кредита по определенному алгоритму:
class Account { // ... function overdraftCharge() { if ($this->_type->isPremium()) { $result = 10; if ($this->_daysOverdrawn > 7) { $result += ($this->_daysOverdrawn - 7) * 0.85; } return $result; } return $this->_daysOverdrawn * 1.75; } function bankCharge() { $result = 4.5; if ($this->_daysOverdrawn > 0) { $result += $this->overdraftCharge(); } return $result; } private $_type; // AccountType private $_daysOverdrawn; // ... }
А теперь рассмотрим такую ситуацию: нам нужно ввести несколько новых типов счетов со своими собственными правилами начислений за превышение срока кредита (овердрафт называется). Метод начисления у нас - это overdraftCharge. Его нужно перенести в класс AccountType.
Посмотрим, какие функции использует метод overdraftCharge, и решим, переносить ли нам один только метод или переносить всю группу. Заметим, что в нашем случае поле _daysOverdrawn (на сколько дней превышен срок кредита) должно остаться в классе Account, т.к. оно будет принимать разные значения у разных счетов.
Скопируем тело метода overdraftCharge в класс AccountType и подгоним его под требования нового класса:
class AccountType { // ... function overdraftCharge($daysOverdrawn) { if ($this->isPremium()) { $result = 10; if ($this->daysOverdrawn > 7) { $result += ($this->daysOverdrawn - 7) * 0.85; } return $result; } return $this->daysOverdrawn * 1.75; } // ... }
Теперь заменим метод overdraftCharge в классе Account так, чтобы он использовал код, созданный нами в классе AccountType, т.е. сделаем делегирующий метод:
function overdraftCharge() { return $this->_type->overdraftCharge($this->_daysOverdrawn); }
Можно оставить метод делегирующим, а можно удалить overdraftCharge из исходного класса Account. Но в этом случае конечно нужно заменить все соответствующие вызовы:
class Account { // ... function bankCharge() { $result = 4.5; if ($this->_daysOverdrawn > 0) { $result += $this->_type->overdraftCharge($this->_daysOverdrawn); } return $result; } // ... }
После замены во всех точках вызовов мы можем смело удалять объявление метода overdraftCharge из класса Account.
Заметьте, что я передал в метод overdraftCharge параметр _daysOverdrawn. Я смог это сделать, но если бы внутри overdraftCharge использовался какой-нибудь метод из класса Account, то мне бы пришлось передавать в качестве параметра объект класса Account:
class AccountType { // ... function overdraftCharge($account) { if ($this->isPremium()) { $result = 10; if ($account->getDaysOverdrawn() > 7) { $result += ($account->getDaysOverdrawn() - 7) * 0.85; } return $result; } return $account->getDaysOverdrawn() * 1.75; } // ... }
На этом позволю себе закончить В следующий раз поговорим о перемещении полей.