設計模式之Observer

Observer

tags: design pattern,Observer


要求:

模擬以下情景:

小孩在睡覺
醒了之後要吃東西

第一種設計方法

(說實話這是我第一反應想到的方法,我果然還是圖樣圖森破。。。)

有一個Dad類, 有一個Child類, Dad類持有Child類的引用, Dad監測着Child, 如果Child醒了, Dad就調用feed方法去喂小孩。

package simulation;

class Child implements Runnable {

    private boolean wakeUp = false;

    public void wakeUp() {
        this.wakeUp = true;
    }

    public boolean isWakeUp() {
        return this.wakeUp;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.wakeUp();
    }

}

class Dad implements Runnable {

    private Child child;

    public Dad(Child c) {
        this.child = c;
    }

    @Override
    public void run() {
        while (!child.isWakeUp()) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.feed(this.child);
    }

    public void feed(Child c) {
        System.out.println("feed child");
    }

}

public class Test {
    public static void main(String[] args) {
        Child c = new Child();
        new Thread(new Dad(c)).start();
    }
}

分析:
程序可行是可行, 但是有極其不合理的地方:Dad每隔一秒鐘看一下Child, 完全幹不了別的事。–> CPU的資源被無端消耗, 上面的代碼在效率和資源消耗上都有很大問題!!!

那麼應該如何改進呢?

第二種設計方法:

化主動爲被動!

把Dad主動監測Child變爲被動監測, 就是說, 反過來, 讓Child監測Dad。換句話說, 在Child睡覺的時候Dad可以幹別的事, 但Child一醒過來,Dad馬上過來喂他吃東西。

這時候把上面的代碼修改成下面的:

package simulation;

class Child implements Runnable {

    private Dad dad;
    // private boolean wakeUp = false;

    public Child(Dad d) {
        this.dad = d;
    }

    public void wakeUp() {
        // this.wakeUp = true;
        this.dad.feed(this);
    }

    // public boolean isWakeUp() {
    // return this.wakeUp;
    // }

    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.wakeUp();
    }

}

class Dad {

    public void feed(Child c) {
        System.out.println("feed child");
    }

}

public class Test {
    public static void main(String[] args) {
        Dad d = new Dad();
        Child c = new Child(d);
        new Thread(c).start();
    }
}

這時候Dad完全可以不用作爲一個線程類, Dad這個類裏也可以只保留feed這個方法, Child中持有Dad的引用, Child一醒過來, Dad就調用feed方法。

這樣修改之後, 明顯比第一種方法更具有效率。

但是作爲設計來講, 在一個程序當中, 如果只考慮當前而沒有預料到將來一定時間內將會發生的變化, 那麼程序不具有可擴展性,彈性很差。

比如上面這個情景, Child醒過來這件事,包含了許多信息:幾點醒過來?是在早上還是在晚上?
睡了多久?在哪裏醒過來?等等。 針對不同的事件信息,作爲監測者的Dad應該有不同的處理方式, 不能說一醒過來就喂, 如果是晚上Child剛吃完飯睡了一下, 醒過來, Dad又喂他吃東西, 那麼Child就撐死了。

所以第二種方法僅僅是把程序寫通,可擴展性很差。

對於事件的處理

Child醒過來這件事的發生包含了許多具體情況(具體信息),應該把這些情況告訴監測者, 也就是Dad, Dad根據這件事情的具體情況, 來做出具體的處理方式。

因此把事件抽象出來,封裝成另外一個類。

package simulation;

class WakeUpEvent {

    private long time;
    private String location;
    private Object source;

    public WakeUpEvent(long time, String location, Object source) {
        this.time = time;
        this.location = location;
        this.source = source;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public Object getSource() {
        return source;
    }

    public void setSource(Object source) {
        this.source = source;
    }

}

class Child implements Runnable {

    private Dad dad;

    // private boolean wakeUp = false;

    public Child(Dad d) {
        this.dad = d;
    }

    public void wakeUp() {
        // this.wakeUp = true;
        this.dad.actionToWakeUp(new WakeUpEvent(System.currentTimeMillis(),
                "bed", this));
    }

    // public boolean isWakeUp() {
    // return this.wakeUp;
    // }

    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.wakeUp();
    }
}

