38
Copyright 2009 Trend Micro Inc. Classification 06/07/2022 1 Refactoring Improving The Design of Existing Code Ch. 7 Moving Features Between Objects Joe C Wu Developer, WSE(Web Services Engineering) Volume

重構—改善既有程式的設計(chapter 7)

Embed Size (px)

DESCRIPTION

 

Citation preview

Page 1: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 1

Refactoring Improving The Design of Existing Code

Ch. 7 Moving Features Between ObjectsJoe C Wu Developer, WSE(Web Services Engineering) Volume

Page 2: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.

Outline

• Ch. 7 Moving Features Between Objects– Move Method ( 搬移函式 )– Move Field ( 搬移欄位 )– Extract Class ( 提練類別 )– Inline Class ( 將類別內聯化 )– Hide Delegate ( 隱藏「委託關係」 )– Remove Middle Man ( 移除中間人 )– Introduce Foreign Method ( 加入外加函式 )– Introduce Local Extension ( 引入區域性擴展 )

• Summary

Classification 04/10/2023 2

Page 3: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 3

Move Method (搬移函式 )

• 在該函式最常引用的 class 中建立一個有著類似行為的新函式。將舊函式變成一個單純的委託函式 (delegating method) ,或是將舊函式完全移除。

Page 4: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 4

Move Method (搬移函式 )

• 動機– 當一個 Class 有太多的行為 .– 當一個 Class 與另個 Class 有太多合作而型成高度耦合 (highly

coupled).– 使系統中的 classes 更簡單。

• 作法– 這個函式與哪個物件的交流比較多 ?

• 檢查 source class 定義的 method 所使用的一切 features.• 檢查 sub class, super class.• 搬移 source method to target method -> Compile target class.• 決定如何從 source 正確引用 target object.• 修改 source method 成為 delegating method. -> Compile and Test• 刪除 source method 或將它當作一個 delegating method 保留下來。• 將 source class 中對 source method 的引用替換為 target method.• Compile and Test.

– 很難決定 -> 或許移動這個函式與否,並不那麼重要。

Page 5: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc. 5

Move Method (搬移函式 )

• 範例

Classification 04/10/2023

Class Account…

double overdraftCharge() {

if ( _type.isPremium()) {

double result = 10;

if (_dayOverdrawn > 7) result += (_daysOverdrawn – 7) * 0.85;

return result;

}// end if

else return _daysOverdrawn * 1.75;

} // end overdraftChrge

double bankCharge () {

double result = 4.5;

if (_daysOverdrawn > 0) result += overdraftCharge();

return result;

} // end bankCharge

private AccountType _type;

private int _daysOverdrawn;

Suppose you wish to have other account types

and decide to build an account type class to contain the methods

Page 6: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc. 6

Move Method (搬移函式 )

Classification 04/10/2023

Class AccountType… several account types – diff method

double overdraftCharge(int daysOverdrawn) {

if (isPremium()) {

double result = 10;

if (dayOverdrawn > 7) result += (daysOverdrawn – 7) * 0.85;

return result;

}// end if

else return _daysOverdrawn * 1.75;

} // end overdraftChrge

Class Account…

double bankCharge () {

double result = 4.5;

if (_days|Overdrawn |> 0) result += _type.overdraftCharge(_daysOverdrawn);

return result;

} // end bankCharge

Page 7: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 7

Move Field (搬移欄位 )

• 某個 class 的 field ,被別的 class 使用更多次。• 在 target class 建立一個 new field ,修改 source field 的所

有用戶,令它們改用 new field.

Page 8: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 8

Move Field (搬移欄位 )

• 動機– 某個 class 的 field ,被別的 class 使用更多次。

• 透過 Get/Set 間接進行。• 也移動使用這個 field 的使用者。 ( 取決於介面是否保持一致 )

– 使用 Extract Class 時,也可能需要搬移 field ,這時會先搬 field.

