### Duplicate Observed Data(復制「被監視數據」)
(譯注:本節大量保留domain,presentation,event,getter/setter,observed等字眼。所謂presentation class,用以處理「數據表現形式」;所謂domain class,用以處理業務邏輯。)
你有一些domain class置身于GUI控件中,而domain method需要訪問之。
將該筆數據拷貝到一個domain object中。建立一個Observer模式,用以對domain object和GUI object內的重復數據進行同步控制(sync.)。

**動機(Motivation)**
一個分層良好的系統,應該將處理用戶界面(UI)和處理業務邏輯(business logic)的代碼分開。之所以這樣做,原因有以下幾點:(1) 你可能需要使用數個不同的用 戶界面來表現相同的業務邏輯;如果同時承擔兩種責任,用戶界面會變得過分復雜; (2) 與GUI隔離之后,domain class的維護和演化都會更容易;你甚至可以讓不同的開發者負責不同部分的開發。
盡管你可以輕松地將「行為」劃分到不同部位,「數據」卻往往不能如此。同一筆 數據有可能既需要內嵌于GUI控件,也需要保存于domain model里頭。自從MVC(Model-View-Controller)模式出現后,用戶界面框架都使用多層系統(multitiered system)來提供某種機制,使你不但可以提供這類數據,并保持它們同步(sync.)。
如果你遇到的代碼是以雙層(two-tiered)方式開發,業務邏輯(business logic)被內嵌于用戶界面(UI)之中,你就有必要將行為分離出來。其中的主要工作就是函數的分解和搬移。但數據就不同了:你不能僅僅只是移動數據,你必須將它復制到新建部位中,并提供相應的同步機制。
**作法(Mechanics)**
(譯注:建議搭配范例閱讀)
- 修改presentation class,使其成為 domain class 的 Observer[GoF]。
- 如果尚未有domain class,就建立一個。
- 如果沒有「從presentation class到domain class的關聯性(link), 就將domain class保存于咖presentation class的一個值域中。
- 針對GUI class內的domain data,使用Self Encapsulate Field 。
- 編譯,測試。
- 在事件處理函數(event handler)中加上對設值函數(setter)的調用,以「直接訪問方式」(譯注:亦即直接調用組件提供的相關函數)更新GUI組件。
- 在事件處理函數中放一個設值函數(setter),利用它將GUI組件更新為domain data的當前值。當然這其實沒有必要,你只不過是拿它的值設定它自己。但是這樣使用setter,便是允許其中的任何動作得以于日后被執行起來,這是這一步驟的意義所在。
- 進行這個改變時,對于組件,不要使用取值函數(getter),應該采取「直接取用」方式(譯注:亦即直接調用GUI組件所提供的函數),因為稍后我們將修改取值函數(getter),使其從domain object(而非GUI組件)取值。設值函數(setter)也將遭受類似修改。
- 確保測試代碼能夠觸發新添加的事件處理(event handler)機制。
- 編譯,測試。
- 在domain class中定義數據及其相關訪問函數(accessors)。
- 確保domain class中的設值函數(setter)能夠觸發Observer模式的通報機制(notify mechanism)。
- 對于被觀察(被監視)的數據,在domain class中使用「與presentation class所用的相同型別」(通常是字符串)來保存。后續重構中你可以自由改變這個數據型別。
- 修改presentation class中的訪問函數(accessors),將它們的操作對象改為 domain object (而非GUI組件)。
- 修改observer(譯注:亦即presentation class)的update(),使其從相應的domain object中將所需數據拷貝給GUI組件。
- 編譯,測試。
**范例(Example)**
我們的范例從圖8.1所示窗口開始。其行為非常簡單:當用戶修改文本框中的數值,另兩個文本框就會自動更新。如果你修改Start或End,length就會自動成為兩者計算所得的長度;如果你修改length,End就會隨之改變。

