我們平時(shí)編寫代碼時(shí)蝶桶,很少有人為了刻意迎合軟件設(shè)計(jì)原則而編寫。其實(shí)掉冶,有時(shí)候是你用到了其中的某個(gè)或多個(gè)設(shè)計(jì)原則真竖,而不自知而已儡蔓。也有可能是有的人壓根就不知道設(shè)計(jì)原則是什么。
不過疼邀,沒關(guān)系喂江,為了搞明白既抽象又玄幻的六大準(zhǔn)則,我總結(jié)了一句話來概括每一種設(shè)計(jì)原則所體現(xiàn)的主要思想旁振。
里氏替換原則是指繼承時(shí)不要破壞父類原有的功能获询;依賴倒置原則是指要面向接口編程;開閉原則是指對(duì)擴(kuò)展是開放的拐袜,對(duì)修改是關(guān)閉的吉嚣;職責(zé)單一原則是指實(shí)現(xiàn)類的職責(zé)要單一;接口隔離原則是指設(shè)計(jì)的接口要盡量簡(jiǎn)單蹬铺,專一尝哆;迪米特法則是指要降低類之間的耦合度。
下面一一介紹六大類設(shè)計(jì)原則甜攀,看完之后秋泄,你會(huì)對(duì)上邊的總結(jié)有更深的理解。
一规阀、里氏替換原則
里氏替換原則恒序,乍一看名字,讓人摸不著頭腦谁撼。其實(shí)歧胁,這是一位姓里的女士提出來的,因此用她的姓氏命名厉碟。里氏替換原則喊巍,通俗來講,就是指子類繼承父類時(shí)箍鼓,可以擴(kuò)展父類的功能崭参,但是不要修改父類原有的功能。什么意思呢袄秩,舉個(gè)例子阵翎。
//父類
public class Calculate {
public int cal(int a,int b){
return a + b;
}
}
//子類
public class Calculate2 extends Calculate {
public int cal(int a,int b){
return a - b;
}
}
//測(cè)試
public class TestCal {
public static void main(String[] args) {
Calculate2 cal2 = new Calculate2();
int res = cal2.cal(1, 1);
System.out.println("1+1="+res); // 1+1 = 0
}
}
子類繼承了父類之后逢并,想實(shí)現(xiàn)新功能之剧,卻沒有擴(kuò)展新方法,而是重寫了父類的cal方法砍聊,因此導(dǎo)致結(jié)果 1+1=0. 這就違反了里氏替換原則背稼。
應(yīng)該把子類Calculate2修改為,添加一個(gè)新方法cal2來實(shí)現(xiàn)相減功能
public class Calculate2 extends Calculate {
public int cal2(int a, int b){
return a - b;
}
}
public class TestCal {
public static void main(String[] args) {
Calculate2 cal2 = new Calculate2();
int res = cal2.cal2(1, 1);
System.out.println("1-1="+res); // 1-1=0
}
}
有心的人可能會(huì)發(fā)現(xiàn)玻蝌,里氏替換原則規(guī)定子類不能重寫父類的方法蟹肘。這不是和面向?qū)ο笾械娜筇卣髦弧岸鄳B(tài)”沖突嗎词疼,多態(tài)實(shí)現(xiàn)的一個(gè)重要前提就是子類繼承父類并重寫父類的方法啊。
其實(shí)帘腹,剛開始學(xué)習(xí)里氏替換原則贰盗,我也產(chǎn)生了這樣的疑惑。后來查了很多資料阳欲,才明白舵盈,子類不應(yīng)該去重寫父類已經(jīng)實(shí)現(xiàn)的方法(非抽象方法),而是去實(shí)現(xiàn)父類的抽象方法球化。也就是說秽晚,盡量要基于抽象類和接口的繼承,而不是基于可實(shí)例化的父類繼承筒愚。關(guān)于這一點(diǎn)的解釋赴蝇,可以看這篇文章,我感覺總結(jié)的挺不錯(cuò)的:http://www.reibang.com/p/e6a7bbde8844?utm_campaign
二巢掺、單一職責(zé)原則
簡(jiǎn)單來說句伶,就是要控制類的粒度大小,降低類的復(fù)雜度陆淀,一個(gè)類只負(fù)責(zé)一項(xiàng)職責(zé)熄阻。
例如,在研發(fā)一個(gè)產(chǎn)品新功能時(shí)倔约。需要項(xiàng)目經(jīng)理接需求秃殉,評(píng)估工作量,然后分發(fā)任務(wù)給程序員浸剩。程序員钾军,根據(jù)需求編寫代碼,然后自測(cè)绢要。各司其職吏恭,才能保證項(xiàng)目穩(wěn)定向前推進(jìn)。其類圖如下
另外重罪,單一職責(zé)原則也適用于方法樱哼,一個(gè)方法只做一件事。
三剿配、依賴倒置原則
依賴倒置原則的定義為:高層模塊不應(yīng)該依賴低層模塊搅幅,二者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細(xì)節(jié)呼胚,細(xì)節(jié)應(yīng)該依賴抽象茄唐。其實(shí),就是在說要面向抽象蝇更,面向接口編程沪编。
舉個(gè)栗子呼盆,如果一個(gè)學(xué)生去學(xué)習(xí)歷史知識(shí),只需要把歷史書給他就可以了
public class History {
public String getKnowledge(){
return "歷史知識(shí)";
}
}
public class Student {
public void study(History history){
System.out.println("學(xué)習(xí)" + history.getKnowledge());
}
}
public class Test {
public static void main(String[] args) {
Student stu = new Student();
stu.study(new History()); //學(xué)習(xí)歷史知識(shí)
}
}
但是蚁廓,如果他需要學(xué)習(xí)地理知識(shí)呢访圃,我們需要把History改為Geography,然后修改study方法的參數(shù)類型為Geography
public class Geography {
public String getKnowledge(){
return "地理知識(shí)";
}
}
public class Student {
public void study(Geography geography){
System.out.println("學(xué)習(xí)" + geography.getKnowledge());
}
}
//學(xué)習(xí)地理知識(shí)
雖然相嵌,這樣實(shí)現(xiàn)也是可以的挽荠,但是通用性太差,類之間的耦合度太高了平绩。設(shè)想圈匆,如果該學(xué)生又要學(xué)習(xí)數(shù)學(xué)知識(shí)呢,語(yǔ)文呢捏雌,英語(yǔ)呢跃赚,是不是每次都要修改study方法。這樣的設(shè)計(jì)不符合依賴倒置原則性湿,應(yīng)該把各個(gè)學(xué)科知識(shí)抽象出來纬傲,定義一個(gè)接口IKnowledge,然后每個(gè)學(xué)科去實(shí)現(xiàn)這個(gè)接口肤频,而study方法的參數(shù)傳一個(gè)固定類型IKnowledge就可以了叹括。
public interface IKnowledge {
String getKnowledge();
}
public class History implements IKnowledge{
public String getKnowledge(){
return "歷史知識(shí)";
}
}
public class Geography implements IKnowledge{
public String getKnowledge(){
return "地理知識(shí)";
}
}
public class Student {
public void study(IKnowledge iKnowledge){
System.out.println("學(xué)習(xí)" + iKnowledge.getKnowledge());
}
}
public class Test {
public static void main(String[] args) {
Student stu = new Student();
stu.study(new History()); //學(xué)習(xí)歷史知識(shí)
stu.study(new Geography()); //學(xué)習(xí)地理知識(shí)
}
}
這樣的話,如果需要再學(xué)習(xí)英語(yǔ)知識(shí)宵荒,只需要定義一個(gè)English類汁雷,去實(shí)現(xiàn)IKnowledge接口就可以了。 這就是依賴倒置原則的面向接口編程报咳。
它們之間的類圖關(guān)系如下
四侠讯、接口隔離原則
接口隔離原則的定義:客戶端不應(yīng)該依賴它不需要的接口;一個(gè)類對(duì)另一個(gè)類的依賴應(yīng)該建立在最小的接口上暑刃。
什么意思呢厢漩,就是說設(shè)計(jì)接口的時(shí)候,不要把一大堆需要實(shí)現(xiàn)的抽象方法都定義到同一個(gè)接口中岩臣,應(yīng)該根據(jù)不同的功能溜嗜,來拆分成不同的接口。我們知道架谎,實(shí)現(xiàn)類去實(shí)現(xiàn)接口的時(shí)候炸宵,需要實(shí)現(xiàn)所有的抽象方法。如果接口中有某些不需要的方法狐树,也需要實(shí)現(xiàn)焙压,但是方法體卻是空的,這樣完全沒有意義抑钟。
例如涯曲,我定義一個(gè)Animal的接口,用獅子去實(shí)現(xiàn)接口
public interface Animal {
void eat();
void fly();
void run();
}
public class Lion implements Animal {
@Override
public void eat() {
System.out.println("獅子吃肉");
}
@Override
public void fly() {
}
@Override
public void run() {
System.out.println("獅子奔跑");
}
}
很明顯在塔,獅子是不會(huì)飛的幻件,fly方法的方法體是空的。這樣設(shè)計(jì)蛔溃,不符合接口隔離原則绰沥。因此,我們把接口進(jìn)行拆分贺待,拆分為Animal徽曲,IFly,IRun三個(gè)接口麸塞,讓獅子選擇性實(shí)現(xiàn)秃臣。
public interface Animal {
void eat();
}
public interface IFly {
void fly();
}
public interface IRun {
void run();
}
public class Lion implements Animal,IRun {
@Override
public void eat() {
System.out.println("獅子吃肉");
}
@Override
public void run() {
System.out.println("獅子奔跑");
}
}
// 獅子只需要實(shí)現(xiàn)吃的方法和奔跑的方法就可以了,不需要實(shí)現(xiàn)IFly接口哪工。
可以發(fā)現(xiàn)奥此,接口隔離原則和職責(zé)單一原則非常之相似,但其實(shí)是不同的雁比。職責(zé)單一原則主要是約束類稚虎,針對(duì)的是具體的實(shí)現(xiàn),強(qiáng)調(diào)類職責(zé)的單一偎捎。而接口隔離原則主要是約束接口的蠢终,注重的是高層的抽象和對(duì)接口依賴的隔離。
另外茴她,需要注意蜕径,接口設(shè)計(jì)的過細(xì)也不太好,會(huì)增大系統(tǒng)的復(fù)雜度败京。想象一下兜喻,你為了實(shí)現(xiàn)某些功能,卻需要實(shí)現(xiàn)十幾個(gè)接口的場(chǎng)景是多崩潰吧赡麦。因此需要適度地進(jìn)行接口拆分朴皆。
五、迪米特法則
迪米特法則定義:一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持最少的了解泛粹。什么意思呢遂铡,就是說要盡量降低類之間的耦合度,提高類的獨(dú)立性晶姊,這樣當(dāng)一個(gè)類修改的時(shí)候扒接,對(duì)其他類的影響也會(huì)降到最低。
通俗點(diǎn)講,就是一個(gè)類對(duì)它依賴的類知道的越少越好钾怔。對(duì)于被依賴的類來說碱呼,不管內(nèi)部實(shí)現(xiàn)多復(fù)雜,只需給其他類暴露一個(gè)可以調(diào)用的公共方法宗侦。
舉個(gè)簡(jiǎn)單的例子愚臀。當(dāng)公司老板需要下發(fā)一個(gè)任務(wù)時(shí),不會(huì)直接把每個(gè)員工都叫到一起矾利,給每個(gè)人分配具體的任務(wù)姑裂。而是先召集各部門經(jīng)理給他們發(fā)布任務(wù),然后部門經(jīng)理再給下邊員工分派任務(wù)男旗。老板只需要監(jiān)督部門經(jīng)理即可舶斧,不需要關(guān)心部門經(jīng)理給每個(gè)員工分配的任務(wù)具體是什么。
用代碼可以這樣表示
public class Employee {
public void doTask(){
System.out.println("員工執(zhí)行任務(wù)");
}
}
public class DeptManager {
public void task(){
System.out.println("部門領(lǐng)導(dǎo)發(fā)布任務(wù)");
Employee employee = new Employee();
employee.doTask();
}
}
public class Boss {
private DeptManager deptMgr;
public void setDeptMgr(DeptManager mgr){
this.deptMgr = mgr;
}
public void task(){
System.out.println("老板發(fā)布任務(wù)");
deptMgr.task();
}
}
public class TestD {
public static void main(String[] args) {
Boss boss = new Boss();
boss.setDeptMgr(new DeptManager());
boss.task();
}
}
//老板發(fā)布任務(wù)
//部門領(lǐng)導(dǎo)發(fā)布任務(wù)
//員工執(zhí)行任務(wù)
這樣察皇,老板跟具體的每個(gè)員工就沒有任何直接聯(lián)系茴厉,降低了耦合度。
可以看到让网,其實(shí)部門經(jīng)理在這其中充當(dāng)了中介的作用呀忧,用于建立老板和員工之間的聯(lián)系。需要注意溃睹,要適度的使用中介而账,如果中介太多,就會(huì)導(dǎo)致系統(tǒng)復(fù)雜度太高因篇,通訊的效率降低泞辐。就如同一個(gè)公司,部門越多竞滓,級(jí)別層級(jí)越多咐吼,越不容易管理,溝通成本增加商佑,執(zhí)行任務(wù)的效率下降锯茄。因此,需要合理設(shè)計(jì)中介類茶没。
六肌幽、開閉原則
開閉原則定義:對(duì)擴(kuò)展是開放的,對(duì)修改是關(guān)閉的抓半。
其實(shí)喂急,這句話就體現(xiàn)了封裝,繼承和多態(tài)的思想笛求。一個(gè)實(shí)體類廊移,如果已經(jīng)實(shí)現(xiàn)了原有的功能糕簿,就不應(yīng)該再對(duì)其進(jìn)行修改,需要的話應(yīng)該對(duì)其進(jìn)行功能擴(kuò)展狡孔。這句話聽起來是不是跟里氏替換原則特別像懂诗。其實(shí),開閉原則更像是對(duì)其他幾個(gè)原則的總結(jié)步氏,最終要達(dá)到的目的就是用抽象構(gòu)建高層模塊响禽,用實(shí)現(xiàn)擴(kuò)展具體的細(xì)節(jié)徒爹。
里氏替換原則和依賴倒置原則告訴你應(yīng)該對(duì)類和方法進(jìn)行抽象荚醒。單一職責(zé)和接口隔離告訴你應(yīng)該怎樣做抽象才合理,迪米特法則告訴你具體實(shí)現(xiàn)怎樣做才能做到高內(nèi)聚隆嗅,低耦合界阁。
其實(shí),六大設(shè)計(jì)原則就規(guī)定了一些規(guī)則胖喳,它告訴你按照這樣做更好泡躯,但是如果你非要不遵守規(guī)則,也不是不行丽焊,代碼照樣可以跑较剃,只不過是增加了代碼出問題的概率,健壯性也不好技健,可維護(hù)性不高写穴。這就像我們生活中的很多規(guī)則,如過馬路雌贱,需要看紅綠燈啊送。但是,你非要不看欣孤,硬闖紅燈馋没,也沒人能把你怎樣,不過是增加了你被撞的概率而已降传。所以篷朵,遵守規(guī)則,能最大限度降低我們的損失婆排。