• 作法– Encapsulate Field (Get/Set)– Compile and Test.– 在 target class 建立同樣的 field ,並建立 Getting/Setting 。– Compile target class.– 取得 target object ( 若現有 field or method 不能做到,建一個 field 來

存。– 將所有 source field 的引用,改為存取 target field 。– Compile and Test.

Page 9: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 9

Move Field (搬移欄位 )

• 範例Class Account…

private AccoutnType _type;

private double _interestRate;

double interestForAmount)days (double amount, int days) {

return )interestRate * amount * days /365;

} // end interestForAmount

Would like to move interestRate to the AccountType class because it is used more in that class.

Page 10: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 10

Move Field (搬移欄位 )

• 範例

Class AccountType….

private double _interestRate;

void setInterestRate (double arg) { _interestRate = arg; }

double getInterestRate () { return _interestRate; }

……….

double interestForAccount_days (double amount, int days) {

return _type.getInterestRate() * amount * days/365;

} // end interestForAccount_days

Move _interestRate.

In the original class

reference get and set methods for the field.

Page 11: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 11

Extract Class (提練類別 )

• 某個 class 做了應該由兩個 classes 做的事。• 建立一個新的 class ,將相關的欄位和函式從舊 class 移至

新 class.

Page 12: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 12

Extract Class (提練類別 )

• 動機– 一個 class 應該是一個清楚的抽象性 (abstract) ,處理一些明確的責

任。– Class 太大會不易理解。– 某些資料和函式總是一起出現、某些資料常同時變化並彼此相依,表

示他們該被分離出去。• 作法

– 決定如何分解 class 所負責任。– 建立新的 class ,將舊 class 的責任交給它。– 建立舊 class 與新 class 之間的 link 。– Move Field -> Test– Move Method -> Test– 檢查、精簡每個 class 的介面。– 決定是否讓新 class 曝光 => reference object / immutable value

object

Page 13: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 13

Extract Class (提練類別 )

• 範例

class Person{ public String getName() { return _name; } public String getTelephoneNumber() { return ("(" + _officeAreaCode + ") " + _officeNumber); } String getOfficeAreaCode() { return _officeAreaCode; } void setOfficeAreaCode(String arg) { _officeAreaCode = arg; } String getOfficeNumber() { return _officeNumber; } void setOfficeNumber(String arg) { _officeNumber = arg; } private String _name; private String _officeAreaCode; private String _officeNumber;}

Page 14: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 14

Extract Class (提練類別 )

• Move Fieldclass Person{ private TelephoneNumber _officeTelephone = new TelephoneNumber();

public String getTelephoneNumber() { return ("(" + getOfficeAreaCode() + ") " + _officeNumber); } String getOfficeAreaCode() { return _officeTelephone.getAreaCode(); } void setOfficeAreaCode(String arg) { _officeTelephone.setAreaCode(arg); }}

class TelephoneNumber{ String getAreaCode() { return _areaCode; } void setAreaCode(String arg) { _areaCode = arg; } private String _areaCode;}

Page 15: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc. 15

Extract Class (提練類別 )

• Move Method

Classification 04/10/2023

class Person{ public String getName() { return _name; } public String getTelephoneNumber() { return _officeTelephone.getTelephoneNumber(); } TelephoneNumber getOfficeTelephone() { return _officeTelephone; } private String _name; private TelephoneNumber _officeTelephone = new TelephoneNumber();}

class TelephoneNumber{ public String getTelephoneNumber() { return ("(" + _areaCode + ") " + _number); } String getAreaCode() { return _areaCode; } void setAreaCode(String arg) { _areaCode = arg; } String getNumber() { return _number; } void setNumber(String arg) { _number = arg; } private String _number; private String _areaCode;}

Page 16: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc. 16

Extract Class (提練類別 )

• TelephoneNumber– Public?– Private?– Protected?

• 若 public新的 class,要考慮別名 (aliasing)的問題– 允許任何物件修改 TelephoneNumber物件的任何部份