圖8.1 一個簡單的GUI窗口
一開始,所有函數都放在IntervalWindow class中。所有文本都能夠響應「失去鍵盤焦點」(loss of focus)這一事件。
~~~
public class IntervalWindow extends Frame...
java.awt.TextField _startField;
java.awt.TextField _endField;
java.awt.TextField _lengthField;
class SymFocus extends java.awt.event.FocusAdapter
{
public void focusLost(java.awt.event.FocusEvent event)
{
Object object = event.getSource();
//譯注:偵測到哪一個文本框失去鍵盤焦點,就調用其event-handler.
if (object == _startField)
StartField_FocusLost(event);
else if (object == _endField)
EndField_FocusLost(event);
else if (object == _lengthField)
LengthField_FocusLost(event);
}
}
~~~
當Start文本框失去焦點,事件監聽器調用StartField_FocusLost ()。另兩個文本框的處理也類似。事件處理函數大致如下:
~~~
void StartField_FocusLost(java.awt.event.FocusEvent event) {
if (isNotInteger(_startField.getText()))
_startField.setText("0");
calculateLength();
}
void EndField_FocusLost(java.awt.event.FocusEvent event) {
if (isNotInteger(_endField.getText()))
_endField.setText("0");
calculateLength();
}
void LengthField_FocusLost(java.awt.event.FocusEvent event) {
if (isNotInteger(_lengthField.getText()))
_lengthField.setText("0");
calculateEnd();
}
~~~
你也許會奇怪,為什么我這樣實現一個窗口呢?因為在我的IDE集成開發環境(Cafe)中,這是最簡單的方式。
如果文本框內的字符串無法轉換為一個整數,那么該文本框的內容將變成0。而后,調用相關計算函數:
~~~
void calculateLength(){
try {
int start = Integer.parseInt(_startField.getText());
int end = Integer.parseInt(_endField.getText());
int length = end - start;
_lengthField.setText(String.valueOf(length));
} catch (NumberFormatException e) {
throw new RuntimeException ("Unexpected Number Format Error");
}
}
void calculateEnd() {
try {
int start = Integer.parseInt(_startField.getText());
int length = Integer.parseInt(_lengthField.getText());
int end = start + length;
_endField.setText(String.valueOf(end));
} catch (NumberFormatException e) {
throw new RuntimeException ("Unexpected Number Format Error");
}
}
~~~
我的任務就是將非視覺性的計算邏輯從GUI中分離出來。基本上這就意味將calculateLength ()和calculateEnd ()移到一個獨立的domain class去。為了這一目的,我需要能夠在不引用(指涉,referring)窗口類的前提下取用Start、End和 length 三個文本框的值。惟一辦法就是將這些數據復制到domain class中,并保持與GUI class數據同步。這就是Duplicate Observed Data 的任務。
截至目前我還沒有一個domain class,所以我著手建立一個:
~~~
class Interval extends Observable {}
~~~
IntervalWindow class需要與此嶄新的domain class建立一個關聯:
~~~
private Interval _subject;
~~~
然后,我需要合理地初始化_subject值域,并把IntervalWindow class變成Interval class的一個Observer。這很簡單,只需把下列代碼放進IntervalWindow構造函數中就可以了 :
~~~
_subject = new Interval();
_subject.addObserver(this);
update(_subject, null);
~~~
我喜歡把這段代碼放在整個建構過程的最后。其中對update()的調用可以確保: 當我把數據復制到domain class后,GUI將根據domain class進行初始化。update()是在java.util.observer接口中聲明的,因此我必須讓IntervalWindow class實現這一接口:
~~~
public class IntervalWindow extends Frame implements Observer
~~~
然后我還需要為IntervalWindow class建立一個update()。此刻我先令它為空:
~~~
public void update(Observable observed, Object arg) { }
~~~
現在我可以編譯并測試了。到目前為止我還沒有做出任何真正的修改。呵呵,小心駛得萬年船。
接下來我把注意力轉移到文本框。一如往常我每次只改動一點點。為了賣弄一下我的英語能力,我從End文本框開始。第一件要做的事就是實施 Self Encapsulate Field。文本框的更新是通過getText()和setText()兩函數實現的,因此我所建立的訪問函數(accessors)需要調用這兩個函數:
~~~
//譯注:class IntervalWindow...
String getEnd() {
return _endField.getText();
}
void setEnd (String arg) {
_endField.setText(arg);
}
~~~
然后,找出_endField 的所有引用點,將它們替換為適當的訪問函數:
~~~
void calculateLength(){
try {
int start = Integer.parseInt(_startField.getText());
int end = Integer.parseInt(getEnd());
int length = end - start;
_lengthField.setText(String.valueOf(length));
} catch (NumberFormatException e) {
throw new RuntimeException ("Unexpected Number Format Error");
}
}
void calculateEnd() {
try {
int start = Integer.parseInt(_startField.getText());
int length = Integer.parseInt(_lengthField.getText());
int end = start + length;
setEnd(String.valueOf(end));
} catch (NumberFormatException e) {
throw new RuntimeException ("Unexpected Number Format Error");
}
}
void EndField_FocusLost(java.awt.event.FocusEvent event) {
if (isNotInteger(getEnd()))
setEnd("0");
calculateLength();
}
~~~
這是Self Encapsulate Field 的標準過程。然而當你處理GUI class 時,情況還更復雜些:用戶可以直接(通過GUI )修改文本框內容,不必調用setEnd() 。 因此我需要在GUI class 的事件處理函數中加上對setEnd() 的調用。這個動作把文本框設定為其當前值。當然,這沒帶來什么影響,但是通過這樣的方式,我 們可以確保用戶的輸入的確是通過設值函數(setter)進行的:
~~~
void EndField_FocusLost(java.awt.event.FocusEvent event) {
setEnd(_endField.getText()); //譯注:注意對以下對此行的討論
if (isNotInteger(getEnd()))
setEnd("0");
calculateLength();
}
~~~
上述調用動作中,我并沒有使用上一頁的getEnd() 取得End 文本框當前內容,而是直接取用該文本框。之所以這樣做是因為,隨后的重構將使上一頁的getEnd() 從domain object(而非文本框)身上取值。那時如果這里用的是getEnd() 函數, 每當用戶修改文本框內容,這里就會將文本框又改回原值。所以我必須使用「直接訪問文本框」的方式獲取當前值。現在我可以編譯并測試值域封裝后的行為了。
現在,在domain class 中加入 _end 值域:
~~~
class Interval...
private String _end = "0";
~~~
在這里,我給它的初值和GUI class 給它的初值是一樣的。然后我再加入取值/設值(getter/setter):
~~~
class Interval...
String getEnd() {
return _end;
}
void setEnd (String arg) {
_end = arg;
setChanged();
notifyObservers(); //譯注:notificaiton code
}
~~~
由于使用了Observer 模式,我必須在設值函數(setter)中加上「發出通告」動作 (即所謂notify code )。我把_end 聲明為一個字符串,而不是一個看似更合理的整數,這是因為我希望將修改量減至最少。將來成功復制數據完畢后,我可以自由自在地于domain class 內部把_end 聲明為整數。
現在,我可以再編譯并測試一次。我希望通過所有這些預備工作,將下面這個較為棘手的重構步驟的風險降至最低。
首先,修改IntervalWindow class 的訪問函數,令它們改用Interval 對象:
~~~
class IntervalWindow...
String getEnd() {
return _subject.getEnd();
}
void setEnd (String arg) {
_subject.setEnd(arg); //(A) 譯注:本頁最下對此行有些說明
}
~~~
同時也修改update() 函數,確保GUI 對Interval 對象發來的通告做出響應:
~~~
class IntervalWindow...
public void update(Observable observed, Object arg) {
_endField.setText(_subject.getEnd());
}
~~~
這是另一個需要「直接取用文本框」的地點。如果我調用的是設值函數(setter),程序將陷入無限遞歸調用(譯注:這是因為IntervalWindow 的設值函數setEnd() 調用了Interval.setEnd() ,一如稍早(A)行所示;而Interval.setEnd() 又調用notifyObservers() ,導致IntervalWindow.update() 又被調用)。
現在,我可以編譯并測試。數據都恰如其分地被復制了。
另兩個文本框也如法炮制。完成之后,我可以使用Move Method 將calculateEnd ()和calculateLength ()搬到Interval class去。這么一來,我就
擁有一個「包容所有domain behavior 和 domain data」并與 GUI code分離的domain class了。
如果上述工作都完成了,我就會考慮徹底擺脫這個GUI class。如果GUI class是個較為老舊的AWT class,我會考慮將它換成一個比較好看的Swing class,而且后者的坐標定位能力也比較強。我可以在domain class之上建立一個Swing GUI。這樣,只要我高興,隨時可以去掉老舊的GUI class。
使用事件監昕器(Event Listeners)
如果你使用事件監聽器(event Listener)而不是Observer/Observable模式,仍然可以實施Duplicate Observed Data。這種情況下,你需要在domain model中建立一個listener class和一個event class (如果你不在意依存關系的話,也可以使用AWT class)。然后,你需要對domain object注冊listeners,就像前例對observable對象注冊observes一樣。每當domain object發生變化(類似上例的update()函數被調用),就向listeners發送一個事件(event)。IntervalWindow class可以利用一個inner class (內嵌類)來實現監聽器接口(listener interface),并在適 當時候調用適當的update()函數。
- 譯序 by 侯捷
- 譯序 by 熊節
- 序言
- 前言
- 章節一 重構,第一個案例
- 起點
- 重構的第一步
- 分解并重組statement()
- 運用多態(Polymorphism)取代與價格相關的條件邏輯
- 結語
- 章節二 重構原則
- 何謂重構
- 為何重構
- 「重構」助你找到臭蟲(bugs)
- 何時重構
- 怎么對經理說?
- 重構的難題
- 重構與設計
- 重構與性能(Performance)
- 重構起源何處?
- 章節三 代碼的壞味道
- Duplicated Code(重復的代碼)
- Long Method(過長函數)
- Large Class(過大類)
- Long Parameter List(過長參數列)
- Divergent Change(發散式變化)
- Shotgun Surgery(散彈式修改)
- Feature Envy(依戀情結)
- Data Clumps(數據泥團)
- Primitive Obsession(基本型別偏執)
- Switch Statements(switch驚悚現身)
- Parallel Inheritance Hierarchies(平行繼承體系)
- Lazy Class(冗贅類)
- Speculative Generality(夸夸其談未來性)
- Temporary Field(令人迷惑的暫時值域)
- Message Chains(過度耦合的消息鏈)
- Middle Man(中間轉手人)
- Inappropriate Intimacy(狎昵關系)
- Alternative Classes with Different Interfaces(異曲同工的類)
- Incomplete Library Class(不完美的程序庫類)
- Data Class(純稚的數據類)
- Refused Bequest(被拒絕的遺贈)
- Comments(過多的注釋)
- 章節四 構筑測試體系
- 自我測試代碼的價值
- JUnit測試框架
- 添加更多測試
- 章節五 重構名錄
- 重構的記錄格式
- 尋找引用點
- 這些重構準則有多成熟
- 章節六 重新組織你的函數
- Extract Method(提煉函數)
- Inline Method(將函數內聯化)
- Inline Temp(將臨時變量內聯化)
- Replace Temp with Query(以查詢取代臨時變量)
- Introduce Explaining Variable(引入解釋性變量)
- Split Temporary Variable(剖解臨時變量)
- Remove Assignments to Parameters(移除對參數的賦值動作)
- Replace Method with Method Object(以函數對象取代函數)
- Substitute Algorithm(替換你的算法)
- 章節七 在對象之間搬移特性
- Move Method(搬移函數)
- Move Field(搬移值域)
- Extract Class(提煉類)
- Inline Class(將類內聯化)
- Hide Delegate(隱藏「委托關系」)
- Remove Middle Man(移除中間人)
- Introduce Foreign Method(引入外加函數)
- Introduce Local Extension(引入本地擴展)
- 章節八 重新組織數據
- Self Encapsulate Field(自封裝值域)
- Replace Data Value with Object(以對象取代數據值)
- Change Value to Reference(將實值對象改為引用對象)
- Replace Array with Object(以對象取代數組)
- Replace Array with Object(以對象取代數組)
- Duplicate Observed Data(復制「被監視數據」)
- Change Unidirectional Association to Bidirectional(將單向關聯改為雙向)
- Change Bidirectional Association to Unidirectional(將雙向關聯改為單向)
- Replace Magic Number with Symbolic Constant(以符號常量/字面常量取代魔法數)
- Encapsulate Field(封裝值域)
- Encapsulate Collection(封裝群集)
- Replace Record with Data Class(以數據類取代記錄)
- Replace Type Code with Class(以類取代型別碼)
- Replace Type Code with Subclasses(以子類取代型別碼)
- Replace Type Code with State/Strategy(以State/strategy 取代型別碼)
- Replace Subclass with Fields(以值域取代子類)
- 章節九 簡化條件表達式
- Decompose Conditional(分解條件式)
- Consolidate Conditional Expression(合并條件式)
- Consolidate Duplicate Conditional Fragments(合并重復的條件片段)
- Remove Control Flag(移除控制標記)
- Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件式)
- Replace Conditional with Polymorphism(以多態取代條件式)
- Introduce Null Object(引入Null 對象)
- Introduce Assertion(引入斷言)
- 章節十一 處理概括關系
- Pull Up Field(值域上移)
- Pull Up Method(函數上移)
- Pull Up Constructor Body(構造函數本體上移)
- Push Down Method(函數下移)
- Push Down Field(值域下移)
- Extract Subclass(提煉子類)
- Extract Superclass(提煉超類)
- Extract Interface(提煉接口)
- Collapse Hierarchy(折疊繼承關系)
- Form Template Method(塑造模板函數)
- Replace Inheritance with Delegation(以委托取代繼承)
- Replace Delegation with Inheritance(以繼承取代委托)
- 章節十二 大型重構
- 這場游戲的本質
- Tease Apart Inheritance(梳理并分解繼承體系)
- Convert Procedural Design to Objects(將過程化設計轉化為對象設計)
- Separate Domain from Presentation(將領域和表述/顯示分離)
- Extract Hierarchy(提煉繼承體系)
- 章節十三 重構,復用與現實
- 現實的檢驗
- 為什么開發者不愿意重構他們的程序?
- 現實的檢驗(再論)
- 重構的資源和參考資料
- 從重構聯想到軟件復用和技術傳播
- 結語
- 參考文獻
- 章節十四 重構工具
- 使用工具進行重構
- 重構工具的技術標準(Technical Criteria )
- 重構工具的實用標準(Practical Criteria )
- 小結
- 章節十五 集成
- 參考書目