《設計模式》——開閉原則

先扯兩句

  人的惰性啊,總是無限的,一不小心偷懶一次,就會是好長時間的懶惰,也不知道從哪裏來的當頭棒喝叫醒了我,才發現竟然又是這麼長時間沒有進步了。不過想來能來看這篇文章的你肯定是不會懶惰的,那就讓我們一同堅持下去吧。加油!!!
  炫耀一下已有成功激勵一下自己《設計模式》——目錄,然後讓我們進入正題。

定義

什麼是開閉原則

  一不小心就到了《設計模式之禪》中六大設計原則的最後一個設計原則——“開閉原則”。其實就開閉原則而言,我們看名字還是很容易理解的,就是講述了在架構的時候,哪些需要開放、哪些需要關閉的問題,而至於是對什麼開放,對什麼關閉呢?優先看一下《設計模式之禪》的描述吧。

Software entities like classes, modules and functions should be open for extension but closed for modifications.(一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉

  可以看到這句描述中,老頭子我着重標註了兩句話:

  • 對擴展開放
  • 對修改關閉

  對於這部分怎麼解釋,我不知道想掉了多少根頭(這可都是程序猿的命根子啊),終於想到了一個例子,終於還是在動物界找到了些靈感。
  先說說修改,大家都知道鯨魚是世界上最大的哺乳動物,也不知道多少萬年前,陸地上活不下去了,鯨魚的祖先就回到的水裏。把四肢修改成了魚鰭,先不說鯨魚在水中生活的開心不開心,雖然達成了生存的目的,但是鯨魚卻失去了原本在陸地上生存的能力。
鯨魚的嘆息

  而拓展呢,暫時沒有找到實際的例子,但是想必大家都聽說過一次成語:“如虎添翼”,這不就是在的基礎上拓展出了的功能,使得虎不僅是百獸之王,竟然還會飛,你誰它氣不氣人。

如虎添翼

  很顯然,拓展不僅能夠支持新的功能,還能夠保留原有功能的特性。而反觀修改,每有一個新功能,就去改一部分代碼,如此修改下去,遲早會把我們的代碼改成特修斯之船,到時候回去看我們早已退化成魚鰭的四肢,只能徒勞迷茫:

我是誰

  說到這裏,我就當你聽懂了爲什麼對擴展開發,對修改關閉,下面我們來看看官方些的解釋吧。

大牛說

  開閉原則的定義已經非常明確地告訴我們:軟件實體應該對擴展開放,對修改關閉,其含義是說一個軟件實體應該通過擴展來實現變化,而不是通過修改已有的代碼來實現變化。
那什麼又是軟件實體呢?軟件實體包括以下幾個部分:

  • 項目或軟件產品中按照一定的邏輯規則劃分的模塊。
  • 抽象和類。
  • 方法。

  如果是小學作文的話,這裏一定要這麼說一:美國作家斯賓塞·約翰遜在《誰動了我的奶酪》中說過有這麼一句話:

世界上唯一不變的是變化本身

  (怎麼樣,逼格是不瞬間高了好多?)

  這句話用在我們程序猿身上,簡直不要太貼切,對於大公司來說,程序猿恨不得掐死的是產品經理。在小公司就更慘了,因爲你要掐死的將是給你開工資的領。所以,要麼忍、要麼滾。。。

  說實在的,很難滾到一個不改需求的地方,唯一的區別不過就是改的頻率高低、以及給你拿來改的時間長短罷了。既然怎麼都避不開,所以就只能靠增加我們的代碼的靈活性來解決這個問題了。

  我們來寫個直男的成長史吧,首先是一個人都有個能力,那就是安慰別人,當然安慰之後是否起到積極的作用因人而異,但是大家都具備這個能力,而我們傳統直男在女生肚子疼時安慰的話,想必大家都知道:

/**
 * 人
 */
interface IPerson {
    /**
     * 安慰人
     *
     * @param name 被安慰人的姓名
     */
    void comfort(String name);
}

/**
 * 直男
 */
class StraightMan implements IPerson {

    @Override
    public void comfort(String name) {
        System.out.println(name + "多喝熱水");
    }
}

@Test
public void talkWithGirl() {
    IPerson zhangSan = new StraightMan();
    zhangSan.comfort("everyOne");
}

多喝熱水

  很顯然,這樣的直男是找不到女朋友的,所以有朋友告訴他,對喜歡的女孩(Monica)換個說法。

  這裏有幾種實現的方案:

  1. 修改Iperson,添加comfortMonica()方法
  不過這就意味着所有所有人安慰Monica的時候都改變了,先不說其他人願不願意,我們的直男就不願意啊,大家安慰Monica的方式都與自己一樣,自己還怎麼追女孩。所以這個方法肯定不行。

  2. 修改StraightMan,修改comfort()方法
  這個方法可以實現直男見到Monica的時候安慰的方式與其他人都不同,同時也能實現說的不是“多喝熱水”這種作死的回答。但是一旦修改了這裏,就相當於直男需要穿越回到過去,把每一句與Monica的安慰都進行替換,直男也很想,可惜實力不允許啊,也只能無奈放棄。

  3. 拓展一個StraightManForGirlFriend的類
  拓展一個想要找女朋友的直男,繼承直男的所有特點,不過安慰人的時候添加了一個注意事項,那就是遇到“Monica”的時候,換一種說法。這樣直男想找女朋友的時候,就換安慰方式,而且與其他人也都不一樣。不想找的時候,還是可以保持原本的樣子,也不累,直男很開心的接受了。

/**
 * 人
 */
interface IPerson {
    /**
     * 安慰人
     *
     * @param name 被安慰人的姓名
     */
    void comfort(String name);
}

/**
 * 直男
 */
class StraightMan implements IPerson {

    @Override
    public void comfort(String name) {
        System.out.println(name + "多喝熱水");
    }
}

/**
 * 想找女朋友的直男
 */
class StraightManForGirlFriend extends StraightMan {

    @Override
    public void comfort(String name) {
        if ("Monica".equals(name)) {
            System.out.println(name + "你哪有肚子啊");
        } else {
            System.out.println(name + "多喝熱水");
        }
    }
}

@Test
public void talkWithGirl() {
    IPerson zhangSanForGirlFriend = new StraightManForGirlFriend();
    zhangSanForGirlFriend.comfort("Monica");
    zhangSanForGirlFriend.comfort("otherOne");
}

想找女朋友的直男

  可以看到,對於其他人來說,還是“多喝熱水”,但是對於直男心儀的Monica,卻換了一種說法“你哪有肚子啊”。

  可是直男也就是一時衝動,見改變一句安慰的話並沒有追到Monica,直男直接放棄找女朋友了,變回了自己原本的樣子。

/**
 * 人
 */
interface IPerson {
    /**
     * 安慰人
     *
     * @param name 被安慰人的姓名
     */
    void comfort(String name);
}

/**
 * 直男
 */
class StraightMan implements IPerson {

    @Override
    public void comfort(String name) {
        System.out.println(name + "多喝熱水");
    }
}

@Test
public void talkWithGirl() {
    IPerson zhangSan = new StraightMan();
    zhangSan.comfort("Monica");
}

棄療的直男

  刪掉想要找女朋友的直男後,當Monica來找直男說自己肚子疼的時候,得到的安慰,也變成了“多喝熱水”。

  這裏有一個注意事項,那就是:

  注意:開閉原則對擴展開放,對修改關閉,並不意味着不做任何修改,低層模塊的變更,必然要有高層模塊進行耦合,否則就是一個孤立無意義的代碼片段。

  其實說起來也很簡單,雖然我們添加了一個StraightManForGirlFriend,或者去掉了他,但是在talkWithGirl()測試方法中都需要添加或者刪除StraightManForGirlFriend才能實現具體的功能。如果直男只是自己拓展了功能,但卻不去表達,那對方怎麼能知道直男的心意。而既然有了表達,就意味着兩人交流結果的改變。所以受到被拓展功能實現時的代碼還是要修改的,只是修改的比較少。而且由於是調整了子類,也能夠與父類進行很好的區分,一旦真的不需要的時候,只需要幹掉子類,將所有實現調整會父類即可。

爲什麼使用開閉原則

1. 開閉原則對測試的影響

  開發過程中,很少有功能模塊是能夠做到完全獨立的,而這些都是經過詳細測試的,或者已經在線上跑了很久,經受住了實際考驗的。而再看我們要調整的內容呢,或多或少會對原功能造成影響,而這些影響都是要經過完整測試,才能夠發佈上線的,而且即便測試了,也很難模擬出來所有的實際環境。很難確定發佈後不會造成其他的隱患。

  而且不是所有公司的調整都是經過嚴謹的評估分析的,我們很難保證領導一拍大腿想到的調整方案能夠活過比較長的時間,所以我們現在調整時所有經歷的測試流程,難保在領導反悔的還得再經歷一遍,耗費的時間是完全沒有必要的。(參見例子中的直男)

2. 開閉原則可以提高複用性

  其實這個部分就比較好理解了,畢竟直男的好奇心是很重的,不是單純的想要嘗試找女朋友,說不好哪天又想找男朋友了呢?說不好哪天又想養寵物了呢(想多的自覺面壁去)?如果針對每一個都在直接改變直男的興趣愛好,會造成很大的影響。尤其當直男左手拉着女朋友,右手拉着男朋友去追狗的時候,絕對是個災難。而當將直男的每一面都使用一個獨立的類去刻畫的時候,就可以大大增加複用的可能性。直男想要左手拉着女朋友,右手拉着男朋友去追貓的時候,修改起來也能比較輕鬆。

3. 開閉原則可以提高可維護性

  作爲程序猿來說,想必我們之間應該有一個共識,那就是其他人寫的代碼都是“shit!”,前段時間公司有個APP頁面要重構,同事說想要重新寫,當時我那小暴脾氣,我寫的多好啊,怎麼就得重寫啊!可一想自己有多少次想要重寫他的代碼,瞬間就平衡了。所以在這個時候,堅持修改已有代碼,對於改代碼的和被改代碼的來說都是一種折磨(代碼被改以後,可能會從一個人看不懂,演變成雙方都看不懂),所以還是直接寫自己的拓展類吧,大家都維護自己的內容,輕鬆加愉快。腹黑點說,到時候發現看不懂總不能說別人寫的不好不是。

如何使用開閉原則

  其實作爲一個Android開發的小菜鳥,能夠使用到的設計模式,其實並沒有後臺的多,因此對於設計模式的理解實際也是比較有限的,不然也不會這麼長時間了,博客才寫到開閉原則。因此在看開閉原則的時候,還是有些吃力的,尤其是“如何使用”這部分,其中舉了一個login的例子,說是簡單的例子,我反覆看了5遍,也買看懂是怎麼實現的元數據模塊行爲。好吧,其實元數據我是百度好久纔看懂的(見PS1)。所以這部分先暫時按照我個人的理解去寫了,後面大神們還是建議直接去看書,如果跟我水平差不多的,大家也可以看一看,不過要多找其他資料印證一下,後續對這部分有深入瞭解後,會回來重新調整(當時會認爲自己現在歸納的特別精闢也說不定呢)

1. 需求優先

  框架搭建之初詳細瞭解具體的需求都有哪些,並依據需求,詳細列舉所有的功能模塊、數據類等,以及相互之間的交互關係,依據這些交互關係規劃接口、抽象方法、實現類、數據類等內容,且制定後基類最好就不要再調整了。

2. 字段規範

  公司前段時間接口返回字段,“公司地址”,在A接口中是“projectAddress”,B接口中是“address”,在寫接收的數據類時就需要添加兩個接收的字段,且爲了使顯示部分的代碼不至於混亂,因此在get方法中添加了邏判斷。這樣顯然是不符合設計規範的,可是卻由於字段規範問題導致不得不編寫這些冗餘代碼。

3. 封裝變化:

  其實這個名詞我還是比較陌生的,查了N多文檔,也沒敢說完全瞭解,至少書中的“相同的變化”與“不同的變化”我就沒理解是什麼意思。所以這裏就採用最淺顯的理解說明了:

  對變化我這裏的個人理解是有兩種,第一種是可預測的變化,第二種則是不可預測的變化,而上面的安慰的話語打印就是可預測的,我們知道對方究竟會傳入什麼信息,以及接受到信息後,我們需要作出什麼樣的反饋。而不可預測的變化,就是我們可能根本不知道對方會傳入什麼東西,而且接收到了以後,也完全搞不懂對方會用來做什麼。因此就要在封裝的時候作出靈活的應對:

class StraightMan {
    /**
     * 可預測變化
     */
    public void comfort(String name) {
        System.out.println(name + "多喝熱水");
    }
}

  上面的方法其實就是對於可預測變化的封裝,因爲我們並不知道需要打印的name具體是什麼,所以就使用封裝方法的形式,無論傳入的是誰的名字,我們打印的結果都是讓其多喝熱水。

class StraightMan<T> {
    private OnStraightManComfort<T> onStraightManComfort;

    public StraightMan(OnStraightManComfort<T> onStraightManComfort) {
        this.onStraightManComfort = onStraightManComfort;
    }

    /**
     * 不可預測變化
     */
    public void comfort(T t) {
        if (null != onStraightManComfort) {
            onStraightManComfort.comfort(t);
        }
    }

    public interface OnStraightManComfort<T> {
        void comfort(T t);
    }
}

  而當我們的變化並不可預測的時候,則可以通過回調的方式,將信息回傳,並依據當時的實際情況作出對應的設計。

  如此在明確的時候,可以實現簡單明瞭的封裝,又可以在不可預測的時候增加框架的可擴展性,也同時避免不可預測時猜測可能傳入的數據類型,以及回傳的內容,而一一列舉所導致的過度設計。

PS1:元數據:鳴謝什麼是元數據?爲何需要元數據?
元數據

  元數據(meta data)——“data about data” 關於數據的數據,一般是結構化數據(如存儲在數據庫裏的數據,規定了字段的長度、類型等)。

  一個基本的元數據由元數據項目和元數據內容的構成。這裏,“題名”就是它的元數據項目,“史蒂夫·喬布斯傳 (美) 沃爾特·艾薩克森著 = Steve Jobs Walter Isaacson eng”就是元數據內容。

PS2:鳴謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章