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, 越難知道這個方法做啥

有益的地方

  1. 更多可讀性的code
  2. 更少的 code duplication
  3. 區隔開來的code錯誤也會比較少

如何重構

  1. 建立新method, 且確定method 名稱是有意義的
  2. 將舊的code複製到新method, 刪除舊有的code並且用新的呼叫方式, 找出所有variable的宣告並且將這些變數變成local variable
  3. 將需要的parameter從外部傳進來
  4. 如果看到區域變數有改變在舊的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 會更直觀

如何重構

  1. 確定 method 沒有重新定義在 subclass, 如果method重新定義避這種狀況
  2. 找到所有呼叫這個方法的的地方, 用 method content 去取代
  3. 刪除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 更好理解, 藉由分解成不同片段

  1. if () / ?:
  2. 很長的數學運算且是沒有中介部分
  3. 很長很多行

有益的地方

增加可讀性, 嘗試讓這些變數代表要表示的意思

如何重構

  1. 新增新的一列去區隔相對的expression, 定義新的 variable 並且去 assign 複雜的 expression
  2. 取代部份的expression 用新的變數
  3. 重複上述步驟

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 可能會在別的地方被發現. 所以寫在一個地方比較好共用

有益的地方

  1. 可讀性. getTax() 比 orderPrice() * 0.2 還好讀懂
  2. 同樣的 code 是不好的

如何重構

  1. 確定這個值是只有被assign 給變數一次, 使用 Split Temporary Variable 確定變數只有被使用來存取 expression 的結果
  2. 使用 Extract Method 去將 expression 放在一個新 method 裡. 確保這個 method 只有回傳值並且沒有改變 object 的 state
  3. 用新 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;

為何重構

當你需要改變變數. 你會必須要重複檢查去確保正確的值是被使用的

有益的地方

  1. 程式碼應該只負責一件事情而且只有一件. 這會讓 maintain 更簡單
  2. 增加可讀性

如何重構

  1. 找出變數名稱並且給予它正確的名稱(跟數值相關的名稱)
  2. 用新名字取代
  3. 重複上述步驟直到每個值都有它特殊的變數名

Remove Assignments to Parameters

使用時機 : 方法輸入的變數值會發生改變時

問題

直接用傳進來的參數

function discount($inputVal, $quantity) {
  if ($inputVal > 50) {
    $inputVal -= 2;
  }
...
}

解法

給傳進來的參數一個變數

function discount($inputVal, $quantity) {
  $result = $inputVal;
  if ($inputVal > 50) {
    $result -= 2;
  }
  ...

為何重構

  1. 假設傳進來的是 reference, 那麼當參數值一改變在外面的也會變
  2. 當一個變數被很多次的 assign 那麼繼很難知道這個變數是在儲存甚麼值

有益的地方

  1. 程式中的每個 element 應該就只負責一件事情

如何重構

  1. 建立一個 local variable, 然後 assign 你的parameter 變數值給區域變數
  2. 所有的方法都用這種方式, 去改變

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 且你無法分割因為眾多的區域變數

有益的地方

分離一個過長的方法是避免方法過大的手段

如何重構

  1. 建立新的 class, 根據那些變數去命名 class
  2. 在新的 class 建立 private field, 給每個 local variable
  3. 建立 constructor, 去初始化 private field 值
  4. 宣告主要的方法且複製原始的方法 code 且用 private filed 去替換
  5. 將原本 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 "";
}

為何重構

  1. 隨著時間的演進, method 可能變成不適合的
  2. 需求可能改變

如何重構

  1. 確定你有簡化方法
  2. 將舊的寫法改寫並且測試
  3. 假設結果不符合舊的寫法, 那就先復原看看差距在哪
  4. 假設所有的測試都通過那就刪除舊的換新的

簡化條件描述式 - 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);
}

為何重構

  1. 過長的程式碼片段很難理解

有益的地方

  1. 很好容易了解這個條件在做啥

如何重構

  1. 將條件變成一個 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. 不夠簡潔

有益的地方

  1. 刪除重複的流程控制 1.分離複雜的expression在新的方法可以用名字表現出他的意思

如何重構

  1. 先確定這樣做不會有任何的副作用
  2. 用 and 和 or 去表示
  3. 用 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();

為何重構

  1. 重複的 code 是邪惡的