class Dad {

    public void actionToWakeUp(WakeUpEvent event) {
        // do something according to the event
    }

}

public class Test {
    public static void main(String[] args) {
        Dad d = new Dad();
        Child c = new Child(d);
        new Thread(c).start();
    }
}

注意上面WakeUpEvent這個類裏面, 屬性除了有timelocation之外還有一個source, 而且類型是Object(其實也可以寫成Child類型, 但爲了更像AWT, 所以寫成Object),這裏表示一個事件源對象,就是發生這件事的對象。比如說Child醒了這個事件, Child就是事件源。

然後再思考:Child一醒過來Dad就要喂他, 那麼喂這個動作就已經被固定下來了。假如Child醒來之後不想讓Dad喂他, 而是想讓Dad抱他出去玩,那麼明顯feed這個方法已經不合適了。更靈活的方法是:Child一醒過來, 發生了這麼一件事, Dad便對這件事做出反應,至於是喂他還是抱他出去玩都可以。

所以把Dad這個類中原來的feed方法修改了,方法名改成了actionToWakeUp,參數改成了WakeUpEvent event 在方法體內, 便可以增加判斷,根據事件的不同具體信息做出不同的反應。這樣寫明顯比使用feed方法靈活得多。

繼續思考:如果現在不只有Dad, Child醒過來之後,他的Grandpa也要做出反應,那麼應該怎麼辦呢?按照之前的思路, 增加一個Grandpa類, 裏面也有一個actionToWakeUp(WakeUpEvent event)方法,另外在Child這個類裏增加一個Grandpa的引用。那麼如果現在不僅是爸爸、爺爺對小孩醒過來做出反應,小孩的媽媽、奶奶、外公、外婆甚至是家裏的狗都要做出反應呢?那豈不是要不斷的修改Child這個類的源代碼?!在OO裏面有一個極其重要的核心原則————OCP, open close principle, 開閉原則, 對擴展開放, 對修改關閉。上面的方法要不斷修改Child的源代碼,顯然是不符合這個原則的, 說明了設計還不到位!

第三種設計方法 Observer

現在有好多好多監測小孩醒過來這件事的人,而且每個人對這件事的響應各不相同,但是有一個共同點,可以把響應的方法都叫actionToWakeUp(WakeUpEvent event),參數都是小孩醒過來這件事。這時候可以考慮使用接口把變化的這部分給抽象出來,因爲接口抽象出來的是一系列的類所具有的共同特點。
因此增加一個WakeUpListener的接口,裏面有actionToWakeUp(WakeUpEvent event)這個方法。 然後讓Dad和Grandpa這兩個類實現這個接口。

這時候再思考一下,爲什麼這個方法裏傳的參數是WakeUpEvent而不是Child? 假如寫成Child, 那麼這個方法就只能用在小孩身上了,但是如果是WakeUpEvent,那麼小狗醒了也可以用這個方法,小貓醒了也可以用這個方法,也就是說, 事件本身也是和事件源是脫離的。這個時候靈活性最高!

把上面的代碼修改:

package simulation;

import java.util.*;

class WakeUpEvent {

    private long time;
    private String location;
    private Object source;

    public WakeUpEvent(long time, String location, Object source) {
        this.time = time;
        this.location = location;
        this.source = source;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public Object getSource() {
        return source;
    }

    public void setSource(Object source) {
        this.source = source;
    }

}

class Child implements Runnable {

    private List<WakeUpListener> listeners = new ArrayList<WakeUpListener>();

    public void addWakeUpListener(WakeUpListener listener) {
        this.listeners.add(listener);
    }

    public void wakeUp() {

        for (Iterator<WakeUpListener> it = this.listeners.iterator(); it
                .hasNext();) {
            WakeUpListener l = it.next();
            l.actionToWakeUp(new WakeUpEvent(System.currentTimeMillis(), "bed",
                    this));
        }
    }

    // public boolean isWakeUp() {
    // return this.wakeUp;
    // }

    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.wakeUp();
    }

}

interface WakeUpListener {
    public void actionToWakeUp(WakeUpEvent event);
}

class Dad implements WakeUpListener {

    public void actionToWakeUp(WakeUpEvent event) {
        System.out.println("feed child");
    }
}

