Java面向?qū)ο笤O(shè)計模式總結(jié)之二設(shè)計原則

一. 軟件設(shè)計模式

1. 什么是軟件設(shè)計模式?

軟件設(shè)計模式(Software Design Pattern)冤竹,又稱設(shè)計模式财饥,是指在軟件開發(fā)中落竹,經(jīng)過驗證的,用于解決在特定環(huán)境下剪况、重復(fù)出現(xiàn)的教沾、特定問題的解決方案。

2. 軟件設(shè)計模式的作用是什么?

設(shè)計模式的本質(zhì)是面向?qū)ο笤O(shè)計原則的實際運用译断,是對類的封裝性授翻、繼承性和多態(tài)性以及類的關(guān)聯(lián)關(guān)系和組合關(guān)系的充分理解。正確使用設(shè)計模式具有以下優(yōu)點孙咪。

  • 可以提高程序員的思維能力堪唐、編程能力和設(shè)計能力。
  • 使程序設(shè)計更加標(biāo)準(zhǔn)化翎蹈、代碼編制更加工程化羔杨,使軟件開發(fā)效率大大提高,從而縮短軟件的開發(fā)周期杨蛋。
  • 使設(shè)計的代碼可重用性高、可讀性強理澎、可靠性高逞力、靈活性好、可維護(hù)性強糠爬。

二. 面向?qū)ο笤O(shè)計的七大原則

  • 開閉原則(Open Closed Principle寇荧,OCP)
  • 單一職責(zé)原則(Single Responsibility Principle, SRP)
  • 里氏代換原則(Liskov Substitution Principle,LSP)
  • 依賴倒轉(zhuǎn)原則(Dependency Inversion Principle执隧,DIP)
  • 接口隔離原則(Interface Segregation Principle揩抡,ISP)
  • 合成/聚合復(fù)用原則(Composite/Aggregate Reuse Principle户侥,CARP)
  • 迪米特法則(Law of Demeter,LOD) 或者最少知識原則(Least Knowledge Principle峦嗤,LKP)

其中蕊唐,單一職責(zé)原則、開閉原則烁设、迪米特法則替梨、里氏代換原則和接口隔離原則的英文首字母拼在一起就是SOLID(穩(wěn)定的),所以也稱之為SOLID原則装黑。

1. 單一職責(zé)原則(Single Responsibility Principle)

對類來說的副瀑,即一個類應(yīng)該只負(fù)責(zé)一項職責(zé)。如類A負(fù)責(zé)兩個不同職責(zé):職責(zé)1恋谭,職責(zé)2糠睡。當(dāng)職責(zé)1需求變更而改變A時,可能造成職責(zé)2執(zhí)行錯誤疚颊,所以需要將類A的粒度分解為A1和A2狈孔。
類的職責(zé)要單一,不能將太多的職責(zé)放在一個類中串稀。

例如:大學(xué)學(xué)生工作管理程序除抛。
分析:大學(xué)學(xué)生工作主要包括學(xué)生生活輔導(dǎo)和學(xué)生學(xué)業(yè)指導(dǎo)兩個方面的工作,其中生活輔導(dǎo)主要包括班委建設(shè)母截、出勤統(tǒng)計到忽、心理輔導(dǎo)、費用催繳清寇、班級管理等工作喘漏,
學(xué)業(yè)指導(dǎo)主要包括專業(yè)引導(dǎo)、學(xué)習(xí)輔導(dǎo)华烟、科研指導(dǎo)翩迈、學(xué)習(xí)總結(jié)等工作。如果將這些工作交給一位老師負(fù)責(zé)顯然不合理盔夜,正確的做 法是生活輔導(dǎo)由輔導(dǎo)員負(fù)責(zé)负饲,學(xué)業(yè)指導(dǎo)由學(xué)業(yè)導(dǎo)師負(fù)責(zé),其類圖如圖 1 所示喂链。

