范例(Examples)
我們的范例其行為非常簡單:當(dāng)用戶修改文本框中的數(shù)值,另兩個(gè)文本框就會(huì)自動(dòng)更新.如果你修改Start或End,length就會(huì)自動(dòng)成為兩者計(jì)算所得的長度;如果你修改length,End就會(huì)隨之變動(dòng).
一開始,所有函數(shù)都放在IntervalWindow class中.所有文本框都能夠響應(yīng)[失去鍵盤焦點(diǎn)](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);
??? ?? }
}
當(dāng)Start文本框失去焦點(diǎn),事件監(jiān)聽器調(diào)用StartField_FocusLost()。另兩個(gè)文本框的處理也類似。事件處理函數(shù)大致如下:
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();
}
如果文本框的字符串無法轉(zhuǎn)換為一個(gè)整數(shù),那么該文本框的內(nèi)容將變成0。而后,調(diào)用相關(guān)計(jì)算函數(shù):
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");
??? }
}
我的任務(wù)就是非視覺性的計(jì)算邏輯從GUI中分離出來。基本上這就意味將calculateLength()和calculateEnd()移到一個(gè)獨(dú)立的domain class去。為了這一目的,我需要能夠在不引用窗口類的前提取用Start、End和length三個(gè)文本框的值。唯一辦法就是將這些數(shù)據(jù)復(fù)制到domain class中,并保持與GUI class數(shù)據(jù)同步。這就是Duplicate Observed Data(189)的任務(wù)。
截至目前我還沒有一個(gè)domain class,所以我著手建立一個(gè):
class Interval extends Observable {}
IntervalWindow class需要與此嶄新的domain class建立一個(gè)關(guān)聯(lián):
private Interval _subject;
然后,我需要合理地初始化_subject值域,并把IntervalWindow class變成Interval class的一個(gè)Observer。這很簡單,只需把下列代碼放進(jìn)IntervalWindow構(gòu)造函數(shù)中就可以了:
_subject = new Interval();
_subject.addObserver(this);
update(_subject, null);
我喜歡把這段代碼放在整個(gè)建構(gòu)過程的最后。其中對update()的調(diào)用可以確保:當(dāng)我把數(shù)據(jù)復(fù)制到domain class后,GUI將根據(jù)domain class進(jìn)行初始化。update()是在java.util.observer接口中聲明的,因此我必須讓IntervalWindow class實(shí)現(xiàn)這一接口:
public class IntervalWindow extends Frame implements Observer
然后我還需要為IntervalWindow class建立一個(gè)update()。此刻我先令它為空:
public void update(Observable observed, Object arg)? {
}
現(xiàn)在我可以編譯并測試了。到目前為止我還沒有作出任何真正的修改。呵呵,小心駛得萬年船。
接下來我把注意力轉(zhuǎn)移到文本框。一如往常我每次只改動(dòng)一點(diǎn)點(diǎn)。為了賣弄一下我的英語能力,我從End文本框開始。第一件要做的事就是實(shí)施Self Encapsulate Field(171)。文本框的更新是通過getText()和setText()兩函數(shù)實(shí)現(xiàn)的,因此我所建立的訪問函數(shù)(accessors)需要調(diào)用這兩個(gè)函數(shù):
String getEnd() {
??? return _endField.getText();
}
void setEnd(String arg) {
??? _endField.setText(arg);
}
然后,找出_endField的所有引用點(diǎn),將它們替換為適當(dāng)?shù)脑L問函數(shù):
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)的標(biāo)準(zhǔn)過程。然后當(dāng)你處理GUI class時(shí),情況還更復(fù)雜些:用戶可以直接(通過GUI)修改文本框內(nèi)容,不必調(diào)用setEnd()。因此我需要在GUI class的事件處理函數(shù)中加上對setEnd()的調(diào)用。這個(gè)動(dòng)作把End文本框設(shè)定為其當(dāng)前值。當(dāng)然,這沒帶來什么影響,但是通過這樣的方式,我們可以確保用戶的輸入的確是通過設(shè)值函數(shù)(setter)進(jìn)行的:
void EndField_FocusLost(java.awt.event.FocusEvent event) {
??? setEnd(_endField.getText());
??? if(isNotInteger(getEnd()))
??? ?? setEnd("0");
??? calculateLength();
}
上述調(diào)用動(dòng)作中,我并沒有使用上一頁的getEnd()取得End文
本框當(dāng)前內(nèi)容,而是直接取用該文本框。之所以這樣做是因?yàn)椋S后的重構(gòu)將使上一頁的getEnd()從domain
object(而非文本框)身上取值。那時(shí)如果這里用的是getEnd()函數(shù),每當(dāng)用戶修改文本框內(nèi)容,這里就會(huì)將文本框又改回原值。所以我必須使用
[直接訪問文本框]的方式獲得當(dāng)前值。現(xiàn)在我可以編譯并測試值域封裝后的行為了。
現(xiàn)在,在domain class中加入_end值域:
class Interval...
??? private String _end = "0";
在這里,我給它的初始值和GUI class給它的初值是一樣的。然后我再加入取值/設(shè)值函數(shù)(getter/setter):
class Interval...
??? String getEnd() {
??? ?? return _end;
??? }
??? void setEnd(String arg) {
??? ?? _end = arg;
??? ?? setChanged();
??? ?? notifyObservers();
??? }
由于使用了Observer模式,我必須在設(shè)值函數(shù)(setter)
中加上[發(fā)出通告]動(dòng)作(即所謂notification
code)。我把_end聲明為一個(gè)字符串,而不是一個(gè)看似更合理的整數(shù),這是因?yàn)槲蚁M麑⑿薷牧繙p至最少。將來成功復(fù)制數(shù)據(jù)完畢后,我可以自由自在地于
domain class內(nèi)部把_end聲明為整數(shù)。
現(xiàn)在,我可以再編譯并測試一次。我希望通過所有這些預(yù)備工作,將下面這個(gè)較為棘手的重構(gòu)步驟的風(fēng)險(xiǎn)降至最低。
首先,修改IntervalWindow class的訪問函數(shù),令它們改用Interval對象:
class IntervalWindow...
??? String getEnd() {
??? ?? return _subject.getEnd();
??? }
??? void setEnd(String arg) {
??? ?? _subject.setEnd(arg);
??? }
同時(shí)也修改update()函數(shù),確保GUI對Interval對象發(fā)來的通告做出響應(yīng):
class IntervalWindow...
??? public void update(Observable observed, Object arg) {
??? ?? _endField.setText(_subject.getEnd());
??? }
這是另一個(gè)需要[直接取用文本框]的地點(diǎn)。如果我調(diào)用的是設(shè)值函數(shù)(setter),程序?qū)⑾萑霟o限遞歸調(diào)用(這是因?yàn)镮ntervalWindow的設(shè)
值函數(shù)setEnd()調(diào)用了Interval。setEnd(),一如稍早行所示:而Interval.setEnd()又調(diào)用
notifyObservers(),導(dǎo)致IntervalWindow.update()又被調(diào)用)。
現(xiàn)在,我可以編譯并測試,數(shù)據(jù)都恰如其分地被復(fù)制了。
另兩個(gè)文本框也如法炮制。完成之后,我可以使用Move Method(142)將calculateEnd()和calculateLength()搬到Interval class。這么一來,我就擁有一個(gè)[包容所有domain behavior和domain data]并與GUI code分離的domain class了。
如果上述工作都完成了,我就會(huì)考慮徹底擺脫這個(gè)GUI class。如果GUI class是個(gè)較為老舊的AWT
class,我會(huì)考慮將它換成一個(gè)比較好看的Swing class,而且后者的坐標(biāo)定位能力也比較強(qiáng)。我可以在domain
class之上建立一個(gè)Swing GUI。這樣,只要我高興,隨時(shí)可以去掉老舊的GUI class。
使用事件監(jiān)聽器(Event Listeners)
如果你使用事件監(jiān)聽器(event listener)而不是Observer/Observable模式,仍然可以實(shí)施Duplicate Observed Data(189)。這種情況下,你需要在domain model中建立一個(gè)listener class和一個(gè)event
class。然后,你需要對domain
object注冊listeners,就像前例對observable對象注冊observers一樣。每當(dāng)domain
object發(fā)生變化(類似上例的update()函數(shù)被調(diào)用),就向listeners發(fā)送一個(gè)事件(event)。IntervalWindow class可以利用一個(gè)inner class(內(nèi)嵌類)來實(shí)現(xiàn)監(jiān)聽器接口(listener interface),并在適當(dāng)時(shí)候調(diào)用適當(dāng)?shù)膗pdate()函數(shù)。