《重構(gòu)》學(xué)習(xí)筆記(06)-- 重新組織數(shù)據(jù)

在面向?qū)ο蟮恼Z言中,通常會有直接訪問數(shù)據(jù)還是通過方法訪問數(shù)據(jù)的爭論。同時,面向?qū)ο蟮恼Z言也允許使用自己定義的新類型取代傳統(tǒng)語言的簡單數(shù)據(jù)類型。將數(shù)組轉(zhuǎn)換為對象、自封裝字段魔法數(shù)字的消除,都是本周要介紹的點。

Self encapsulate Field(自封裝字段)

你可以直接訪問一個字段,但是字段之間的耦合關(guān)系會逐漸變得笨拙。因此為字段設(shè)置set/get方法,并且只以這些方法來訪問字段。就稱為自封裝字段。

public class RefactorMain {
    private int _low, _high;

    boolean includes(int arg) {
        return (arg >= _low && arg <= _high);
    }
}

重構(gòu)為

public class RefactorMain {
    //重構(gòu)之后
    private int _low, _high;

    boolean includes(int arg) {
        return (arg >= getLow() && arg <= getHigh());
    }

    int getLow() {
        return _low;
    }

    int getHigh() {
        return _high;
    }
}

在“字段訪問方式”這個問題上,存在兩種截然不同的觀點??偨Y(jié)優(yōu)缺點如下:間接訪問變量的好處是,子類可以通過覆寫一個函數(shù)而改變獲取數(shù)據(jù)的路徑,它還支持更靈活的數(shù)據(jù)管理方式,例如懶加載。直接訪問的方式的好處是代碼比較容易閱讀,閱讀代碼時不用查看函數(shù)定義才知道用法。通常重構(gòu)做法:

  • 為待封裝字段建立取值/設(shè)值函數(shù)。
  • 找出該字段的所有引用點,將它們?nèi)扛臑檎{(diào)用取值/設(shè)值函數(shù)。
  • 將該字段聲明為private.
  • 復(fù)查,確保找出所有引用點。
  • 編譯、測試。

Replace Data Value with Object(以對象取代數(shù)據(jù)值)

如有有一個數(shù)據(jù)項,需要與其他數(shù)據(jù)和行為放在一起使用才有意義,那么將其變成對象

重構(gòu)前

重構(gòu)后
重構(gòu)后

隨著程序的開發(fā),一些原本簡單的字符串,可能與其他數(shù)據(jù)適合組裝成為一個對象。通常的重構(gòu)做法為:

  • 為待替換數(shù)值新建一個類,并在這個新類中新建一個const字段(Java final)并保持其類型和源類中你需要替換的數(shù)值類型一樣,然后在新類中加入一個這個字段的取值函數(shù)(get),并加上一個接受此字段為參數(shù)的構(gòu)造函數(shù)。
  • 編譯。
  • 將源類中待替換數(shù)值的類型改為你前面新建的類類型。
  • 修改源類中關(guān)于這個字段的取值函數(shù),令他調(diào)用新類的取值函數(shù)。
  • 如果源類構(gòu)造函數(shù)中用到這個待替換字段(多半是賦值動作)你就應(yīng)該修改構(gòu)造函數(shù),讓它變?yōu)橛眯骂惖臉?gòu)造函數(shù)來給這個字段賦值。
  • 修改源類中待替換字段的設(shè)值函數(shù)(set)令他為新類創(chuàng)建一個實例。
  • 編譯,測試。

Change Value to Reference(將值對象改為引用對象)

本節(jié)理解欠佳,需重復(fù)閱讀。

你有一個class,衍生出許多相等實體(equal instances),你希望將它們替換為單一對象。將這個value object(實值對象)變成一個reference object(引用對象)。

我們舉一個例子。設(shè)計一個顧客與訂單的系統(tǒng),在這個系統(tǒng)中,一個訂單對應(yīng)一個顧客,但是多個訂單可能是一個顧客產(chǎn)生的。原代碼如下:

class Customer {
    public Customer(String name) {
       _name = name;
    }

    public String getName() {
       return _name;
    }
    private final String _name;
}

它被以下的order class使用:

class Order...
    public Order(String customerName) {
       _customer = new Customer(customer);
    }

    public String getCustomerName() {
       return _customer.getName();
    }
    
    public void setCustomer(String customerName) {
       _customer = new Customer(customerName);
    }
    private Customer _customer;