有益的地方

  1. 簡化code

如何重構

  1. 假設重複的 code 是在 condition 的開頭將code 移動的 condition 之前
  2. 在結果, 就放在 condition 之後
  3. 假設是隨機的地方先嘗試將code 移動到開頭或結尾, 根據code
  4. 假設超過一行就用 extract method 去重構

Remove Control Flag

使用時機 : 當有一個 control flag 對很多 boolean 表示式時

問題

有一個變數用來當作 control flag

解法

用 break, contine, return 去取代變數

為何重構

  1. 現在有特殊的operator 可以修正這樣的結果

有益的地方

  1. 不合時宜

如何重構

  1. 移除 control flag 相關的 code
  2. 用 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;
}

解法

  1. 區隔所有的 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 實在是很難閱讀和知道它要做什麼

有益的地方

如何重構

  1. 嘗試丟棄code 副作用
  2. 分離所有的 condition 之後呼叫 exception 或直接回傳值
  3. 確定修改後所有的測試都有通過

Replace Conditional with Polymorphism

使用時機 : 當有條件式必須要根據物件型態時

問題

  1. 你有一個條件的表現方式是根據物件類型或 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();

為何重構

  1. 幫助你的code 包含 operators 去表現不同的任務

有益的地方

  1. 移除所有的重複code

如何重構

  1. Extract Method
  2. hierarchy subclass, 重新定義方法且是包含conditional和複製code根據相對應的branch
  3. 刪除 branch
  4. 重複直到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 太長

如何重構

  1. 建立預設動作的 null object
  2. 在 both class 建立 isNull() 回傳true for null object , real class 回傳false
  3. 找到所有回傳 null 的將他們用null object 回傳
  4. 找到所有和null 比的 real class, 用 isNull取代

簡化函式呼叫 - Simplifying Method Calls

Rename Method

使用時機 : 當 method name 沒有表示出它要做的事情時

問題

方法名稱無法解釋要做什麼

解法

  1. 重新定義名稱

為何重構

  1. 可能剛開始命名的時候沒有命名好, 也有可能是隨著功能的增加需要一個更好的名字

有益的地方

  1. 程式碼可讀性

如何重構

  1. 確定 method 定義的地方
  2. 建立新的 method 用新的名稱, 將複製所有舊方的 code, 刪除所有舊方法的 code 且 插入所有新的呼叫方法給新方法
  3. 找出所有呼叫舊方法的 reference 用新的取代
  4. 刪除舊方法

Add Parameter

使用時機 : 當 method 沒有足夠的資料去表現出它要做的事情時且移動code 從符合的分支到它 然後取代 conditional 用相對應的 method call

問題

  1. 沒有足夠的資料去表現特定行為

解法

  1. 新增參數

為何重構

  1. 當 method 有改變且需要新的參數傳進來判斷

有益的地方

  1. 加新參數 或 新的 property 差別在 property 比較適合在這個class 中很常使用它時再新增,

如何重構

  1. 找出所有用到 method 的地方
  2. 拷貝舊方法, 用新的方法取代舊方法, 新參數可以設定預設值給它
  3. 找出所有 reference, 用新的 reference 取代
  4. 刪除舊方法

Remove Parameter

使用時機 :當 method 沒有用到這個變數時

問題

  1. 參數沒有要用

解法

  1. 移除沒有要用的參數

為何重構

  1. 參數會影響我們對於方法的認知

有益的地方

  1. 方法只需要有使用到的參數再傳進來

如何重構

  1. 建立新方法藉由複製舊方法, 和刪除相關的參數, 用新的方法取代舊的
  2. 找出所有呼叫方法的地方
  3. 刪除舊方法

Separate Query from Modifier

使用時機 : 當 method 做兩件事情以上時

問題

  1. 會回傳值且改變物件中的東西

解法

  1. 分成兩個 method

為何重構

  1. 只做一件事

有益的地方

  1. 可以一直呼叫不用怕說呼叫就會改變什麼

如何重構

  1. 建立新的方法給舊的方法用, 且是將舊方法的邏輯放進去(不同種類)
  2. 改變原本的方法讓它只能回傳值, 之後再用舊方法做其他事情
  3. 取代所有 references 用新方法 且將舊方法要做的值傳入
  4. 丟棄原本的 code, 讓方法只做一件事情