單一職責(zé)原則注意事項和細(xì)節(jié):

  • 降低類的復(fù)雜度返十,一個類只負(fù)責(zé)一項職責(zé)。
  • 提高類的可讀性椭微,可維護(hù)性洞坑。
  • 降低變更引起的風(fēng)險。
  • 通常情況下蝇率,我們應(yīng)當(dāng)遵守單一職責(zé)原則迟杂,只有邏輯足夠簡單刽沾,才可以在代碼級違反單一職責(zé)原則:只有類種方法數(shù)量足夠少,可以在方法級別保持單一職責(zé)原則排拷。

注意:單一職責(zé)同樣也適用于方法侧漓。一個方法應(yīng)該盡可能做好一件事情。如果一個方法處理的事情太多攻泼,其顆粒度會變得很粗火架,不利于重用。

2.開閉原則(Open-Closed Principle)

對擴(kuò)展開放忙菠,對修改關(guān)閉何鸡。
一般情況,我們接到需求變更的通知牛欢,通常方式可能就是修改模塊的源代碼骡男,然而修改已經(jīng)存在的源代碼是存在很大風(fēng)險的,尤其是項目上線運行一段時間后傍睹,開發(fā)人員發(fā)生變化隔盛,這種風(fēng)險可能就更大。
所以拾稳,為了避免這種風(fēng)險吮炕,在面對需求變更時,我們一般不修改源代碼访得,即所謂的對修改關(guān)閉龙亲。不允許修改源代碼,我們?nèi)绾螒?yīng)對需求變更呢悍抑?答案就是我們下面要說的對擴(kuò)展開放鳄炉。

通過擴(kuò)展去應(yīng)對需求變化,就要求我們必須要面向接口編程搜骡,或者說面向抽象編程拂盯。所有參數(shù)類型、引用傳遞的對象必須使用抽象(接口或者抽象類)的方式定義记靡,不能使用實現(xiàn)類的方式定義谈竿;
通過抽象去界定擴(kuò)展,比如我們定義了一個接口A的參數(shù)摸吠,那么我們的擴(kuò)展只能是接口A的實現(xiàn)類空凸。這樣原則設(shè)計出來的系統(tǒng),遇到增加功能的需求時蜕便,幾乎不用修改源代碼,只是增加幾個類贩幻,然后調(diào)用就好轿腺。
這樣既增加了新功能滿足了需求两嘴,又維護(hù)了原本系統(tǒng)的穩(wěn)定性。

例如:

  1. 首先創(chuàng)建一個手機接口:
public interface Phone {
    String getName();//名稱

    Double getPrice();//價格
}
  1. 創(chuàng)建一個IPhone手機實現(xiàn)手機接口:
public class IPhone implements Phone {
    private String name;
    private Double price;

    public IPhone(String name, Double price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Double getPrice() {
        return price;
    }
}
  1. 使用類
public class PhoneSore {
    public static void main(String[] args) {
        Phone phone = new IPhone("Iphone 4S", 6000.00);
        System.out.println("歡迎購買:名字:" + phone.getName() + " 價格:" + String.valueOf(phone.getPrice()));
    }
}

上面的代碼可以正常地運行族壳,我們可以方便地添加新的手機憔辫。但是如果需求發(fā)生了變更,手機店推出了打折地活動仿荆。我們?nèi)绾谓鉀Q贰您?
有下面三種方法可以解決此問題:

  • 修改接口
    在IPhone接口中,增加一個方法getDiscountPrice拢操,專門用于處理打折需求锦亦。但是這個方法是有問題的,接口應(yīng)該是穩(wěn)定且可靠的令境,不應(yīng)該經(jīng)常發(fā)生變化杠园,否則接口作為契約的作用就失去了。且違背了開閉原則舔庶,因此否定抛蚁。

  • 修改實現(xiàn)類
    第二種方法是通過修改實現(xiàn)類中的getPrice方法或者增加getDiscountPrice方法實現(xiàn)其需求,但是這樣一個類中就存在了兩個讀取價格的方法惕橙,且違背了開閉原則瞧甩,所以此方法也不是一個最優(yōu)方案。

  • 通過擴(kuò)展實現(xiàn)變化
    我們可以通過增加一個子類IPhoneDiscount弥鹦,復(fù)寫getPrice方法肚逸,此方法修改少,對現(xiàn)有的代碼沒有影響惶凝,風(fēng)險少吼虎,是個好方法。

  1. 添加打折類