此外,還有一些代碼也會使用Customer對象:

private static int numberOfOrdersFor(Collection orders, String customer) {
    int result = 0;
    Iterator iter = orders.iterator();
    while(iter.hasNext()) {
       Order each = (Order)iter.next();
       if(each.getCustomerName().equals(customer)) result ++;
    }
    return result;
}

這種設(shè)計中,即使多份訂單同屬于一個客戶,但是每個Order對象還是擁有各自的Customer對象。

我們對這段代碼進(jìn)行重構(gòu),為簡單起見,我們在Customer中新建一個static字段模擬靜態(tài)字典。

class Customer...
    static void loadCustomers() {
       new Customer("Lemon Car Hire").store();
        new Customer("Associated Coffee Machines").store();
        new Customer("Bilston Gasworks").store();
    }
    private void store() {
       _instance.put(this.getName(), this);
    }

現(xiàn)在,我要修改factory method,讓它返回預(yù)先創(chuàng)建好的Customer對象:

public static Customer create(String name) {
    return (Customer)_instance.get(name);
}

由于create()總是返回既有的Customer對象,所以我應(yīng)該使用Rename Method(273)修改這個factory method的名稱,以便強(qiáng)調(diào)(說明)這一點。

class Customer...
public static Customer getNamed(String name) {
    return (Customer)_instances.get(name);
}

總結(jié)下,這種重構(gòu)通常的做法為:

  • 使用Replace Constructor with Factory Method方法,編譯測試。
  • 決定由什么對象負(fù)責(zé)提供訪問新對象的途徑。
  • 決定這些引用應(yīng)用預(yù)先創(chuàng)建好,或是應(yīng)該動態(tài)創(chuàng)建。
  • 修改工廠函數(shù),令它返回引用對象。編譯測試。

Change Reference to Value(將引用對象改為值對象)

值對象應(yīng)該是不可變的。無論何時,調(diào)用此對象的查詢函數(shù)得到的都是一個結(jié)果,比如第一個例子中,customer作為值對象,每個order 都有自己的一份customer。

order1.getCustomer("張三").setTelepho("123");
order2.getCustomer("張三").getTelepho();

如果張三開始的號碼是135。order2得到的customer 的值還是135,雖然order1已經(jīng)改變了張三的電話號碼。
引用對象應(yīng)該是可變的,確保某一對象修改,自動會更新其它代表某一相同事物的其它對象的修改。要把reference Object 變成value Object 只需要重寫equals()和hashCode()兩個方法,并且去掉Method Factory 對構(gòu)造函數(shù)的調(diào)用。通常用的做法為:

  • 檢查重構(gòu)目標(biāo)是否為不可變對象,或是否可修改為不可變對象。
    -- 如果改對象目前還不是不可變的,使用remove setting method,直到其成為不可變的為止。
    -- 如果無法將對象修改為不可變的,就放棄使用本項重構(gòu)。
  • 建立equal()、hashcode()。這兩個函數(shù)的修改必須同時進(jìn)行,負(fù)責(zé)依賴hash的任何集合對象(hashtable、hashset、hashmap……)都可能產(chǎn)生意外行為。
  • 考慮是否可以刪除工廠函數(shù),并將構(gòu)造函數(shù)聲明為public.
    注意:要把一個引用對象變成值對象,關(guān)鍵動作:檢查是否不可變。如果不是,就不能使用本項重構(gòu)??勺兊闹祵ο髸斐蔁┤说膭e名問題。

Replace Array with Object(以對象取代數(shù)組)

如果你有一個數(shù)組,但是數(shù)組中并沒有排列的關(guān)系,那么以對象替換數(shù)組,對于數(shù)組中的每個元素,以一個字段來表示。

String[] row = new String[3];
row[0] = "Livepool";
row[1] = "15";

重構(gòu)后

performance row = new Performance();
row.setName("Livepool");
row.setWins("15");

