一步一步将自己的代码转换为观察者模式
?????之前有發表博文,簡單的講解一下觀察者模式的大概內容(http://www.cnblogs.com/wenjiang/archive/2013/05/07/3065040.html),主要是利用java對觀察者模式的內置支持來實現觀察者模式,現在想要換個思路,自定義觀察者模式。
???? 這次使用Eclipse的單元測試框架,前面那個例子就不適合了,所以特意挑一個有關時鐘報時的例子,方便測試。
???? 敏捷開發的原則就是測試先于代碼,這里就采用這個原則,先從測試代碼開始:
public class ClockTest extends TestCase {private TimeScreen screen;private TimeSource source;public ClockTest(String name) {super(name);
}
public void testTimeChange() { TimeSource source = new TimeSource();
TimeScreen screen = new TimeScreen();
Clock clock = new Clock(source, screen);
source.setTime(3, 4, 5);
assertEquals(3, screen.getHours());
assertEquals(4, screen,getMinutes());
assertEquals(5,screen.getSeconds());
} }
????? 該測試主要測試:當時鐘時間改變時,屏幕能否跟著改變。
??????因為時鐘可能是電子時鐘或者其他時鐘,所以,我們定義一個時間來源的抽象:Source:
????? 同樣屏幕也要有一個抽象:
public interface Screen{public void setTime(itn hours, int minutes, int seconds); }????? 接著是Clock的代碼:
public class Clock{priate Screen screen;public Clock(Source source, Screen screen){source.setClock(this);this.screen = screen;}public void update(int hours, int minutes, int seconds){screen.setTime(hours, minutes, seconds);} }????? Clock通知屏幕更新時間,所以它必須擁有Screen的運用,又因為它是從Source獲取時間,所以它必須將自己傳給Source,也就是說,它是Screen和Source之間的郵差。
??????我們來實現具體的Screen和Source:
public class TimeSource implements Source{private Clock clock;public void setTime(int hours, int minutes, int seconds){clock.update(hours, minutes, seconds);}public void setClock(Clock clock){this.clock = clock;} }?
public class TimeScreen implements Screen{private int hours;private int minutes;private int seconds;public int getHours(){return this.hours;}public int getMinutes(){return this.minutes;}public int getSeconds(){return this.seconds;}public void setTime(int hours, int minutes, int seconds){
this.hours = hours;
this.minutes = minutes;
this.seconds = seconds;
}
}
????? ?UML圖如:
??????
????? 上面的代碼能夠通過測試,但并不是一個好方案,最主要的問題就是TimeSource持有Clock的引用。Clock確實是Source和Screen的郵差,但我們并不依賴于具體的郵差幫我們傳送數據,郵差本身也可以是一個抽象:
public interface TimeObserver{public void update(int hours, int minutes, int seconds); }????? 為了貼近今天的主題,特意將這個抽象命名為TimeObserver,因為它就是一個觀察者,觀察數據什么時候改變,然后通知相應的屏幕。
???? 然后就是實現這個抽象:
public class Clock implements TimeObserver{private Screen screen;public Clock(Source source, Screen screen){source.setObserver(this);this.screen = screen;}public void update(int hours, int minutes, int seconds){screen.setTime(hours, minutes, seconds);} }????? 接著就是將原本引用Clock的地方都改為TimeObserver就行,像是下面這樣:
public interface Source{public void setObserver(TimeObserver observer); }????? 加入這樣的抽象的好處非常明顯,就是消除我們之前的依賴,也就是通過提供間接層的方式消除依賴的做法,這也是接口的作用。
?????
????? 通過查看代碼,我們發現TimeObserver的update()其實就是調用Screen的setTime(),這是因為它必須通知Screen修改顯示的時間,那么我們是否可以直接將Screen傳遞給Source的方法,而不是像之前那樣需要TimeObserver?顯然我們的測試代碼需要進一步修改,修改的地方只有一處:
source.setObserver(screen);?????? 然后是我們的Source:
public class TimeScreen implements TimeObserver{private int hours;private int minutes;private int seconds;public int getHours(){return this.hours;}public int getMinutes(){return this.minutes;}public int getSeconds(){return this.seconds;}public void update(int hours, int minutes, int seconds){this.hours = hours;this.minutes = minutes;this.seconds = seconds;} }??????
????? 為什么我們一開始會有一個Clock呢?因為我們需要一個郵差,但為什么不讓我們的Source直接通知Screen呢?我們經常會犯這樣的錯誤,尤其是在使用接口的時候,認為凡事有個間接層都是好的,都是動態的,其實不然,接口的確是個好東西,但是,該讓誰實現這個接口就是一個問題。我們很容易像是上面一樣,引入了一個具體類型Clock,而且代碼的運行也沒有錯,它依然能夠工作得很好,我們還可以和別人炫耀:看看我的郵差工作得多努力!
?????如何設計好接口,是面向對象編程中一個很重要的努力方向。
?????我的個人經驗,當然,這經驗是微不足道的(面向對象編程經驗只有一年OTZ),如果兩個類型之間需要進行通信,應該是讓它們的抽象之間進行通信,也就是它們各自實現的接口,這樣就能消除具體類型的耦合,而且這種通信的方式是以傳參的方式進行。這樣,我們的對象層次上既保證一定的解耦,又能保證邏輯上的耦合關系的完整。
?????之前的測試實在是太簡單了,只是單獨測試一個Screen,現實生活中的情況是非常復雜的,我們很可能需要多個屏幕,而且它們是不同材質,不同地方,這需要增加一個測試:
public void testMultipleScreens(){TimeSource source = new TimeSource();TimeScreen screen = new TimeScreen();source.registerObserver(screen);TimeScreen screen2 = new TimeScreen();source.registerObserver(screen2);source.setTime(3, 4, 5);assertScreenEquals(screen, 3, 4, 5);assertScreenEquals(screen2, 3, 4, 5); } private void assertScreenEquals(TimeSource source, int hours, int minutes, int seconds){assertEquals(hours, screen.getHours());assertEquals(minutes, screen.getMinutes());assertEquals(seconds, screen.getSeconds()); }????? 我們在Source里增加了一個方法:registerObserver(),正如其名,就是將相關的Screen注冊進Source需要通知的名單中,新的Source如:
public interface Source{public void registerObserver(TimeObserver observer); }????? 接著我們的TimeSource如:
public class TimeSource implements Source{private List<TimeObserver> observers = new ArrayList<TimeObserver>();public void registerObserver(TimeObserver observer){list.add(observer);}public void setTime(int hours, int minutes, int seconds){for(TimeObserver observer : observers){observer.update(hours, minutes, seconds);}} }??????我們用ArrayList來作為存儲需要通知的Screen的名單,然后在時間更新的時候,逐個通知它們更新自己的時間。
????? 但問題也來了,任何一個Source的實現類都必須實現注冊和更新的代碼,哪怕它們都是一樣的。這樣代碼的重復性太高了,我們得想辦法解決這個問題。
????? 將Source從接口變為類型就可以解決了:
public class Source{private List<TimeObserver> observers = new ArrayList<TimeObserver>();protected void notify(int hours, int minutes, int seconds){for(TimeObserver observer : observers){observer.update(hours, minutes, seconds);}}public void registerObserver(TimeObserver observer){observers.add(observer);} }?????? 然后我們的TimeSource只需要這樣:
public class TimeSource extends Source{public void setTime(int hours, int minutes, int seconds){notify(hours, minutes, seconds);} }??????我們的派生類型的確是不需要重新寫注冊和更新的代碼,只要調用基類的相關方法就行。
?????
??????從上面我們可以知道,接口可以為我們提供間接層,減少具體類型的依賴,使得我們的代碼更具動態,但是,它會使我們面臨代碼重復性較高的危險,更可怕的是,它會讓我們陷入這樣的怪論:"只要能呱呱叫,就是鴨子"。這是面向對象編程的一個經典現象,因為所有實現類都要實現接口規定的方法,而且我們不能阻止非目標類型對該接口的實現。
?????使用繼承可以解決上面的怪論:"只有鴨子才能呱呱叫"。這是繼承的本質,它規定的是一種類型,而不是一組行為協定。當然,接口也有自己的對策:將行為協議劃分得更細,最好就是一組相關的行為放到一個接口里。前面之所以會出現這樣的怪論,是因為程序員可能會這樣設計接口:
public interface Duck{public void fly();public void shout(); }???? 這樣的接口就會讓人產生誤解,正確的接口應該是這樣的:
public interface FlyAble{public void fly(); }public interface ShoutAble{public void shout(); }???? 接口的命名應該是動詞,而不是名詞,因為它規定的是一組行為協議。
???? 但繼承也存在自己的問題:"不是所有的鴨子都會呱呱叫",有些鴨子可能不會叫,但是它們是有方法可以呱呱叫的,這就會出現錯誤。
?????所以,使用繼承解決問題的時候,我們必須明確一點:派生類能從基類中繼承的職責到底是什么?
???? 在這里,很明確的就是,我們的Source根本就沒有必要理會注冊和更新的行為,它本來應該只知道時間而已。于是,我們需要將這部分的職責從Source中移除。
???? 使用委托是一個不錯的選擇:
public class TimeNotify{private List<TimeObserver> observers = new ArrayList<TimeObserver>();public void registerObserver(TimeObserver observer){list.add(observer);}public void setTime(int hours, int minutes, int seconds){for(TimeObserver observer : observers){observer.update(hours, minutes, seconds);}} } public class TimeSource implements Source{private TimeNotify notify = new TimeNotify();public void registerObserver(TimeObserver observer){notify.registerObserver(observer);}public void setTime(int hours, int minutes, int seconds){notify.notify(hours, minutes, seconds);} } public class TimeNotify{private List<TimeObserver> observers = new ArrayList<TimeObserver>();public void registerObserver(TimeObserver observer){list.add(observer);}public void setTime(int hours, int minutes, int seconds){for(TimeObserver observer : observers){observer.update(hours, minutes, seconds);}} } public class TimeSource implements Source{private TimeNotify notify = new TimeNotify();public void registerObserver(TimeObserver observer){notify.registerObserver(observer);}public void setTime(int hours, int minutes, int seconds){notify.notify(hours, minutes, seconds);} } public class TimeNotify{private List<TimeObserver> observers = new ArrayList<TimeObserver>();public void registerObserver(TimeObserver observer){list.add(observer);}public void setTime(int hours, int minutes, int seconds){for(TimeObserver observer : observers){observer.update(hours, minutes, seconds);}} } public class TimeSource implements Source{private TimeNotify notify = new TimeNotify();public void registerObserver(TimeObserver observer){notify.registerObserver(observer);}public void setTime(int hours, int minutes, int seconds){notify.notify(hours, minutes, seconds);} }?????? 使用委托是增加了一個間接層,專門用于負責注冊和更新的具體實現,而我們的Source只要調用它的相應方法就行。
??????
?????? 哦,間接層怎么又來了!明明開頭我們消除了一個郵差,現在又來了個新的郵差!!此郵差非彼郵差。之前的郵差是因為我們的Source的具體類型要持有一個郵差的引用才能通知Screen的具體類型,但是事實就是Source的具體類型應該可以直接通知Screen的具體類型,這是職責的分離。但這里我們是職責過分集中在一個類型中,所以需要通過間接層將職責分離出去。
????? 我們知道,這樣的解釋實在是太模糊了!同樣是郵差,為什么一個郵差要被趕走,另一個郵差卻要被雇傭,而且評價甚高!!這不公平!!!仔細想想它們的工作就知道了,之前的郵差它負責的工作是更新數據,而且還是命令Screen更新!!這就是冗余,所以它才會被趕走,但是現在這個郵差卻負責了新的工作:通知Screen更新數據和注冊新的Screen,Source的工作僅僅是命令它做事而已。這樣辛苦工作的郵差怎么可能被炒呢!!
????? 現在的我們已經將整個觀察者實現出來了,只要將Source改為TimeSubject就行,因為在觀察者模式中,被觀察的就是Subject,而java中習慣的命名方式是TimeObservable。我們這里采用的是"推模型",也就是通過把數據傳給notify和update方法從而把數據從Subject推給觀察者Observer,而另一種方式"拉模型"是Observer在收到更新消息后,查詢Subject得到。該使用哪種方式,就在于Observer是否知道是哪個Subject發生變化(Subject可以是多個),如果確定的話,可以使用"拉模型",否則使用"推模型"比較方便。
????? 下面就是觀察者模式的大概UML圖:
?????
??????觀察者模式是一個非常好用的設計模式,它應用的范圍非常廣泛,解決了很多設計問題,而且存在各種變形,但萬變不離其宗,只要我們謹記模式的意圖,就能在我們毫無頭緒的時候指點迷津,尤其是在一開始設計類的時候,如果畫一下UML圖,就會發現我們可以用觀察者模式來解決這個問題。
?????
轉載于:https://www.cnblogs.com/wenjiang/p/3149990.html
總結
以上是生活随笔為你收集整理的一步一步将自己的代码转换为观察者模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: event级别设置Resumable S
- 下一篇: virtual hust 2013.6.