public class IPhoneDiscount extends IPhone {
    public IPhoneDiscount(String name, Double price) {
        super(name, price);
    }

    //打折活動
    public Double getPrice() {
        //九折優(yōu)惠
        return super.getPrice() * 0.90;
    }
}

3.里式替換原則(Liskov Substitution Principle)

所有引用基類(父類)的地方苍鲜,都必須能透明地使用其子類的對象思灰。父類可被子類替換,但反之不一定成立混滔。也就是說洒疚,代碼中可以將父類全部替換為子類,程序不會出現(xiàn)異常坯屿。
當(dāng)使用繼承時油湖,遵循里氏替換原則。類B繼承類A時领跛,除添加新的方法完成新增功能P2外乏德,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。
里氏替換原則通俗的來講就是:子類可以擴(kuò)展父類的功能喊括,但不能改變父類原有的功能胧瓜。
例如:我喜歡動物,那我一定喜歡狗郑什,因為狗是動物的子類府喳。但是我喜歡狗,不能據(jù)此斷定我喜歡動物蘑拯,因為我并不喜歡老鼠钝满,雖然它也是動物脉让。

盡量不要重寫父類方法扒吁,而是增加自己特有的方法缓呛。
繼承給程序設(shè)計帶來巨大便利的同時傻丝,也帶來了弊端借浊。如果一個類被其他的類所繼承版保,則當(dāng)這個類需要修改時拐揭,必須考慮到所有的子類娶吞,并且父類修改后玄窝,所有涉及到子類的功能都有可能會產(chǎn)生BUG牵寺。
例如:

  1. 先定義一個鳥的接口。
public class Bird {
    private int velocity;

    public int getVelocity() {
        return velocity;
    }

    public void setVelocity(int velocity) {
        this.velocity = velocity;
    }
}
  1. 定義鴕鳥去實現(xiàn)鳥的功能恩脂。
public class Ostrich extends Bird{

    public int getVelocity() {
        //鴕鳥是不會飛的所以他的飛行時間就為0
        return 0;
    }
}
  1. 測試
public class main {

    public static void main(String[] args) {
        //計算鳥的飛行時間    
        Bird bird = new Bird();
        bird.setVelocity(100);
        int h = flyTime(bird);
        System.out.println("飛行時間是:"+h);

        //計算鴕鳥的飛行時間
        Bird ostrich = new  Ostrich();
        ostrich.setVelocity(100);
        int h = flyTime(ostrich);
        System.out.println("飛行時間是:"+h);
    }
/*
*計算飛行3000米需要的時間
*/
    public static int flyTime(Bird bird)
    {
        return 3000/bird.getVelocity();
    }
}

結(jié)果:
普通鳥運行結(jié)果正確帽氓,飛行時間是:30。
計算鴕鳥的飛行時間報錯俩块。

面向?qū)ο蟮恼Z言的三大特點是繼承黎休,封裝,多態(tài)玉凯,里氏替換原則是依賴于繼承势腮,多態(tài)這兩大特性。里氏替換原則的定義是漫仆,所有引用基類的地方必須能透明地使用其子類的對象捎拯。
通俗來講是只要父類能出現(xiàn)的地方子類就可以出現(xiàn),而且替換為子類也不會產(chǎn)生任何錯誤和異常盲厌。而我們在使用flyTime方法時 署照,當(dāng)使用者flyTime方法里的參數(shù)Bird被Ostrich替換掉后,
結(jié)果出現(xiàn)了異常吗浩,那么它明顯違背了里氏替換原則建芙。

4.接口隔離原則(Interface Segregation Principle)