使用這種重構(gòu)手段,可以用變量名去自注釋。這種重構(gòu)方法,應(yīng)該重視調(diào)用地方不要漏改。通常的做法為:

  • 新建一個類表示數(shù)組所擁有的信息,并在其中以一個public字段保存原先的數(shù)組。
  • 修改數(shù)組的所有用戶,讓它們改用新類的實例。
  • 編譯、測試。
  • 逐一為數(shù)組元素條件取值/設(shè)值函數(shù)。根據(jù)元素的用途,為這些訪問函數(shù)命名。修改客戶端代碼,讓它們通過訪問函數(shù)取用數(shù)組內(nèi)的元素。每次修改后,編譯并測試。
  • 當(dāng)所有對數(shù)組的直接訪問轉(zhuǎn)而調(diào)用訪問函數(shù)后,將新類中保存該數(shù)組的字段聲明為private。
  • 編譯。
  • 對應(yīng)數(shù)組內(nèi)的每個元素,在新類中創(chuàng)建一個類型相當(dāng)?shù)淖侄?。修改元素的訪問函數(shù),令它改用上述的新建字段。
  • 修改每個元素,編譯并測試。
  • 數(shù)組的所有元素都有了相應(yīng)的字段后,刪除該數(shù)組。

Duplicate Observed Data(復(fù)制“被監(jiān)視數(shù)據(jù)”)

一個設(shè)計良好的系統(tǒng),view層和業(yè)務(wù)邏輯應(yīng)該分開。一方面業(yè)務(wù)層可能支撐不同的view層,另一方面有利于模塊解耦。由于前端框架大部分都考慮了MV分離,因此本節(jié)不再詳細(xì)描述。有需要的同學(xué)可以購買《重構(gòu)》這本書了解。
這里描述下duplicate Obeserved Data的通常做法:
做法

  • 修改展現(xiàn)類,使其成為領(lǐng)域類的Observer。
  • 針對GUI類中的領(lǐng)域數(shù)據(jù),使用Self Encapsulate Field。
  • 編譯,測試。
  • 在時間處理函數(shù)中調(diào)用設(shè)值函數(shù),直接更新GUI組件。
  • 編譯,測試。
  • 在領(lǐng)域類中定于數(shù)據(jù)及其相關(guān)訪問函數(shù)。
  • 修改展現(xiàn)類中的訪問函數(shù),將它們的操作對象改為領(lǐng)域?qū)ο蟆?/li>
  • 修改Observer的update(),使其從相應(yīng)的領(lǐng)域?qū)ο笾袑⑺钄?shù)據(jù)復(fù)制給GUI組件。
  • 編譯,測試。

Change Unidirectional Association to Bidirectional(將單向關(guān)聯(lián)改為雙向關(guān)聯(lián))

如果兩個類都需要用到對方的特性,但其間只有一條單向鏈接。這時候就需要加一條"反向指針"。不過筆者以為雙向關(guān)聯(lián)會增加系統(tǒng)的復(fù)雜度,不符合現(xiàn)代軟件“依賴倒置”原則。除非非常有必要,否則不要使用雙向關(guān)聯(lián)。
單向關(guān)聯(lián)改雙向關(guān)聯(lián)的通常做法為:

  • 在被引用類中增加一個字段,用以保存反向指針。
  • 決定由哪個類——引用段還是被引用端——控制關(guān)聯(lián)關(guān)系。
  • 在被控制建立一個輔助函數(shù)。如果既有的修改在控制端,讓那個它負(fù)責(zé)更新反向指針。
  • 如果既有的修改函數(shù)在被控制,就在控制端建立一個控制函數(shù),并讓既有的修改函數(shù)調(diào)用這個新建的控制函數(shù)。
    重構(gòu)前
class Order {
  getCustomer() {
    return this._customer
  } 
  setCustomer(arg) {
    this._customer = arg
  }
}

重構(gòu)后

class Customer {
  _orders = new Set()
 
  friendOrders() {
    return this._orders
  }
 
  addOrder(arg) {
    arg.setCustomer(this)
  }
}
 
class Order {
  getCustomer() {
    return this._customer
  }
 
  /**
   * 控制函數(shù)
   * @param {} arg 
   */
  setCustomer(arg) {
    if(arg) {
      this._customer.friendOrders().delete(this)
    }
    this._customer = arg
    if(this._customer) {
      this._customer.friendOrders().add(this)
    }
  }
}

以上例子中,Order新增加了一個控制函數(shù)進(jìn)行對Customer的控制。通常,一對多的系統(tǒng)由單一方承擔(dān)控制者角色。如果多對多,那么無所謂。

Change Bidirectional Association to Unidirectional(將雙向關(guān)聯(lián)改為單向關(guān)聯(lián))

