設(shè)計模式——觀察者模式

在日常生活中嗜憔,觀察者模式與我們息息相關(guān)木缝,就比如愛打游戲的我在操控虛擬角色時便锨,這個角色有hp和mp,在游戲中會有一些對應(yīng)的面板來展示hp和mp氨肌,這個時候其實就是利用了觀察者模式鸿秆,具體是怎么實現(xiàn)的呢?請往下看怎囚。
就拿我最先接觸到的游戲的傳奇來說卿叽,一般的,在游戲中恳守,展示人物hp和mp有多個地方


image.png

假設(shè)游戲角色被怪物只因打了一下考婴,此時三個地方需要發(fā)生變化,角色上方血條催烘,圓盤血條沥阱,和數(shù)字血條。那我們?nèi)绾卧O(shè)計呢伊群?
不考慮其他因素考杉,最簡單的設(shè)計就是當(dāng)人物掉血時,改變?nèi)齻€地方舰始,代碼如下:崇棠、

class Role {
    private String name;
    private Integer hp;
    private Integer mp;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getHp() {
        return hp;
    }

    public void setHp(Integer hp) {
        this.hp = hp;
        // 改變?nèi)宋镅縣p
        System.out.println("血條更新hp為:"+hp);
        System.out.println("球型更新hp為:"+hp);
        System.out.println("面板更新hp為:"+hp);
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }
}

class Monster {
    public void attact(Role r){
        r.setHp(r.getHp()-10);
    }
}

public class Test {
    public static void main(String[] args) {
        Role role = new Role();
        role.setName("qiansion");
        role.setHp(100);
        role.setMp(100);
        Monster monster = new Monster();
        monster.attact(role);
    }
}

輸出如下:
血條更新hp為:100
球型更新hp為:100
面板更新hp為:100
血條更新hp為:90
球型更新hp為:90
面板更新hp為:90
看似沒有什么問題,但是現(xiàn)在變化來了丸卷。產(chǎn)品經(jīng)理需要再添加一個顯示血量的地方
那么就需要在以前的代碼中枕稀,繼續(xù)新添加代碼,這樣不好

  1. 不符合開閉原則
  2. 就是上面代碼本身耦合性就太高


    image.png

    鑒于以上問題,我們需要重新設(shè)計一下整個模型萎坷,在之前的代碼中凹联,面板,球型哆档,血條顯示都是在人物的類中蔽挠,現(xiàn)在可以將他們抽取出來。等人物hp發(fā)生了變化虐呻,然后通知各個組件改變相應(yīng)的數(shù)據(jù)象泵。而兩個類之間傳遞數(shù)據(jù)只有兩種模式就是 pull 和 push寞秃,在本例中斟叼,明顯push更為方便(因為pull需要各個組件不停的詢問人物血條是否變化,內(nèi)耗過大且實時性較差)春寿,當(dāng)人物血條發(fā)生變化朗涩,人物主動的push數(shù)據(jù)到其他組件。而其他組件就等著人物來push绑改,所以我們可以稱這些組件為“觀察者”

現(xiàn)在谢床,我們可以著手重新設(shè)計Role類了,首先思考Role血量發(fā)生變化厘线,他需要通知到各個觀察者們识腿,那么Role類就必須知道觀察者都有哪些,所以需要一個集合去存放這些觀察者們造壮,相應(yīng)的渡讼,也需要一些操作觀察者的方法,當(dāng)然最重要的還是通知方法耳璧。

觀察者們除了關(guān)注人物血條的變化成箫,還得接受數(shù)據(jù)(人物當(dāng)前血量),面板旨枯,球型蹬昌,血條都需要接收這個數(shù)據(jù),所以可以抽出一個接受數(shù)據(jù)的接口攀隔,讓三個組件去實現(xiàn)皂贩。

綜上,代碼如下

class Role {
    private String name;
    private Integer hp;
    private Integer mp;
    private List<Observer> observers = new ArrayList<Observer>();

    public void addObserver(Observer obj){
        observers.add(obj);
    }

    public void removeObserver(Object obj){
        observers.remove(obj);
    }

