設計模式是對軟件編程領域內方法和技巧的總結,能有效的提高代碼的可閱讀性,複用性,可擴展性和可維護性,是軟件工程的基石。設計模式也是計算機專業應屆生求職過程中最常見一個類題,單例,觀察者模式又是最常見的題目。因此熟練掌握和使用設計模式是每一個軟件開發人員必備的技能。
引入:
題目:請用面向對象的編程方式實現以下功能:貓叫了,老鼠被嚇跑了,主人被吵醒。(PS:當年我面試時遇到該題不下3次)
當年我學習過C++和Java編程語言,但是沒有項目經驗,更沒有學習過設計模式,我的實現代碼是:
//貓類
<span style="color:#000000;">public class Cat {
private String name;
public Cat(){}
public Cat(String Name){
this.name = Name;
}
public String getName(){
return name;
}
public void setName(String Name){
name = Name;
}
public void Miao(){
System.out.println("Cat "+getName() + " is shout: Miao miao!");
}
}</span>
//老鼠類
<span style="color:#000000;">public class Mouce {
private String name;
public Mouce(){}
public Mouce(String Name){
this.name = Name;
}
public String getName(){
return this.name;
}
public void setName(String Name){
this.name = Name;
}
public void run(){
System.out.println("Mouce "+getName()+" is running!");
}
}</span>
//主人類
<span style="color:#000000;">public class Host {
public Host(){
}
public void awake(){
System.out.println("Host is awaking!");
}
}</span>
//測試執行類
<span style="color:#000000;">public class Main {
public static void main(String[] args) {
Cat cat = new Cat("Jerry");
Mouce mouce = new Mouce("Tom");
Host host = new Host();
cat.Miao();
mouce.run();
host.awake();
}
}</span>
有沒有采用面向對象?有。有沒有實現貓叫,老鼠跑,主人醒?有。但是仔細看看實體類,Cat只有一個Miao方法,Mouce只有一個run方法,Host只有一個awake方法,而測試執行類創建了三個類的實例再分別調用對應的方法,實際上是打着面向對象的旗幟,幹着面向方法的事情。
貓一叫,老鼠被嚇跑了,主人被驚醒了,整個過程應該由貓來觸發,每次貓的Miao方法被調用後就應該觸發老鼠的run方法以及主人的awake方法;貓又如何知道是哪隻老鼠,哪個主人呢?如果貓持有老鼠和主人的引用,實現起來就非常簡單了。(PS:還要修改測試執行類)
//貓類
<span style="color:#000000;">public class Cat {
private String name;
private Mouce mouce;
private Host host;
public Cat(){}
public Cat(String Name){
this.name = Name;
}
public String getName(){
return name;
}
public void setName(String Name){
name = Name;
}
public void setMouce(Mouce mouce){
this.mouce = mouce;
}
public void setHost(Host host){
this.host = host;
}
public void Miao(){
System.out.println("Cat "+getName() + " is shout: Miao miao!");
mouce.run();
host.awake();
}
}</span>
//測試執行類
<span style="color:#000000;">public class Main {
public static void main(String[] args) {
Mouce mouce = new Mouce("Tom");
Host host = new Host();
Cat cat = new Cat("Jerry");
cat.setMouce(mouce);
cat.setHost(host);
cat.Miao();
}
}</span>
測試輸出:
如果又有新的要求:老鼠和主人的個數不確定,可以在程序運行中動態的調整,該如何實現?
很顯然,直接在cat中定義老鼠和主人的實例引用無法滿足程序運行中調整老鼠和主人的需求;但是我們知道集合的大小是可變的,因此我們可以在cat中定義一個集合類型的變量,該集合既可以容納老鼠實例,既可以容納主人實例。什麼集合既可以容納老鼠,又可以容納主人呢,根據面向對象編程的特性之一多態(父類引用指向子類對象),可以很容易實現這一點,因此可以抽象出一個接口/基類Observer,讓Mouce和Host都繼承自Observer, cat中擁有List<Observer>的實例。
//Mouce,Host的父接口
<span style="color:#000000;">public interface Observer {
public void Update();
}</span>
//老鼠類
<span style="color:#000000;">public class Mouce implements Observer {
//其他部分和上面Mouce類一樣
@Override
public void Update() {
run();
}
}</span>
//主人類
<span style="color:#000000;">public class Host implements Observer {
//其他部分和上面Host類一樣
@Override
public void Update() {
awake();
}
}</span>
//貓類
<span style="color:#000000;">import java.util.*;
public class Cat {
private String name;
private List<Observer> Observers;
public Cat(){
this.Observers = new ArrayList<Observer>();
}
public Cat(String Name){
this.name = Name;
this.Observers = new ArrayList<Observer>();
}
public String getName(){
return name;
}
public void setName(String Name){
name = Name;
}
public void Register(Observer o){
if(!Observers.contains(o)){
Observers.add(o);
}
}
public void unRegister(Observer o){
int index =Observers.indexOf(o);
if(index >= 0){
Observers.remove(index);
}
}
public void Miao(){
System.out.println("Cat "+getName() + " is shout: Miao miao!");
for(int i=0;i<Observers.size(); i++){
Observer obs = Observers.get(i);
obs.Update();
}
}
}</span>
//測試執行類
<span style="color:#000000;">public class Main {
public static void main(String[] args) {
Mouce mouce = new Mouce("Tom");
Mouce mice = new Mouce("Mice");
Host host = new Host();
Cat cat = new Cat("Jerry");
//兩隻老鼠被嚇跑,主人被吵
cat.Register(mouce);
cat.Register(mice);
cat.Register(host);
cat.Miao();
System.out.println("-------");
//貓第二次叫,只嚇跑一隻老鼠
cat.unRegister(host);
cat.unRegister(mice);
cat.Miao();
}
}</span>
測試執行結果:
到此爲止,一個簡單的觀察者小實例就完成了,貓是一個被觀察者,內部定義一個觀察者集合實例,並提供觀察者訂閱和取消訂閱觀察的方法(cat類內部的register和unregister)。被觀察者行爲或狀態發生改變時通知觀察者(cat.Miao方法內步遍歷集合,調用每個觀察者的update方法)。
數據傳遞:
在很多情況下,觀察者需要獲取被觀察者的數據變化,觀察者取得數據有兩種方式:推方式和拉方式,換句話說就是被動接收數據和主動提取數據。
推方式:在被觀察者通知觀察者時,將需要處理的數據按照一定的方式傳遞給觀察者,觀察者接收後在進行處理。
拉方式:在被觀察者通知觀察者時,將被觀察者自身的引用傳遞給觀察者,觀察者通過被觀察者暴露的方法提取和處理數據。
下面對上述類進行適當修改,在一個例子中將綜合使用推方式和拉方式。
//Mouce,Host的父接口
<span style="color:#000000;">public interface Observer {
public void Update(Cat c, String name);
}</span>
//老鼠類
<span style="color:#000000;">public class Mouce implements Observer {
//其他部分和上面Mouce類一樣
@Override
public void Update(Cat c, String name) {
<span style="color:#ff0000;">//這裏使用推方式,接收被觀察者傳遞來的數據name
</span> System.out.println(name);
run();
}
}</span>
//主人類
<span style="color:#000000;">public class Host implements Observer {
//其他部分和上面Host類一樣
@Override
public void Update(Cat c, String name) {
<span style="color:#ff0000;">//這裏使用拉方式,通過被觀察者的引用來獲取需要處理的數據
</span> System.out.println(c.getName());
awake();
}
}</span>
老鼠類使用了推方式來接收數據,主人類使用了拉方式來接收數據。雖然傳遞的數據是簡單的String類型,但足以說明推方式和拉方式的區別。 在實際應用中,如果需要處理的數據比較複雜,可能會將要傳遞的數據進行封裝,封裝成一個新的業務對象來傳遞,也可能會結合使用推方式和拉方式。
PS:本文簡單的介紹了觀察者模式的結構:1)被觀察者擁有觀察者關心的內容;2)所有觀察者擁有相同的父接口或繼承自相同的父類;3)被觀察者擁有一個集合類型的實例變量,用以保存註冊的觀察者; 4)被觀察者提供註冊和取消註冊的方法,供外部類訂立訂閱或取消訂閱;5)被觀察者的行爲或狀態發生改變時能通知所有觀察者; 6)被觀察者通知觀察者數據有兩種方式:推方式和拉方式。
本文是自己對觀察者模式學習的總結,方便自己回顧知識點,同時也希望能給廣大初學者帶來幫助。觀察者模式的內容還有其他方面,如方法的異步調用,有興趣的童鞋不妨自己深入研究。