Getter、Setter的用與不用


iThome 網站首載:Getter、Setter的用與不用

在Java界有個該不該使用Getter、Setter的老問題,不單是初學者經常覺得多此一舉,就連老手們偶而也會從封裝、維護、抽象化等角度戰上數回,有人將罪過推給了JavaBean對Getter、Setter做了規範,然而,若問題根源是如此,那麼直接支援特性(Properties)存取語法的語言又是怎麼一回事呢?

直接來對Getter、Setter吧!

如果在類別中直接建立一個public的值域(Field),那麼馬上就會有人說這樣的設計不好,破壞了封裝,應該將值域設為private,然後加上一對Getter、Setter,例如類別中若有個name值域,就會成為這副模樣:

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


然而老實說吧!這種寫法有什麼封裝能力嗎?跟類別中直接寫個public String name有什麼不同呢?主張一開始就要加上Getter、Setter的人會列舉優點,像是方法可以有publicprotectedprivate等存取權限、未來可於方法中加上存取控制(驗證、延遲加載、生命週期等)、可以改變值域名稱、可以重新定義(Override)、有的框架預期有這樣的命名慣例、可以用於Lambda方法參考,甚至是除錯時加上中斷點很方便之類的理由。

現代功能較齊全的IDE,可以自動產生Getter、Setter,似乎加強了這類觀點,如果嫌一堆Getter、Setter使得程式碼變醜變冗長了,還可以考慮使用Lombok這類框架,以標註方式來提供Getter、Setter,為了未來的種種可能性,為每個值域都產生一對Getter、Setter似乎就沒錯了,不過,若試著將這種想法帶到具有特性存取語法的語言中實現,像是在Python、Ruby、JavaScript(ECMAScrpt 5)等,似乎又有點怪異了,在這類語言中,通常是在必要情況下,才會使用特性存取語法加以定義。

需要的是固定存取原則?

顯然地,Java的Getter、Setter語法來自JavaBean規範,有些框架也經常要求Getter、Setter的命名方式,然而,對於支援特性存取語法的語言,定義特性時的語法本身不也是個規範嗎?因而問題根源並不在於JavaBean規範或者一些框架的要求助長了醜陋的Getter、Setter,應當思考的是,當需要為private的值域加上一對Getter、Setter,是否在思考的是固定存取原則(Uniform access principle)的需求?

舉例來說,在Python中若一開始於類別的__init__(self, radius)中定義了self.radius = radius,那麼使用者在建立實例並指定給ball之後,就可以透過ball.radius取值、透過ball.radius = 2.31設值,若將來打算在設定時加上驗證、生命週期等功能,可以使用@property@property.setter定義類別的特性:   
@property
def radius(self):
    return self.__radius
   
@radius.setter
def radius(self, radius):
    # 一些存取控制
    self.__radius = radius

   
那麼原先使用了ball.radius取值、ball.radius = 2.31設值的客戶端並不需要修改。許多提供特性存取語法的語言,都能達到這類功能,這樣的實現稱為固定存取原則,也就是提供客戶端服務時所使用的名稱必須固定,無論該服務是透過計算或既有的結果。

然而,Java並不具備這樣的能力,因此,為了保有未來實現固定存取原則的可能性,以應付stackoverflow上〈Why use getters and setters?〉這類問題中提到的未來種種需求,就只好未雨綢繆地事先建立Getter、Setter。

- 這應該是物件的特性嗎?

實際上,特性不等於值域,特性是對客戶端公開的協定,若為每個private值域建立一對公開的Getter、Setter,等於是「將值域直接對應為物件的特性」,有此類需求的一個場合,是將物件單純作為一種資料結構(或者稱為資料容器),針對這類值域就是特性的場合,為了保有未來實現固定存取原則的可能性,或者符合框架需求,直接存取private值域的Getter、Setter並不會是多此一舉。

然而,未經思考就直接為每個private值域建立一對公開的Getter、Setter,就有很大的可能性混淆了特性與值域的角色。將代表物件內部狀態的值域,直接透過Getter傳回給客戶端,客戶端就有可能進一步操作值域參考之物件,例如若值域實際為ArrayList,那麼直接透過Getter傳回,就有可能造成客戶端將ArrayList清空,而這並不是提供Getter的物件想要之結果,另一個可能性是客戶端取得值域後,進一步探索值域的值域,因而違反了迪米特法則,造成嚴重的相依性。

正因為值域不應對客戶端公開,因此是否保有固定存取原則實現可能性的考量,應是針對特性,而不是值域,當Getter、Setter直接曝露值域時,應當問的是這個值域會是物件的公開特性嗎?若答案是否定的,那麼直接為值域建立Getter、Setter就是破壞封裝,而且會造成客戶端取得值域後,將邏輯寫在其他地方,而不是讓邏輯處於值域所在的物件之中,這也是〈Why getter and setter methods are evil〉在探討的問題之一。

針對客戶端取得值域後,將邏輯寫在其他地方的問題,可思考Tell, Don't Ask原則,也就是別查詢物件狀態後做些什麼,而是命令物件來做些什麼,這個原則迪米特法則相似,如果流程中不斷透過某物件的Getter、Setter取值、執行某個邏輯、設值,那麼也許應該在物件上建立一個方法,直接命令物件完成任務,在《The ThoughtWorks Anthology》中提到讓軟體設計更好的第九個練習是「不要使用任何 getters/setters/properties」,真正目的就是在遵循Tell, Don't Ask。

- 別只是個GetterEradicators

然而,就像迪米特法則並非斤斤計較連續dot的數量,Tell, Don't Ask原則也不是一看到Getter方法就要感到不快,以免成了Martin Fowler說的〈GetterEradicators〉,就如同他在〈TellDontAsk〉(http://goo.gl/JaSbuA)中談到的,重點是將資料以及與資料相關的行為放在一起。

出現Getter並不見得就是壞事,如前面談到,物件也許單純作為一種資料結構,另一個應用場合是資源的隱藏或抽象化,例如也許從某視窗物件上透過getBackgroudColor取得Color,實際上,視窗物件內部也許是從JComponent值域或其他類型取得顏色資訊後包裝為Color,客戶端並不得而知。類似地,看到Setter也不見得是壞事,除了當作是一種公開可設定的特性之外,如果物件確實依賴在另一個物件上,那Setter接受物件並直接設定給值域也沒什麼不對(當然,設計上可以依賴在公開介面而非實作)。

問題的根源從不在是否使用了Getter、Setter(這也不僅是Java中才會有的問題),是的!有些框架確實要求要有Getter、Setter的命名規範,不過,這些框架真正要求的並不是要曝露物件的值域,而是這些物件要有框架要求的公開特性,反過來說,如果沒有思考特性與值域的差別,就算曝露值域的方法不是被命名為getXXXsetXXX,或者是在其他語言中使用語法定義特性直接曝露值域,也有極大的可能性破壞了封裝!