    public void notifyObservers(){
        for (Observer observer : observers) {
            observer.update(hp);
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getHp() {
        return hp;
    }

    public void setHp(Integer hp) {
        this.hp = hp;
        notifyObservers();
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }
}

interface Observer{
    // 需要一個方法昆汹,來接受主體發(fā)來的新數(shù)據(jù)
    void update(int hp);
}

class Panel implements Observer{

    @Override
    public void update(int hp) {
        System.out.println("面板血條更改為:"+ hp);
    }
}

class BallPanel implements Observer{

    @Override
    public void update(int hp) {
        System.out.println("球形血條更改為:"+ hp);
    }
}

class HeadPanel implements Observer{

    @Override
    public void update(int hp) {
        System.out.println("頭部血條更改為:"+ hp);
    }
}

class Monster {
    public void attact(Role r){
        r.setHp(r.getHp()-10);
    }
}

public class Test {
    public static void main(String[] args) {
        Role role = new Role();
        role.setName("qiansion");
        role.setHp(100);
        role.setMp(100);
        Panel panel = new Panel();
        BallPanel ballPanel = new BallPanel();
        HeadPanel headPanel = new HeadPanel();
        role.addObserver(panel);
        role.addObserver(ballPanel);
        role.addObserver(headPanel);
        Monster monster = new Monster();
        monster.attact(role);
    }
}

輸出如下:
面板血條更改為:90
球形血條更改為:90
頭部血條更改為:90
跟之前的代碼相比明刷,明顯耦合度降低了,符合了單一職責(zé)筹煮。產(chǎn)品經(jīng)理如果需要新添加一個展示血條的地方遮精,那么可以新建一個類實現(xiàn)Observer接口,并將該類添加到role的觀察者列表中。
現(xiàn)在仍然存在一個缺點:
目前主體Role只會把自己的hp廣播給所有的觀察者本冲,那么如果想也把mp一起廣播呢准脂?勢必要違反開閉原則!檬洞!而且游戲業(yè)務(wù)經(jīng)常變化狸膏,導(dǎo)致Role類的屬性越來越多,難道每次多一個屬性添怔,都要修改Observer的update 方法嗎湾戳?

顯然,上面的方法過于簡漏广料,在Observer的update方法里直接把Role對象傳進去即可解決砾脑,組件想展示什么就能夠展示什么

class Role {
    private String name;
    private Integer hp;
    private Integer mp;
    private List<Observer> observers = new ArrayList<Observer>();

    public void addObserver(Observer obj){
        observers.add(obj);
    }

    public void removeObserver(Object obj){
        observers.remove(obj);
    }

    public void notifyObservers(){
        for (Observer observer : observers) {
            observer.update(this);
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getHp() {
        return hp;
    }

    public void setHp(Integer hp) {
        this.hp = hp;
        notifyObservers();
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }

    @Override
    public String toString() {
        return "Role{" +
                "name='" + name + '\'' +
                ", hp=" + hp +
                ", mp=" + mp +
                '}';
    }
}

interface Observer{
    // 需要一個方法,來接受主體發(fā)來的新數(shù)據(jù)
    void update(Role r);
}

class Panel implements Observer{

    @Override
    public void update(Role r) {
        System.out.println("面板血條更改為:"+ r);
    }
}

class BallPanel implements Observer{

    @Override
    public void update(Role r) {
        System.out.println("球形血條更改為:"+ r);
    }
}

class HeadPanel implements Observer{

    @Override
    public void update(Role r) {
        System.out.println("頭部血條更改為:"+ r);
    }
}

class Monster {
    public void attact(Role r){
        r.setHp(r.getHp()-10);
    }
}

public class Test {
    public static void main(String[] args) {
        Role role = new Role();
        role.setName("qiansion");
        role.setHp(100);
        role.setMp(100);
        Panel panel = new Panel();
        BallPanel ballPanel = new BallPanel();
        HeadPanel headPanel = new HeadPanel();
        role.addObserver(panel);
        role.addObserver(ballPanel);
        role.addObserver(headPanel);
        Monster monster = new Monster();
        monster.attact(role);
    }
}

輸出如下:
面板血條更改為:Role{name='qiansion', hp=90, mp=100}
球形血條更改為:Role{name='qiansion', hp=90, mp=100}
頭部血條更改為:Role{name='qiansion', hp=90, mp=100}
目前每當(dāng)主體Role的狀態(tài)發(fā)生變化艾杏,就算主體有很多個屬性韧衣,也不會影響代碼
修改之后也帶來一些問題,作為一個接口购桑,Observer接口中的update方法畅铭,居然出現(xiàn)了具體類名!如此勃蜘,Observer就只能觀察Role這個類了硕噩,觀察不了別的類的對象了,不容易拓展缭贡。

繼續(xù)修改

class Role {
    private String name;
    private Integer hp;
    private Integer mp;
    private List<Observer> observers = new ArrayList<Observer>();

    public void addObserver(Observer obj){
        observers.add(obj);
    }

    public void removeObserver(Object obj){
        observers.remove(obj);
    }

    public void notifyObservers(){
        for (Observer observer : observers) {
            observer.update();
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getHp() {
        return hp;
    }

    public void setHp(Integer hp) {
        this.hp = hp;
        notifyObservers();
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }

    @Override
    public String toString() {
        return "Role{" +
                "name='" + name + '\'' +
                ", hp=" + hp +
                ", mp=" + mp +
                '}';
    }
}

interface Observer{
    // 需要一個方法炉擅,來接受主體發(fā)來的新數(shù)據(jù)
    void update();
}

class Panel implements Observer{
    private Role r;
    public Panel(Role r){
        this.r = r;
    }
    @Override
    public void update() {
        System.out.println("面板血條更改為:"+ r);
    }
}

class BallPanel implements Observer{
    private Role r;
    public BallPanel(Role r){
        this.r = r;
    }
    @Override
    public void update() {
        System.out.println("球形血條更改為:"+ r);
    }
}

class HeadPanel implements Observer{
    private Role r;
    public HeadPanel(Role r){
        this.r = r;
    }
    @Override
    public void update() {
        System.out.println("頭部血條更改為:"+ r);
    }
}

class Monster {
    public void attact(Role r){
        r.setHp(r.getHp()-10);
    }
}

public class Test {
    public static void main(String[] args) {
        Role role = new Role();
        role.setName("qiansion");
        role.setHp(100);
        role.setMp(100);
        Panel panel = new Panel(role);
        BallPanel ballPanel = new BallPanel(role);
        HeadPanel headPanel = new HeadPanel(role);
        role.addObserver(panel);
        role.addObserver(ballPanel);
        role.addObserver(headPanel);
        Monster monster = new Monster();
        monster.attact(role);
    }
}

輸出如下:
面板血條更改為:Role{name='qiansion', hp=90, mp=100}
球形血條更改為:Role{name='qiansion', hp=90, mp=100}
頭部血條更改為:Role{name='qiansion', hp=90, mp=100}
修改后,主體不再主動推送數(shù)據(jù)匀归,從push的方式修改為 push + pull的方式坑资,主體只是通知觀察者“我變了”,觀察者收到消息穆端,就自己去看主體哪兒變了袱贮,巧妙之處就在于此,因為這種推+拉的模式体啰,主體中有觀察者攒巍,觀察者中有主體。再以上代碼中還可以做一些抽象荒勇,Role類中的addObserver和removeObserver等都可以抽象為接口柒莉,這就是最完整的觀察者模式

interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

class Role implements Subject{
    private String name;
    private Integer hp;
    private Integer mp;
    private List<Observer> observers = new ArrayList<Observer>();

    @Override
    public void addObserver(Observer observer){
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer){
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(){
        for (Observer observer : observers) {
            observer.update();
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getHp() {
        return hp;
    }

    public void setHp(Integer hp) {
        this.hp = hp;
        notifyObservers();
    }

    public Integer getMp() {
        return mp;
    }

    public void setMp(Integer mp) {
        this.mp = mp;
    }

    @Override
    public String toString() {
        return "Role{" +
                "name='" + name + '\'' +
                ", hp=" + hp +
                ", mp=" + mp +
                '}';
    }
}

interface Observer{
    // 需要一個方法,來接受主體發(fā)來的新數(shù)據(jù)
    void update();
}

class Panel implements Observer{
    private Role r;
    public Panel(Role r){
        this.r = r;
        r.addObserver(this);
    }
    @Override
    public void update() {
        System.out.println("面板血條更改為:"+ r);
    }
}

class BallPanel implements Observer{
    private Role r;
    public BallPanel(Role r){
        this.r = r;
        r.addObserver(this);
    }
    @Override
    public void update() {
        System.out.println("球形血條更改為:"+ r);
    }
}

class HeadPanel implements Observer{
    private Role r;
    public HeadPanel(Role r){
        this.r = r;
        r.addObserver(this);
    }
    @Override
    public void update() {
        System.out.println("頭部血條更改為:"+ r);
    }
}

class Monster {
    public void attact(Role r){
        r.setHp(r.getHp()-10);
    }
}

public class Test {
    public static void main(String[] args) {
        Role role = new Role();
        role.setName("qiansion");
        role.setHp(100);
        role.setMp(100);
        Panel panel = new Panel(role);
        BallPanel ballPanel = new BallPanel(role);
        HeadPanel headPanel = new HeadPanel(role);
        Monster monster = new Monster();
        monster.attact(role);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沽翔,一起剝皮案震驚了整個濱河市兢孝,隨后出現(xiàn)的幾起案子窿凤,更是在濱河造成了極大的恐慌,老刑警劉巖跨蟹,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雳殊,死亡現(xiàn)場離奇詭異,居然都是意外死亡窗轩,警方通過查閱死者的電腦和手機夯秃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痢艺,“玉大人仓洼,你說我怎么就攤上這事〉淌妫” “怎么了色建?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長植酥。 經(jīng)常有香客問我镀岛,道長弦牡,這世上最難降的妖魔是什么友驮? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮驾锰,結(jié)果婚禮上卸留,老公的妹妹穿的比我還像新娘。我一直安慰自己椭豫,他們只是感情好耻瑟,可當(dāng)我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赏酥,像睡著了一般喳整。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上裸扶,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天框都,我揣著相機與錄音,去河邊找鬼呵晨。 笑死魏保,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摸屠。 我是一名探鬼主播谓罗,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼季二!你這毒婦竟也來了檩咱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刻蚯,沒想到半個月后蜂筹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡芦倒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年艺挪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兵扬。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡麻裳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出器钟,到底是詐尸還是另有隱情津坑,我是刑警寧澤,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布傲霸,位于F島的核電站疆瑰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏昙啄。R本人自食惡果不足惜穆役,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梳凛。 院中可真熱鬧耿币,春花似錦、人聲如沸韧拒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叛溢。三九已至塑悼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間楷掉,已是汗流浹背厢蒜。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留靖诗,地道東北人郭怪。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像刊橘,于是被迫代替她去往敵國和親鄙才。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容