• 使它成為 Reference Object。 (person為的存取點 ) (Ch.8)– 不許任何人”不透過 Person就修改它”

• 設為 immutable或是提供 immutable interface。– 先複製一個 TelephoneNumber物件,將這個物件傳給用戶,當然這也會造成一定程度的迷惑。

• 你可以為提煉後的 class加鎖 (lock)。但要小心鎖定的問題。

Classification 04/10/2023

Page 17: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 17

Inline Class (將類別內聯化 )

• 你的某個 class 沒有做太多事情 ( 沒有承擔足夠的責任 )

• 將 class 的所有特性搬移到另一個 class 中,然後移除原class.

Page 18: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 18

Inline Class (將類別內聯化 )

• 動機– 如果一個 class 不再承擔足夠的責任,不再有單獨存在的理由。– 挑選最頻繁使用這個「萎縮 class 」的用戶,以 Inline Class 手法塞

進去。• 作法

– 在 absorbing class( 合併端 class) 宣告 source class 的 public 協定,並將所有函式委託 (delegate) 至 source class.

– 修改 source class 的引用點 ->absorbing class 。– Compile and Test– Move Method and Move Field ,將 source class 的特性移過去。– 幫 source class 舉行一個簡單的喪禮。

Page 19: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 19

Inline Class (將類別內聯化 )

• 範例class Person{ public String getName() {return _name;} public String getTelephoneNumber() {return _officeTelephone.getTelephoneNumber();} TelephoneNumber getOfficeTelephone() {return _officeTelephone;} private String _name; private TelephoneNumber _officeTelephone = new TelephoneNumber();

String getAreaCode() { return _officeTelephone.getAreaCode(); }

void setAreaCode(String arg) { _officeTelephone.setAreaCode(arg); }

String getNumber() { return _officeTelephone.getNumber(); }

void setNumber(String arg) { _officeTelephone.setNumber(arg); }}

class TelephoneNumber{ public String getTelephoneNumber() { return ("(" + _areaCode + ") " + _number); } String getAreaCode() { return _areaCode; } void setAreaCode(String arg) { _areaCode = arg; } String getNumber() { return _number; } void setNumber(String arg) { _number = arg; } private String _number; private String _areaCode;}

Page 20: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 20

Inline Class (將類別內聯化 )

Person martin = new Person();martin.getOfficeTelephone().setAreaCode ("781");

Person martin = new Person();martin.setAreaCode ("781");

Page 21: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 21

Hide Delegate (隱藏「委託關係」 )

• 客戶直接呼叫其 server object( 服務物件 ) 的 delegate class 。• 在 Server 端建立客戶所需的所有函式,用以隱藏委托關係

(delegation) 。

Page 22: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 22

Hide Delegate (隱藏「委託關係」 )

• 動機– 封裝即使不是物件的最關鍵特徵,也是最關鍵特徵之一。– 如果某個客戶呼叫了「建立於 server object 的某個欄位基礎之上」

的函式,客戶就必需知道這個委託物件 (delegate object) ,萬一委託關係發生變化,客戶也得相應變化。

– 將委託關係隱藏起來,去除這種依存性,將變化限制在 server 中,不會波及客戶。

Page 23: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 23

Hide Delegate (隱藏「委託關係」 )

• 作法– 對每一個委託關係中的函式,在 server 端建立一個簡單的委託函式

(delegating method) 。– 調整客戶,令它只呼叫 server 提供的函式– Compile and Test– 如果將來不再有任何客戶需要用到 Delegate ,便可移除 server 中的

存取函式– Compile and Test

Page 24: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 24

Hide Delegate (隱藏「委託關係」 )

• 範例class Person{ Department _department; public Department getDepartment() { return _department; } public void setDepartment(Department arg) { _department = arg; }}

class Department{ private String _chargeCode; private Person _manager; public Department(Person manager) { _manager = manager; } public Person getManager() { return _manager; }}