雙向關(guān)聯(lián)的弊端在于要維護(hù)雙向連接、確保對象被正確的創(chuàng)建和刪除而增加復(fù)雜度,并且大量的雙向連接容易造成"僵尸對象"。只有真正需要雙向關(guān)聯(lián)的時候才去使用它,否則就去掉其中一條關(guān)聯(lián)。
改為單向關(guān)聯(lián)的通常做法為:

  • 找出保存你想去除的指針的字段,檢查它的每一個用戶,判斷是否可以去除該指針。
  • 如果客戶使用了取值函數(shù),先運用Self Encapsulate Field將待刪除字段自我封裝起來,然后使用Substitute Algorithm對付取值函數(shù),令它不再使用該字段。然后編譯、測試。
  • 如果客戶并未使用取值函數(shù),那就直接修改待刪除字段的所有被引用點,改為以其他途徑獲得該字段所保存的對象。每次修改后,編譯并測試。
  • 如果已經(jīng)沒有任何函數(shù)使用待刪除字段,移除所有對該字段的更新邏輯,然后刪除該字段。
  • 編譯,測試。
    使用上一節(jié)的例子。
class Customer {
  _orders = new Set()
 
  friendOrders() {
    return this._orders
  }
 
  addOrder(arg) {
    arg.setCustomer(this)
  }
 
  getPriceFor(order) {
    return order.getDiscountedPrice()
  }
}
 
class Order {
  getCustomer() {
    return this._customer
  }
 
  /**
   * 控制函數(shù)
   * @param {} arg 
   */
  setCustomer(arg) {
    if(arg) {
      this._customer.friendOrders().delete(this)
    }
    this._customer = arg
    if(this._customer) {
      this._customer.friendOrders().add(this)
    }
  }
 
  getDiscountedPrice() {
    return this.getGrossPrice() * (1- this._customer.getDiscount())
  }
}

重構(gòu)為

class Customer {
  _orders = new Set()
 
  friendOrders() {
    return this._orders
  }
 
  addOrder(arg) {
    arg.setCustomer(this)
  }
 
  getPriceFor(order) {
    return order.getDiscountedPrice(this)
  }
}
 
class Order {
  getDiscountedPrice(customer) {
    return this.getGrossPrice() * (1- customer.getDiscount())
  }
}

Replace Magic Number with Symbolic Constant(以字面常量取代魔法數(shù))

代碼中的魔法數(shù)字是最悠久的不良現(xiàn)象之一,它的缺點在于無法自注釋,而且多個地點引用同一邏輯數(shù),不符合開閉原則。

mass * 9.8 * height

可以重構(gòu)為

static final double GRAVITATIONAL = 9.8;
...
mass * GRAVITATIONAL * height;

注意:通常常量要大寫。
這種重構(gòu)的做法為:

  • 聲明一個常量,令其值為原本的魔法數(shù)值。
  • 找出這個魔法數(shù)的所有引用點。
  • 檢查是否可以使用這個新聲明的常量來替換該魔法數(shù)。如果可以,便以此常量替換之。
  • 編譯。
  • 所有魔法數(shù)都被替換完畢后,編譯并測試。此時整個程序應(yīng)該運轉(zhuǎn)如常,就像沒有做任何修改一樣。

Encapsulate Field(封裝字段)

這種在Java中畢竟常見,將一個public 字段 增加set/get方法,并將自己修改為private,達(dá)到“數(shù)據(jù)隱藏”的效果。例如

private String _name;
public String getName(){
    return _name;
}
public void setName(String name){
    _name = name;
}

這種重構(gòu)畢竟簡單,為了規(guī)范化也寫上常用的步驟。

  • 為public字段提供取值/設(shè)值函數(shù)。
  • 找到這個類之外使用該字段的所有地點。如果客戶只是讀取該字段,就把引用替換為對取值函數(shù)的調(diào)用;如果客戶修改了該字段,就將此引用點替換為對設(shè)值函數(shù)的調(diào)用。
  • 每次修改后,編譯并測試。
  • 將字段的所有用戶修改完畢后,將字段聲明為private。
  • 編譯,測試。

Encapsulate Collection(封裝集合)