使用多個專門的接口,而不使用單一的總接口懂扼。不要對外暴露沒有實際意義的接口禁荸。也就是說使用多個專門的接口比使用單一的總接口要好。
例如:對于鳥的實現(xiàn)(Bird),我們可以定義兩個功能接口赶熟,分別是Fly和Eat品嚣,我們可以讓Bird分別實現(xiàn)這兩個接口。
如果我們還有一個Dog钧大,那么對于Eat接口,可以復(fù)用罩旋。但是如果只有一個接口(包含F(xiàn)ly和Eat兩個功能)啊央,對于Dog來說,
它是不會飛(Fly)的涨醋,那么就需要針對Dog再聲明一個新的接口瓜饥,這是沒有必要的設(shè)計。

5.依賴倒置原則(Dependence Inversion Principle)

高層模塊不應(yīng)該依賴低層模塊浴骂,二者都應(yīng)該依賴其抽象 乓土。
抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象 溯警。
一開始類A依賴于類B趣苏,由于需求發(fā)生了改變。要將類A依賴于類C梯轻,則我們需要修改類A依賴于類B的相關(guān)代碼食磕,這樣會對程序產(chǎn)生不好的影響。假如需求又發(fā)生了改變喳挑,我們又需要修改類A的代碼彬伦。

例如:

public class UserService {
    private Plaintext plaintext; // 明文登錄注冊
    
    public void register(){
        Plaintext.register();    // 調(diào)用明文的注冊方法
    }
    public void login(){
        Plaintext.login();        // 調(diào)用明文的登錄方法
    }
}

上面的例子可以看出,UserService類依賴于Plaintext類伊诵。有一天单绑,由于使用明文登錄注冊不安全,需求改為使用密文登錄注冊曹宴。我們可以怎么辦搂橙?

//不符合 依賴倒置原則
public class UserService {
    // private Plaintext plaintext;
    private Ciphertext ciphertext;    // 密文登錄注冊
    
    public void register(){
        // Plaintext.register();
        Ciphertext.register();        // 調(diào)用密文的注冊方法
    }
    public void login(){
        // Plaintext.login();
        Ciphertext.login();            // 調(diào)用密文的登錄方法
    }
}

在上面的例子,修改一個需求幾乎將整個UserService類都修改了一遍浙炼,這不但麻煩份氧,而且會給程序帶來很多風(fēng)險。所以上面的例子不符合依賴倒置原則弯屈。

//符合 依賴倒置原則
public class UserService {
    private Authentication authentication;    // 依賴于接口(抽象)
    
    public UserServer(Authentication auth) {
        //接口與實現(xiàn)類對接
        this.authentication = auth;
    }
    
    public void register(){
        authentication.register();
    }
    public void login(){
        authentication.login();
    }
}


public interface Authentication {
    //...登錄注冊
}
public class Ciphertext implements Authentication {
    //...使用明文的實現(xiàn)
}
public class Plaintext implements Authentication {
    //...使用密文的實現(xiàn)
}

在上面的例子Ciphertext類和Plaintext類實現(xiàn)了Authentication接口蜗帜。而UserService類依賴于Authentication接口。這樣可以在構(gòu)造函數(shù)里隨意切換登錄注冊的模式资厉。
假設(shè)以后還需要更改需求厅缺,只需要實現(xiàn)Authentication接口然后在構(gòu)造函數(shù)里注入就可以了。

6.迪米特法則(Law Of Demeter)

如果兩個類不彼此通信,那么這兩個類就不應(yīng)當(dāng)直接地發(fā)生相互作用湘捎。如果其中一個類需要另一個類的某一個方法的話诀豁,可以通過第三者轉(zhuǎn)發(fā)這個調(diào)用。
迪米特法則的初衷是降低類之間的耦合窥妇,由于每個類都減少了不必要的依賴舷胜,因此的確可以降低耦合關(guān)系。
但是凡事都有度活翩,雖然可以避免與非直接的類通信烹骨,但是要通信,必然會通過一個“中介”來發(fā)生聯(lián)系材泄,過分的使用迪米特原則沮焕,會產(chǎn)生大量這樣的中介和傳遞類,導(dǎo)致系統(tǒng)復(fù)雜度變大拉宗。
所以在采用迪米特法則時要反復(fù)權(quán)衡峦树,既做到結(jié)構(gòu)清晰,又要高內(nèi)聚低耦合旦事。