manager = john.getDepartment().getManager();

public Person getManager() { return _department.getManager(); }

manager = john.getManager();

Page 25: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 25

Remove Middle Man (移除中間人 )

• 某個 class 做了過多的簡單委託動作 (simple delegation) 。• 讓客戶直接呼叫 delegate(受託類別 ) 。

Page 26: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 26

Remove Middle Man (移除中間人 )

• 動機– Hide Delegate 的代價

• 當客戶要用到 delegate 的新特性時,你就必須在 server 端添加一個簡單委託函式。 delegate 的特性 (功能 )愈來愈多,就愈來愈痛苦。

– 很難說什麼程度的隱藏才是合適的。– 隨著系統的變化,改成合適的就好了。– 重構的意義就在於:你永遠不必說什麼對不起,只要把出問題的地方

修補好就行了。• 作法

– 建立一個函式,用以取代 delegate(受托物件 ) 。– 對於每個委託函式 (delegate method) ,在 server 中刪除該函式,並

將「客戶對該函式的呼叫」替換為「對 delegate 的呼叫」。– Compile and Test

Page 27: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 27

Remove Middle Man (移除中間人 )

• 範例 class Person{ Department _department; public Person getManager() { return _department.getManager(); }}class Department{ private Person _manager; public Department(Person manager) { _manager = manager; }}

manager = john.getManager();

class Person{ public Department getDepartment() { return _department; }}

manager = john.getDepartment().getManager();

Page 28: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 28

Introduce Foreign Method (加入外加函式 )

• 你所使用的 server class 需要一個函式,但你無法修改這個class 。

• 在 client class 中建立一個函式,並以一個 server class實體作為第一引數 (argument) 。

Date newStart = new Date (previousEnd.getYear(),previousEnd.getMonth(), previousEnd.getDate() + 1);

Date newStart = nextDay(previousEnd);

private static Date nextDay(Date arg) {return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);

}

Page 29: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 29

Introduce Foreign Method (加入外加函式 )

• 動機– 你正在使用一個 class ,它真的很好,為你提供了你想要的所有服務。– 當你需要一個新服務,這個 class卻無法供應…– 如果可以修改源碼,你便可以加一個新函式,如果不能,你就得在客

戶端編碼,補足你要的函式。– 如果你需要使用很多次,你就得不斷重初這些程式碼…– 如果你以外加函式實現一個新功能,那就是一個明確信號:

• 這個函式原本應該在提供服務的 class 中加以實現。– 如果一個 server class 加入了大量的外加函式,你就不應該再使用本項重構,而應該使用 Introduce Local Extension 。

Page 30: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 30

Introduce Foreign Method (加入外加函式 )

• 作法– 在 client class 中建立一個函式,用來提供你需要的功能。

• 這個函式不應該取用 client class 的任何特性,如果需要,用參數傳給它。– 以 server class實體作為該函式的第一個參數。– 將該函式注釋為「外加函式 (foreign method) ,應在 server class實

現。• 範例

Date newStart = new Date (previousEnd.getYear(),previousEnd.getMonth(), previousEnd.getDate() + 1);

Date newStart = nextDay(previousEnd);

private static Date nextDay(Date arg) {return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);

}

Page 31: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 31

Introduce Local Extension (引入區域性擴展 )

• 你所使用的 server class 需要一些額外函式,但你無法修改這個class 。

• 建立一個新 class ,使用包含這些額外函式。讓這個擴展品成盥 source class 的 sub class(子類別 ) 或 wrapper( 外覆類別 ) 。

Page 32: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 32

Introduce Local Extension (引入區域性擴展 )

• 動機– 如果只需要一兩個函式,可以使用 Introduce Foreign Method 。– 超果兩個,可以用這個方法將這些函式組織在一起,這種方式稱之為

local extension( 區域性擴展 )• 是一個獨立的 class ,卻也是其 extended class 的 subtype 。• 堅持「函式和資料應該被包裝在形式良好的單元內」。