Parameterize Method

使用時機 : 多個 method 做類似的事情

問題

  1. 多個方法表現類似的事情只因為值不同

解法

  1. 結合方法, 傳入特別的參數值進去

為何重構

  1. 重複程式碼

如何重構

  1. 建立新的方法且傳入 parameter 且將類似的方法放入新方法中
  2. 找到所有類似方法, 用新方法取代並且傳入特別的值

Replace Parameter with Explicit Methods

使用時機 : 根據特定變數做不同的事情

問題

  1. 根據值的不同儲存不同值

解法

  1. 將每個儲存值得部分寫成單一 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;
}

為何重構

  1. 不修改日後可能會變成肥大的 method

有益的地方

  1. 增進可讀性

如何重構

  1. 根據部分程式碼建立不同的 method
  2. 找到所有原本呼叫的這個方法的地方並且用新方法取代
  3. 當所有地方都沒有呼叫舊方法時, 刪除它

Preserve Whole Object

使用時機 : 將多個object 的 property 傳入method

問題

  1. 從物件拿值丟進方法

解法

  1. 直接傳入整個物件
//before
$low = $daysTempRange->getLow();
$high = $daysTempRange->getHigh();
$withinPlan = $plan->withinRange($low, $high);
// after
$withinPlan = $plan->withinRange($daysTempRange);

為何重構

  1. 當物件的參數值改變時, 你要找非常多地方去修改

有益的地方

  1. 只傳物件
  2. 當方法需要更多物件的值時, 你不需要再傳入

如何重構

  1. 建立新參數給方法且是用物件的方式
  2. 接著一個一個移除舊參數, 測試程式

Replace Parameter with Method Call

使用時機 : 太多變數要傳入一個 method 中時

問題

  1. 呼叫 query 方法且將結果傳進 method

解法

  1. 將 query 方法 直接放入 method 中

為何重構

  1. 越長的參數越難了解

有益的地方

  1. 刪除不需要的參數, 簡化呼叫方法

如何重構

  1. 確定取值的方法沒有用到現在的參數
  2. 盡量讓方法呼叫變得簡單
  3. 用移除所有相關參數並且用方法呼叫的方式取值
  4. 使用 Remove Parameter 移除不要的參數

Introduce Parameter Object

使用時機 : 很多 method 會用到同一組參數

問題

解法

為何重構

有益的地方

如何重構

Remove Setting Method

使用時機 : 當有 setMethod 時

問題

  1. method 包含有重複的 parameters 群體

解法

  1. 用物件取代

為何重構

  1. code duplication
  2. 當需要更多物件上的property時會需要修改很多地方

有益的地方

  1. 可讀性

如何重構

  1. 建立新的 class 來儲存 parameter 群體
  2. 在想要修改的method 用 Add Parameter
  3. 刪除舊參數
  4. 測試程式碼

Hide Method

使用時機 : 沒有被其它 class 用到時

問題

  1. 沒有被其他class 的 method

解法

  1. 將 method 改成 private 或 public

為何重構

  1. 當有 public get 或 set method 很常沒有被用到時就可以修正

有益的地方

  1. 讓 code 隨著時間推移

如何重構

  1. 找出所有可以變成 private 的方法並修正

Replace Constructor with Factory Method

使用時機: 複雜的 constructor

問題

  1. 當 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;
  }
  // ...
}

為何重構

有益的地方

  1. 不依定要回傳 class 物件
  2. 可以有比較好的命名去代表要回傳什麼
  3. 可以回傳建立的物件

如何重構

  1. 建立 factory method. 在 constructor 呼叫它
  2. 取代所有 constructor call 用 factory method
  3. 宣告 constructor private
  4. 調查 constructor 且嘗試去分離 code 並且是沒有直接用在 constructing 物件的

Replace Error Code with Exception

使用時機: 當有錯誤發生時, 不要 return 而是丟出錯誤

問題

  1. 回傳特別的值代表錯誤

解法

  1. 丟出錯誤
//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;
}

為何重構

  1. 過時的寫法

有益的地方

  1. 將不需要有大量的 condition 去判斷錯誤

如何重構

  1. 找出所有回傳 error code 的方法且用 try catch 取代
  2. 在方法裡丟出錯誤
  3. 改變 signature 讓它包含要被丟出的錯誤