范例(Examples)
我們的范例其行為非常簡單:當用戶修改文本框中的數值,另兩個文本框就會自動更新.如果你修改Start或End,length就會自動成為兩者計算所得的長度;如果你修改length,End就會隨之變動.
一開始,所有函數都放在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();
???
??? ?? ?? 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");
??? calculateLength();
}
如果文本框的字符串無法轉換為一個整數,那么該文本框的內容將變成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 end = Integer.parseInt(_endField.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去。為了這一目的,我需要能夠在不引用窗口類的前提取用Start、End和length三個文本框的值。唯一辦法就是將這些數據復制到domain class中,并保持與GUI class數據同步。這就是Duplicate Observed Data(189)的任務。
截至目前我還沒有一個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(171)。文本框的更新是通過getText()和setText()兩函數實現的,因此我所建立的訪問函數(accessors)需要調用這兩個函數:
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 end = Integer.parseInt(_endField.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(171)的標準過程。然后當你處理GUI class時,情況還更復雜些:用戶可以直接(通過GUI)修改文本框內容,不必調用setEnd()。因此我需要在GUI class的事件處理函數中加上對setEnd()的調用。這個動作把End文本框設定為其當前值。當然,這沒帶來什么影響,但是通過這樣的方式,我們可以確保用戶的輸入的確是通過設值函數(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();
??? }
由于使用了Observer模式,我必須在設值函數(setter) 中加上[發出通告]動作(即所謂notification code)。我把_end聲明為一個字符串,而不是一個看似更合理的整數,這是因為我希望將修改量減至最少。將來成功復制數據完畢后,我可以自由自在地于 domain class內部把_end聲明為整數。
現在,我可以再編譯并測試一次。我希望通過所有這些預備工作,將下面這個較為棘手的重構步驟的風險降至最低。
首先,修改IntervalWindow class的訪問函數,令它們改用Interval對象:
class IntervalWindow...
??? String getEnd() {
??? ?? return _subject.getEnd();
??? }
??? void setEnd(String arg) {
??? ?? _subject.setEnd(arg);
??? }
同時也修改update()函數,確保GUI對Interval對象發來的通告做出響應:
class IntervalWindow...
??? public void update(Observable observed, Object arg) {
??? ?? _endField.setText(_subject.getEnd());
??? }
這是另一個需要[直接取用文本框]的地點。如果我調用的是設值函數(setter),程序將陷入無限遞歸調用(這是因為IntervalWindow的設 值函數setEnd()調用了Interval。setEnd(),一如稍早行所示:而Interval.setEnd()又調用 notifyObservers(),導致IntervalWindow.update()又被調用)。
現在,我可以編譯并測試,數據都恰如其分地被復制了。
另兩個文本框也如法炮制。完成之后,我可以使用Move Method(142)將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(189)。這種情況下,你需要在domain model中建立一個listener class和一個event class。然后,你需要對domain object注冊listeners,就像前例對observable對象注冊observers一樣。每當domain object發生變化(類似上例的update()函數被調用),就向listeners發送一個事件(event)。IntervalWindow class可以利用一個inner class(內嵌類)來實現監聽器接口(listener interface),并在適當時候調用適當的update()函數。
我們的范例其行為非常簡單:當用戶修改文本框中的數值,另兩個文本框就會自動更新.如果你修改Start或End,length就會自動成為兩者計算所得的長度;如果你修改length,End就會隨之變動.
一開始,所有函數都放在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();
???
??? ?? ?? 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");
??? calculateLength();
}
如果文本框的字符串無法轉換為一個整數,那么該文本框的內容將變成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 end = Integer.parseInt(_endField.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去。為了這一目的,我需要能夠在不引用窗口類的前提取用Start、End和length三個文本框的值。唯一辦法就是將這些數據復制到domain class中,并保持與GUI class數據同步。這就是Duplicate Observed Data(189)的任務。
截至目前我還沒有一個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(171)。文本框的更新是通過getText()和setText()兩函數實現的,因此我所建立的訪問函數(accessors)需要調用這兩個函數:
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 end = Integer.parseInt(_endField.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(171)的標準過程。然后當你處理GUI class時,情況還更復雜些:用戶可以直接(通過GUI)修改文本框內容,不必調用setEnd()。因此我需要在GUI class的事件處理函數中加上對setEnd()的調用。這個動作把End文本框設定為其當前值。當然,這沒帶來什么影響,但是通過這樣的方式,我們可以確保用戶的輸入的確是通過設值函數(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();
??? }
由于使用了Observer模式,我必須在設值函數(setter) 中加上[發出通告]動作(即所謂notification code)。我把_end聲明為一個字符串,而不是一個看似更合理的整數,這是因為我希望將修改量減至最少。將來成功復制數據完畢后,我可以自由自在地于 domain class內部把_end聲明為整數。
現在,我可以再編譯并測試一次。我希望通過所有這些預備工作,將下面這個較為棘手的重構步驟的風險降至最低。
首先,修改IntervalWindow class的訪問函數,令它們改用Interval對象:
class IntervalWindow...
??? String getEnd() {
??? ?? return _subject.getEnd();
??? }
??? void setEnd(String arg) {
??? ?? _subject.setEnd(arg);
??? }
同時也修改update()函數,確保GUI對Interval對象發來的通告做出響應:
class IntervalWindow...
??? public void update(Observable observed, Object arg) {
??? ?? _endField.setText(_subject.getEnd());
??? }
這是另一個需要[直接取用文本框]的地點。如果我調用的是設值函數(setter),程序將陷入無限遞歸調用(這是因為IntervalWindow的設 值函數setEnd()調用了Interval。setEnd(),一如稍早行所示:而Interval.setEnd()又調用 notifyObservers(),導致IntervalWindow.update()又被調用)。
現在,我可以編譯并測試,數據都恰如其分地被復制了。
另兩個文本框也如法炮制。完成之后,我可以使用Move Method(142)將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(189)。這種情況下,你需要在domain model中建立一個listener class和一個event class。然后,你需要對domain object注冊listeners,就像前例對observable對象注冊observers一樣。每當domain object發生變化(類似上例的update()函數被調用),就向listeners發送一個事件(event)。IntervalWindow class可以利用一個inner class(內嵌類)來實現監聽器接口(listener interface),并在適當時候調用適當的update()函數。