– 如果你一直把本該在 extension class 的程式零散放在其他 classes 中,只會讓其他這些 classes 變得過分複雜,並使得其中函式難以被復用。

– local extension 有兩種作法• Subclass

– 必須在物件創建期實施。– 必須新增一個 class ,但是當原物件也被使用時,就要考慮維護兩份資料的問題。

• Wrapper– 通常首選 subclass ,因為工作量比較少。

Page 33: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 33

Introduce Local Extension (引入區域性擴展 )

• 作法– 建立一個 extension class ,將它作為原物 ( 原類別 ) 的 subclass 或

wrapper 。– 在 extension class 中加入轉型建構式 (converting constructors) 。

• Converting constructors: 接受原物作為參數– 在 extension class 中加入新特性。– 根據需要,將原物 (original) 替換為擴展物 (extension) 。– 將「針對原始類別而定義的所有外加函式 (foreign ethods) 」搬移到

extension class 中。

Page 34: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 34

Introduce Local Extension (引入區域性擴展 )

• 範例 (Subclass)

class MfDateSub extends Date{ public MfDateSub (String dateString) { super (dateString); }

public MfDateSub (Date arg) { super (arg.getTime()); }

Date nextDay() { return new Date (getYear(),getMonth(), getDate() + 1); }}

轉型建構式 (converting constructors)

Page 35: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 35

Introduce Local Extension (引入區域性擴展 )

• 範例 (Wrapper)class MfDateWrap{ private Date _original;

public MfDateWrap(String dateString) { _original = new Date(dateString); }

public MfDateWrap(Date arg) { _original = arg; }

// 為原始類別的所有函式提供委託函式… public int getYear() {return _original.getYear();} public int getMonth() {return _original.getMonth();} public int getDate() {return _original.getDate();} public boolean equals(MfDateWrap arg) {return (toDate().equals(arg.toDate()));}

Date nextDay() { return new Date(getYear(), getMonth(), getDate() + 1); }}

只是執行一個單純的委託動作(delegate)public boolean after (Date arg)

aWrapper.after(aDate) aWrapper.after(anotherWrapper)

aDate.after(aWrapper)

如何處理「接受原始類別之實體為參數」的函式?

Page 36: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc. 36

Introduce Local Extension (引入區域性擴展 )

• 範例 (Wrapper)– 上一頁” after” method 這種 overriding 的目的是為了向用戶隱藏

wrapper 的存在,這是一個好策略。

– 不過在某些系統所提供的函式 ( 如 equals) 會出問題。• public boolean equals (Date arg) // causes problems

– 這樣做是危險的,因為 Java 系統的其他部份都認為 equals符合交換律:如果 a.equals(b) 為真, b.equals(a) 也必為真。

– 違反這樣的規則將使我遭遇一大堆莫名其妙的錯誤,但是又無法同時修改 Date ,所以建議向用戶曝露「我進行了包裝」• public boolean equalsDate (Date arg)• public boolean equalsDate (MfDateWrap arg)

– Subclass則不會有這問題,只要不覆寫 original class 中的函式。• 一般來說不會在 extension class中覆寫 original class的函式,只會添加新函式。

Classification 04/10/2023

Page 37: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc.Classification 04/10/2023 37

Summary

• 重構的基本手法– Move Field > Move Method

• Class 責任 過多 或是 過少– Extract Class V.S Inline Class

• Class 使用另一 Class– Hide Delegate 將關係隱藏起來

• 因為 Hide delegate導致擁有者的介面經常變化– Remove Middle Man

• 當我不能存取某個 class 的源碼,又想把其他責任移進去– Introduce Foreign Method (只加一、二個函式 )– Introduce Local Extension

決定把責任放在哪兒 ?

Page 38: 重構—改善既有程式的設計(chapter 7)

Copyright 2009 Trend Micro Inc. 38Classification 04/10/2023