一. 軟件設(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)定性。
例如:
- 首先創(chuàng)建一個手機接口:
public interface Phone {
String getName();//名稱
Double getPrice();//價格
}
- 創(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;
}
}
- 使用類
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)險少吼虎,是個好方法。
- 添加打折類
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牵寺。
例如:
- 先定義一個鳥的接口。
public class Bird {
private int velocity;
public int getVelocity() {
return velocity;
}
public void setVelocity(int velocity) {
this.velocity = velocity;
}
}
- 定義鴕鳥去實現(xiàn)鳥的功能恩脂。
public class Ostrich extends Bird{
public int getVelocity() {
//鴕鳥是不會飛的所以他的飛行時間就為0
return 0;
}
}
- 測試
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è)計模式之七大基本原則