7.合成復(fù)用原則(Composite/Aggregate Reuse Principle)

合成復(fù)用原則目的就是盡量使用對象組合魁巩,而不是繼承來達(dá)到復(fù)用的目的。

通過繼承來進(jìn)行復(fù)用的主要問題在于繼承復(fù)用會破壞系統(tǒng)的封裝性姐浮,因為繼承會將基類的實現(xiàn)細(xì)節(jié)暴露給子類歪赢,由于基類的內(nèi)部細(xì)節(jié)通常對子類來說是可見的,所以這種復(fù)用又稱“白箱”復(fù)用单料,子類與父類的耦合度高埋凯。
父類的實現(xiàn)的任何改變都會導(dǎo)致子類的實現(xiàn)發(fā)生變化,這不利于類的擴(kuò)展與維護(hù)扫尖。而且它限制了復(fù)用的靈活性白对。從父類繼承而來的實現(xiàn)是靜態(tài)的,在編譯時已經(jīng)定義换怖,所以在運行時不可能發(fā)生變化甩恼。

由于組合或聚合關(guān)系可以將已有的對象(也可稱為成員對象)納入到新對象中,使之成為新對象的一部分沉颂,因此新對象可以調(diào)用已有對象的功能条摸,這樣做可以使得成員對象的內(nèi)部實現(xiàn)細(xì)節(jié)對于新對象不可見,所以這種復(fù)用又稱為“黑箱”復(fù)用铸屉,相對繼承關(guān)系而言钉蒲,其耦合度相對較低,成員對象的變化對新對象的影響不大彻坛,可以在新對象中根據(jù)實際需要有選擇性地調(diào)用成員對象的操作顷啼;
合成復(fù)用可以在運行時動態(tài)進(jìn)行踏枣,新對象可以動態(tài)地引用與成員對象類型相同的其他對象。

參考資料:
設(shè)計模式概念和七大原則
設(shè)計模式之七大基本原則
萬字總結(jié)之設(shè)計模式七大原則
設(shè)計模式之七大基本原則

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钙蒙,一起剝皮案震驚了整個濱河市茵瀑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌躬厌,老刑警劉巖马昨,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扛施,居然都是意外死亡偏陪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門煮嫌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抱虐,你說我怎么就攤上這事昌阿。” “怎么了恳邀?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵懦冰,是天一觀的道長。 經(jīng)常有香客問我谣沸,道長刷钢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任乳附,我火速辦了婚禮内地,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赋除。我一直安慰自己阱缓,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布举农。 她就那樣靜靜地躺著荆针,像睡著了一般。 火紅的嫁衣襯著肌膚如雪颁糟。 梳的紋絲不亂的頭發(fā)上航背,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音棱貌,去河邊找鬼玖媚。 笑死,一個胖子當(dāng)著我的面吹牛婚脱,可吹牛的內(nèi)容都是我干的最盅。 我是一名探鬼主播突雪,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼涡贱!你這毒婦竟也來了咏删?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤问词,失蹤者是張志新(化名)和其女友劉穎督函,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體激挪,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡辰狡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了垄分。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宛篇。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖薄湿,靈堂內(nèi)的尸體忽然破棺而出叫倍,到底是詐尸還是另有隱情,我是刑警寧澤豺瘤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布吆倦,位于F島的核電站,受9級特大地震影響坐求,放射性物質(zhì)發(fā)生泄漏蚕泽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一桥嗤、第九天 我趴在偏房一處隱蔽的房頂上張望须妻。 院中可真熱鬧,春花似錦泛领、人聲如沸璧南。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽司倚。三九已至,卻和暖如春篓像,著一層夾襖步出監(jiān)牢的瞬間动知,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工员辩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盒粮,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓奠滑,卻偏偏與公主長得像丹皱,于是被迫代替她去往敵國和親妒穴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

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