Refactor 重構 - fantasy0107/notes GitHub Wiki
https://sourcemaking.com/refactoring/refactorings
整合方法 - Composing methods
Extract Method
使用時機 : 當一個 method 太大時使用, 一個方法應該要確保做一件事情而且是做得很好,所以有太多程式瑪在一個方法時就應該將它拆成別的method
問題
可以把片段code整合再一起
function printOwing() {
$this->printBanner();
// Print details.
print("name: " . $this->name);
print("amount " . $this->getOutstanding());
}
解法
將 code 放到新method 並且將舊有的地方用新的方法呼叫
function printOwing() {
$this->printBanner();
$this->printDetails($this->getOutstanding());
}
function printDetails($outstanding) {
print("name: " . $this->name);
print("amount " . $outstanding);
}
為何重構
一個method中有太多code, 越難知道這個方法做啥
有益的地方
- 更多可讀性的code
- 更少的 code duplication
- 區隔開來的code錯誤也會比較少
如何重構
- 建立新method, 且確定method 名稱是有意義的
- 將舊的code複製到新method, 刪除舊有的code並且用新的呼叫方式, 找出所有variable的宣告並且將這些變數變成local variable
- 將需要的parameter從外部傳進來
- 如果看到區域變數有改變在舊的code 裡, 這表示可能需要將值回傳給main method
Inline Method
使用時機 : 當程式碼所代表的事情比 method name本身還清楚時
問題
當 method body 比 mehtod 名稱更能表示意思的時候
function getRating() {
return ($this->moreThanFiveLateDeliveries()) ? 2 : 1;
}
function moreThanFiveLateDeliveries() {
return $this->numberOfLateDeliveries > 5;
}
解法
用 method body 的 code 去取代 method 呼叫的地方
function getRating() {
return ($this->numberOfLateDeliveries > 5) ? 2 : 1;
}
為何重構
通常 method 的 code 不會太短, 所以有太多類似的 method 會造成困惑而且很難去整理它們
有益的地方
當減少了不需要的method, 你的 code 會更直觀
如何重構
- 確定 method 沒有重新定義在 subclass, 如果method重新定義避這種狀況
- 找到所有呼叫這個方法的的地方, 用 method content 去取代
- 刪除method
Extract Variable
使用時機 : 當條件式本身太難閱讀時, 可以用變數替代
問題
你有一個 expression 很難了解
if (($platform->toUpperCase()->indexOf("MAC") > -1) &&
($browser->toUpperCase()->indexOf("IE") > -1) &&
$this->wasInitialized() && $this->resize > 0)
{
// do something
}
解法
將 expression 用變數的方式去表示它的意思
$isMacOs = $platform->toUpperCase()->indexOf("MAC") > -1;
$isIE = $browser->toUpperCase()->indexOf("IE") > -1;
$wasResized = $this->resize > 0;
if ($isMacOs && $isIE && $this->wasInitialized() && $wasResized) {
// do something
}
為何重構
讓複雜的 expression 更好理解, 藉由分解成不同片段
- if () / ?:
- 很長的數學運算且是沒有中介部分
- 很長很多行
有益的地方
增加可讀性, 嘗試讓這些變數代表要表示的意思
如何重構
- 新增新的一列去區隔相對的expression, 定義新的 variable 並且去 assign 複雜的 expression
- 取代部份的expression 用新的變數
- 重複上述步驟
Inline Temp
使用時機 : 使用到暫存的變數時
問題
你有一個暫時的變數是用來 assign 給 return 而且沒有其他用處
$basePrice = $anOrder->basePrice();
return $basePrice > 1000;
解法
用它原本的expression 取代變數
return $anOrder->basePrice() > 1000;
為何重構
區域變數經常是用來當作 Replace Temp with Query 的一部分 或者給 extract method
有益的地方
沒有特殊的益處, 但如果是接受 method 的回傳那可以增加可讀性
如何重構
找出所有使用這個變數的地方. 並且用 expression 取代
Replace Temp with Query
使用時機 : 物件內儲存的變數需要互相做運算
問題
某個變數是存一個 expression 的結果
$basePrice = $this->quantity * $this->itemPrice;
if ($basePrice > 1000) {
return $basePrice * 0.95;
} else {
return $basePrice * 0.98;
}
解法
將整個 expression 寫成一個 method 並且回傳結果
if ($this->basePrice() > 1000) {
return $this->basePrice() * 0.95;
} else {
return $this->basePrice() * 0.98;
}
...
function basePrice() {
return $this->quantity * $this->itemPrice;
}
為何重構
因為這個 method 可能會在別的地方被發現. 所以寫在一個地方比較好共用
有益的地方
- 可讀性. getTax() 比 orderPrice() * 0.2 還好讀懂
- 同樣的 code 是不好的
如何重構
- 確定這個值是只有被assign 給變數一次, 使用 Split Temporary Variable 確定變數只有被使用來存取 expression 的結果
- 使用 Extract Method 去將 expression 放在一個新 method 裡. 確保這個 method 只有回傳值並且沒有改變 object 的 state
- 用新 method 取代變數
Split Temporary Variable
使用時機 : 使用同一個暫存變數存取不同種類的值
問題
你有一個區域變數是被用來暫時儲存值
$temp = 2 * ($this->height + $this->width);
echo $temp;
$temp = $this->height * $this->width;
echo $temp;
解法
用不同的變數儲存不同的值. 每個變數應該負責一個值
$perimeter = 2 * ($this->height + $this->width);
echo $perimeter;
$area = $this->height * $this->width;
echo $area;
為何重構
當你需要改變變數. 你會必須要重複檢查去確保正確的值是被使用的
有益的地方
- 程式碼應該只負責一件事情而且只有一件. 這會讓 maintain 更簡單
- 增加可讀性
如何重構
- 找出變數名稱並且給予它正確的名稱(跟數值相關的名稱)
- 用新名字取代
- 重複上述步驟直到每個值都有它特殊的變數名
Remove Assignments to Parameters
使用時機 : 方法輸入的變數值會發生改變時
問題
直接用傳進來的參數
function discount($inputVal, $quantity) {
if ($inputVal > 50) {
$inputVal -= 2;
}
...
}
解法
給傳進來的參數一個變數
function discount($inputVal, $quantity) {
$result = $inputVal;
if ($inputVal > 50) {
$result -= 2;
}
...
為何重構
- 假設傳進來的是 reference, 那麼當參數值一改變在外面的也會變
- 當一個變數被很多次的 assign 那麼繼很難知道這個變數是在儲存甚麼值
有益的地方
- 程式中的每個 element 應該就只負責一件事情
如何重構
- 建立一個 local variable, 然後 assign 你的parameter 變數值給區域變數
- 所有的方法都用這種方式, 去改變
Replace Method with Method Object
使用時機 : 一個物件裡面有太多區域變數
問題
你有一個很長的 method 裡面包含了很多變數
class Order {
// ...
public function price() {
$primaryBasePrice = 10;
$secondaryBasePrice = 20;
$tertiaryBasePrice = 30;
// Perform long computation.
}
}
解法
將這個方法裡的變數, 變成一個 class 的 property, 竟且將相關的方法包進這個 class 裡
class Order {
// ...
public function price() {
return (new PriceCalculator($this))->compute();
}
}
class PriceCalculator {
private $primaryBasePrice;
private $secondaryBasePrice;
private $tertiaryBasePrice;
public function __construct(Order $order) {
// Copy relevant information from the
// order object.
}
public function compute() {
// Perform long computation.
}
}
為何重構
一個過長的 method 且你無法分割因為眾多的區域變數
有益的地方
分離一個過長的方法是避免方法過大的手段
如何重構
- 建立新的 class, 根據那些變數去命名 class
- 在新的 class 建立 private field, 給每個 local variable
- 建立 constructor, 去初始化 private field 值
- 宣告主要的方法且複製原始的方法 code 且用 private filed 去替換
- 將原本 class 中的方法用已建立新物件去拿值
Substitute Algorithm
使用時機 : 當 method 可以用比較少行程式碼寫成同樣效果時
問題
想要用新的演算法
function foundPerson(array $people){
for ($i = 0; $i < count($people); $i++) {
if ($people[$i] === "Don") {
return "Don";
}
if ($people[$i] === "John") {
return "John";
}
if ($people[$i] === "Kent") {
return "Kent";
}
}
return "";
}
解法
修改 method 的 body 且結果是和之前的寫的方法一樣
function foundPerson(array $people){
foreach (["Don", "John", "Kent"] as $needle) {
$id = array_search($needle, $people, true);
if ($id !== false) {
return $people[$id];
}
}
return "";
}
為何重構
- 隨著時間的演進, method 可能變成不適合的
- 需求可能改變
如何重構
- 確定你有簡化方法
- 將舊的寫法改寫並且測試
- 假設結果不符合舊的寫法, 那就先復原看看差距在哪
- 假設所有的測試都通過那就刪除舊的換新的
簡化條件描述式 - Simplifying Conditional Expressions
Decompose Conditional
使用時機 : 條件描述的太複雜時, 用 method 包裝
問題
複雜的條件
if ($date->before(SUMMER_START) || $date->after(SUMMER_END)) {
$charge = $quantity * $winterRate + $winterServiceCharge;
} else {
$charge = $quantity * $summerRate;
}
解法
用 method 的方式去計算條件
if (isSummer($date)) {
$charge = summerCharge($quantity);
} else {
$charge = winterCharge($quantity);
}
為何重構
- 過長的程式碼片段很難理解
有益的地方
- 很好容易了解這個條件在做啥
如何重構
- 將條件變成一個 method 回傳
Consolidate Conditional Expression
使用時機 : method 結果相同時, 將各個條件包裝成一個 method
問題
有太多的條件是回傳同一個結果
function disabilityAmount() {
if ($this->seniority < 2) {
return 0;
}
if ($this->monthsDisabled > 12) {
return 0;
}
if ($this->isPartTime) {
return 0;
}
// compute the disability amount
...
解法
將所有的condition 變成一個 expression
function disabilityAmount() {
if ($this->isNotEligableForDisability()) {
return 0;
}
// compute the disability amount
...
為何重構
- 不夠簡潔
有益的地方
- 刪除重複的流程控制 1.分離複雜的expression在新的方法可以用名字表現出他的意思
如何重構
- 先確定這樣做不會有任何的副作用
- 用 and 和 or 去表示
- 用 Extract Method 去重構
Consolidate Duplicate Conditional Fragments
使用時機 : 在多個條件判斷式中有呼叫相同的 method時
問題
有同樣的 code 出現在相關的 condition 底下
if (isSpecialDeal()) {
$total = $price * 0.95;
send();
} else {
$total = $price * 0.98;
send();
}
解法
將code 移動到 condition 外面
if (isSpecialDeal()) {
$total = $price * 0.95;
} else {
$total = $price * 0.98;
}
send();
為何重構
- 重複的 code 是邪惡的
有益的地方
- 簡化code
如何重構
- 假設重複的 code 是在 condition 的開頭將code 移動的 condition 之前
- 在結果, 就放在 condition 之後
- 假設是隨機的地方先嘗試將code 移動到開頭或結尾, 根據code
- 假設超過一行就用 extract method 去重構
Remove Control Flag
使用時機 : 當有一個 control flag 對很多 boolean 表示式時
問題
有一個變數用來當作 control flag
解法
用 break, contine, return 去取代變數
為何重構
- 現在有特殊的operator 可以修正這樣的結果
有益的地方
- 不合時宜
如何重構
- 移除 control flag 相關的 code
- 用 break, continue, return 去取代
Replace Nested Conditional with Guard Clauses
使用時機 : 巢狀迴圈太過多層且必須要回傳數值時
問題
有一堆巢狀的條件
function getPayAmount() {
if ($this->isDead) {
$result = $this->deadAmount();
} else {
if ($this->isSeparated) {
$result = $this->separatedAmount();
} else {
if ($this->isRetired) {
$result = $this->retiredAmount();
} else {
$result = $this->normalPayAmount();
}
}
}
return $result;
}
解法
- 區隔所有的 condition 並且將複雜的 condition 扁平化
function getPayAmount() {
if ($this->isDead) {
return $this->deadAmount();
}
if ($this->isSeparated) {
return $this->separatedAmount();
}
if ($this->isRetired) {
return $this->retiredAmount();
}
return $this->normalPayAmount();
}
為何重構
太過巢狀的 if, else 實在是很難閱讀和知道它要做什麼
有益的地方
如何重構
- 嘗試丟棄code 副作用
- 分離所有的 condition 之後呼叫 exception 或直接回傳值
- 確定修改後所有的測試都有通過
Replace Conditional with Polymorphism
使用時機 : 當有條件式必須要根據物件型態時
問題
- 你有一個條件的表現方式是根據物件類型或 properties
class Bird {
// ...
public function getSpeed() {
switch ($this->type) {
case EUROPEAN:
return $this->getBaseSpeed();
case AFRICAN:
return $this->getBaseSpeed() - $this->getLoadFactor() * $this->numberOfCoconuts;
case NORWEGIAN_BLUE:
return ($this->isNailed) ? 0 : $this->getBaseSpeed($this->voltage);
}
throw new Exception("Should be unreachable");
}
// ...
}
解法
建立子class 去符合所有條件, 建立個 shared的 method 將code移動到適合的 branch
abstract class Bird {
// ...
abstract function getSpeed();
// ...
}
class European extends Bird {
public function getSpeed() {
return $this->getBaseSpeed();
}
}
class African extends Bird {
public function getSpeed() {
return $this->getBaseSpeed() - $this->getLoadFactor() * $this->numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
public function getSpeed() {
return ($this->isNailed) ? 0 : $this->getBaseSpeed($this->voltage);
}
}
// Somewhere in Client code.
$speed = $bird->getSpeed();
為何重構
- 幫助你的code 包含 operators 去表現不同的任務
有益的地方
- 移除所有的重複code
如何重構
- Extract Method
- hierarchy subclass, 重新定義方法且是包含conditional和複製code根據相對應的branch
- 刪除 branch
- 重複直到conditional is empty.
Introduce Null Object
使用時機 : 當method 回傳 null 而非 real object
問題
某些方法回傳null 而不是物件, 你必須要check null時
if ($customer === null) {
$plan = BillingPlan::basic();
} else {
$plan = $customer->getPlan();
}
解法
回傳 null object 且是表現預設的動作
class NullCustomer extends Customer {
public function isNull() {
return true;
}
public function getPlan() {
return new NullPlan();
}
// Some other NULL functionality.
}
// Replace null values with Null-object.
$customer = ($order->customer !== null) ?
$order->customer :
new NullCustomer;
// Use Null-object as if it's normal subclass.
$plan = $customer->getPlan();
為何重構
1. 太多的 null check 會讓code 太長
如何重構
- 建立預設動作的 null object
- 在 both class 建立 isNull() 回傳true for null object , real class 回傳false
- 找到所有回傳 null 的將他們用null object 回傳
- 找到所有和null 比的 real class, 用 isNull取代
簡化函式呼叫 - Simplifying Method Calls
Rename Method
使用時機 : 當 method name 沒有表示出它要做的事情時
問題
方法名稱無法解釋要做什麼
解法
- 重新定義名稱
為何重構
- 可能剛開始命名的時候沒有命名好, 也有可能是隨著功能的增加需要一個更好的名字
有益的地方
- 程式碼可讀性
如何重構
- 確定 method 定義的地方
- 建立新的 method 用新的名稱, 將複製所有舊方的 code, 刪除所有舊方法的 code 且 插入所有新的呼叫方法給新方法
- 找出所有呼叫舊方法的 reference 用新的取代
- 刪除舊方法
Add Parameter
使用時機 : 當 method 沒有足夠的資料去表現出它要做的事情時且移動code 從符合的分支到它 然後取代 conditional 用相對應的 method call
問題
- 沒有足夠的資料去表現特定行為
解法
- 新增參數
為何重構
- 當 method 有改變且需要新的參數傳進來判斷
有益的地方
- 加新參數 或 新的 property 差別在 property 比較適合在這個class 中很常使用它時再新增,
如何重構
- 找出所有用到 method 的地方
- 拷貝舊方法, 用新的方法取代舊方法, 新參數可以設定預設值給它
- 找出所有 reference, 用新的 reference 取代
- 刪除舊方法
Remove Parameter
使用時機 :當 method 沒有用到這個變數時
問題
- 參數沒有要用
解法
- 移除沒有要用的參數
為何重構
- 參數會影響我們對於方法的認知
有益的地方
- 方法只需要有使用到的參數再傳進來
如何重構
- 建立新方法藉由複製舊方法, 和刪除相關的參數, 用新的方法取代舊的
- 找出所有呼叫方法的地方
- 刪除舊方法
Separate Query from Modifier
使用時機 : 當 method 做兩件事情以上時
問題
- 會回傳值且改變物件中的東西
解法
- 分成兩個 method
為何重構
- 只做一件事
有益的地方
- 可以一直呼叫不用怕說呼叫就會改變什麼
如何重構
- 建立新的方法給舊的方法用, 且是將舊方法的邏輯放進去(不同種類)
- 改變原本的方法讓它只能回傳值, 之後再用舊方法做其他事情
- 取代所有 references 用新方法 且將舊方法要做的值傳入
- 丟棄原本的 code, 讓方法只做一件事情
Parameterize Method
使用時機 : 多個 method 做類似的事情
問題
- 多個方法表現類似的事情只因為值不同
解法
- 結合方法, 傳入特別的參數值進去
為何重構
- 重複程式碼
如何重構
- 建立新的方法且傳入 parameter 且將類似的方法放入新方法中
- 找到所有類似方法, 用新方法取代並且傳入特別的值
Replace Parameter with Explicit Methods
使用時機 : 根據特定變數做不同的事情
問題
- 根據值的不同儲存不同值
解法
- 將每個儲存值得部分寫成單一 method
//before
function setValue($name, $value) {
if ($name === "height") {
$this->height = $value;
return;
}
if ($name === "width") {
$this->width = $value;
return;
}
assert("Should never reach here");
}
// after
function setHeight($arg) {
$this->height = $arg;
}
function setWidth($arg) {
$this->width = $arg;
}
為何重構
- 不修改日後可能會變成肥大的 method
有益的地方
- 增進可讀性
如何重構
- 根據部分程式碼建立不同的 method
- 找到所有原本呼叫的這個方法的地方並且用新方法取代
- 當所有地方都沒有呼叫舊方法時, 刪除它
Preserve Whole Object
使用時機 : 將多個object 的 property 傳入method
問題
- 從物件拿值丟進方法
解法
- 直接傳入整個物件
//before
$low = $daysTempRange->getLow();
$high = $daysTempRange->getHigh();
$withinPlan = $plan->withinRange($low, $high);
// after
$withinPlan = $plan->withinRange($daysTempRange);
為何重構
- 當物件的參數值改變時, 你要找非常多地方去修改
有益的地方
- 只傳物件
- 當方法需要更多物件的值時, 你不需要再傳入
如何重構
- 建立新參數給方法且是用物件的方式
- 接著一個一個移除舊參數, 測試程式
Replace Parameter with Method Call
使用時機 : 太多變數要傳入一個 method 中時
問題
- 呼叫 query 方法且將結果傳進 method
解法
- 將 query 方法 直接放入 method 中
為何重構
- 越長的參數越難了解
有益的地方
- 刪除不需要的參數, 簡化呼叫方法
如何重構
- 確定取值的方法沒有用到現在的參數
- 盡量讓方法呼叫變得簡單
- 用移除所有相關參數並且用方法呼叫的方式取值
- 使用 Remove Parameter 移除不要的參數
Introduce Parameter Object
使用時機 : 很多 method 會用到同一組參數
問題
解法
為何重構
有益的地方
如何重構
Remove Setting Method
使用時機 : 當有 setMethod 時
問題
- method 包含有重複的 parameters 群體
解法
- 用物件取代
為何重構
- code duplication
- 當需要更多物件上的property時會需要修改很多地方
有益的地方
- 可讀性
如何重構
- 建立新的 class 來儲存 parameter 群體
- 在想要修改的method 用 Add Parameter
- 刪除舊參數
- 測試程式碼
Hide Method
使用時機 : 沒有被其它 class 用到時
問題
- 沒有被其他class 的 method
解法
- 將 method 改成 private 或 public
為何重構
- 當有 public get 或 set method 很常沒有被用到時就可以修正
有益的地方
- 讓 code 隨著時間推移
如何重構
- 找出所有可以變成 private 的方法並修正
Replace Constructor with Factory Method
使用時機: 複雜的 constructor
問題
- 當 constructor 不只是儲存值的時候
解法
用 factory method 且用它取代 constructor calls.
// before
class Employee {
// ...
public function __construct($type) {
$this->type = $type;
}
// ...
}
// after
class Employee {
// ...
static public function create($type) {
$employee = new Employee($type);
// do some heavy lifting.
return $employee;
}
// ...
}
為何重構
有益的地方
- 不依定要回傳 class 物件
- 可以有比較好的命名去代表要回傳什麼
- 可以回傳建立的物件
如何重構
- 建立 factory method. 在 constructor 呼叫它
- 取代所有 constructor call 用 factory method
- 宣告 constructor private
- 調查 constructor 且嘗試去分離 code 並且是沒有直接用在 constructing 物件的
Replace Error Code with Exception
使用時機: 當有錯誤發生時, 不要 return 而是丟出錯誤
問題
- 回傳特別的值代表錯誤
解法
- 丟出錯誤
//before
function withdraw($amount) {
if ($amount > $this->balance) {
return -1;
} else {
$this->balance -= $amount;
return 0;
}
}
//after
function withdraw($amount) {
if ($amount > $this->balance) {
throw new BalanceException;
}
$this->balance -= $amount;
}
為何重構
- 過時的寫法
有益的地方
- 將不需要有大量的 condition 去判斷錯誤
如何重構
- 找出所有回傳 error code 的方法且用 try catch 取代
- 在方法裡丟出錯誤
- 改變 signature 讓它包含要被丟出的錯誤