class Grandpa implements WakeUpListener {
    public void actionToWakeUp(WakeUpEvent event) {
        System.out.println("hug child");
    }
}

public class Test {
    public static void main(String[] args) {
        Dad d = new Dad();
        Child c = new Child();
        c.addWakeUpListener(d);
        new Thread(c).start();
    }
}

注意:這時候Child這個類裏沒有Dad或者Grandpa的引用, 而是改成了一個List<WakeUpListener>, 並且多了一個addWakeUpListener(WakeUpListener listener)的方法,當需要添加監聽器的時候,添加一個實現了WakeUpListener這個接口的類, 在main方法裏new一個新的監聽器對象,然後直接調用這個方法添加便可以,完全無需修改Child的代碼。擴展程序而無需修改Child的源代碼, 這才符合OCP原則!

比如我還想添加一個小狗, 他也在監聽着小孩醒來這件事:

package simulation;

import java.util.*;

class WakeUpEvent {

    private long time;
    private String location;
    private Object source;

    public WakeUpEvent(long time, String location, Child source) {
        this.time = time;
        this.location = location;
        this.source = source;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public Object getSource() {
        return source;
    }

    public void setSource(Object source) {
        this.source = source;
    }

}

class Child implements Runnable {

    private List<WakeUpListener> listeners = new ArrayList<WakeUpListener>();

    public void addWakeUpListener(WakeUpListener listener) {
        this.listeners.add(listener);
    }

    public void wakeUp() {

        for (Iterator<WakeUpListener> it = this.listeners.iterator(); it
                .hasNext();) {
            WakeUpListener l = it.next();
            l.actionToWakeUp(new WakeUpEvent(System.currentTimeMillis(), "bed",
                    this));
        }
    }

    // public boolean isWakeUp() {
    // return this.wakeUp;
    // }

    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.wakeUp();
    }

}

interface WakeUpListener {
    public void actionToWakeUp(WakeUpEvent event);
}

class Dad implements WakeUpListener {

    public void actionToWakeUp(WakeUpEvent event) {
        System.out.println("feed child");
    }
}

class Grandpa implements WakeUpListener {
    public void actionToWakeUp(WakeUpEvent event) {
        System.out.println("hug child");
    }
}


class Dog implements WakeUpListener {
    public void actionToWakeUp(WakeUpEvent event) {
        System.out.println("Wang!!!!");
    }
}


public class Test {
    public static void main(String[] args) {
        Dad d = new Dad();
        Grandpa g = new Grandpa();
        Child c = new Child();
        c.addWakeUpListener(d);
        c.addWakeUpListener(g);

        Dog dog = new Dog();
        c.addWakeUpListener(dog);

        new Thread(c).start();
    }
}

由上面的代碼可以看見, 只是新建了一個Dog的類, 然後再在main方法裏添加了

        Dog dog = new Dog();
        c.addWakeUpListener(dog);

這兩句話,其他的完全沒有修改。主要的邏輯類Child完全沒有變,可擴展性很高!

再進一步思考!假如我現在有一個Student類,他也可以發出WakeUpEvent這件事,那麼我只需要複製Child裏的代碼到Student這個類裏面就可以了,WakeUpEvent這個類也得到了複用,靈活性更高!!!想想AWT裏面,除了Button這個類會發出ActionEvent這件事, TextField也會, 因此ActionEvent也被重用了。在這裏,Button相當於Child, Textfield相當於Student, ActionEvent相當於WakeUpEvent。

另外還可以封裝一個CryEvent類,HappyEvent類等等各種各樣的event,然後再封裝一個abstract class Event,讓各種event從這個抽象類繼承,方法傳參數的時候形參定義爲Event類型,這樣靈活性更強。

反思與總結

從一開始第一種設計方法的一兩個類實現了基本功能,到最後的多個類、接口,可以看到:使用設計模式是一把雙刃劍。

  • 優點:

    1. 可擴展性強
    2. 維護成本降低
  • 缺點:

    1. 複雜度增加
    2. 開發成本增加

但是切忌爲了使用設計模式而使用設計模式,要具體問題具體分析

發佈了19 篇原創文章 · 獲贊 18 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章