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