如果類中包含一個集合。那么取值函數(shù)不應(yīng)該返回集合自身,因為這會讓用戶得以修改集合內(nèi)容而集合擁有者卻一無所知。不應(yīng)該為整個集合提供一個設(shè)值函數(shù),但應(yīng)該提供用以為集合添加/移除元素的函數(shù)。這樣,集合擁有者(對象)就可以控制集合元素的添加和移除。
舉個例子:

class Course {
  constructor(name, isAdvanced) {
    this._name = name
    this._isAdvanced = isAdvanced
  }
 
  isAdvanced() {
    return this._isAdvanced
  }
}
 
class Person {
  getCourses() {
    return this._courses
  }
 
  setCourses(arg) {
    this._courses = arg
  }
}

重構(gòu)為:

class Course {
  constructor(name, isAdvanced) {
    this._name = name
    this._isAdvanced = isAdvanced
  }
 
  isAdvanced() {
    return this._isAdvanced
  }
}
 
class Person {
  constructor() {
    this._courses = []
  }
 
  addCourse(arg) {
    return this._courses.push(arg)
  }
 
  removeCourse(arg) {
    this._courses.filter(item => item !== arg)
  }
 
  initializeCourses(arg) {
    this._courses = this._courses.concat(arg)
  }
 
  getCourses() {
    return this._courses.map(item => item)
  }
}

思想就是隱藏和封裝

Replace Record with Data Class(以數(shù)據(jù)類取代記錄)

在前端中遇到較少,暫不做筆記

Replace Type Code with Subclass(以子類取代類型碼)

在前端中遇到較少,暫不做筆記

Replace Type Code with State/Strategy(以State/Strategy取代類型碼)

在前端中遇到較少,暫不做筆記

Replace Subclass with Fields(以字段取代子類)

如果各子類中只有“常量函數(shù)”,那么就可以將子類去除,只保留超類。例如以下結(jié)構(gòu)


重構(gòu)前

可以重構(gòu)為


重構(gòu)后

這種重構(gòu)常用的方法為:
  • 對所有子類使用Replace Constructor with Factory Method。
  • 如果有任何代碼直接引用子類,令它改而引用超類。
  • 針對每個常量函數(shù),在超類中聲明一個final字段。
  • 為超類聲明一個protected構(gòu)造函數(shù),用以初始化這些新增字段。
  • 新建或修改子類構(gòu)造函數(shù),使它調(diào)用超類的新增構(gòu)造函數(shù)。
  • 編譯,測試。
  • 在超類中實現(xiàn)所有常量函數(shù),令它們返回相應(yīng)的字段,然后將函數(shù)從子類中刪掉。
  • 每刪除一個常量函數(shù),編譯并測試。
  • 子類中所有的常量函數(shù)都被刪除后,使用Inline Method將子類構(gòu)造函數(shù)內(nèi)聯(lián)到超類的工廠函數(shù)中。
  • 編譯,測試。
  • 將子類刪掉。
  • 編譯,測試。
  • 重復(fù)“內(nèi)聯(lián)構(gòu)造函數(shù),刪除子類”過程,直到所有子類都被刪除。

本章所述部分方法互為鏡像,通常需要開發(fā)者結(jié)合代碼總體情況采用不同的重構(gòu)手段進(jìn)行重構(gòu)。在重構(gòu)過程中,要時刻牢記代碼重構(gòu)原則:【單一職責(zé)】【里氏替換】【迪米特法則】【依賴倒置原則】【接口隔離原則】【開閉原則】。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 Self Encapsulate Field(自封裝字段) 直接訪問一個字段,會導(dǎo)致字段之間的的耦合關(guān)系過于笨...
    hklbird閱讀 605評論 0 0
  • chapter 1 重構(gòu),第一個案例 1.1 什么時候需要重構(gòu) 需要為程序添加一個特性,但代碼結(jié)構(gòu)無法使自己方便的...
    VictorBXv閱讀 2,225評論 0 1
  • 《重構(gòu)》讀書筆記 總覽 第一部分 第一章從實例程序出發(fā),展示設(shè)計的缺陷,對其重構(gòu)可以了解重構(gòu)的過程和方法。 第二部...
    白樺葉閱讀 2,534評論 2 5
  • 可以先看【推薦】:http://www.itdecent.cn/p/d6ff54d72afb原文:http://...
    郭某人1閱讀 1,982評論 0 0
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,650評論 1 32

友情鏈接更多精彩內(nèi)容