設(shè)計(jì)模式之我見(jiàn)(一)--SOLID設(shè)計(jì)原則

前言


設(shè)計(jì)原則----一個(gè)老生常談卻又常談常新的話題。
喚作原則,即為實(shí)際編碼、模式設(shè)計(jì)時(shí)的基本思想,理解在先,使用在后。流于字面的思想經(jīng)不起推敲,融于實(shí)踐才能為己所用。


開(kāi)閉原則(Open Closed Principle)

都說(shuō)“開(kāi)閉原則是最基礎(chǔ)的一個(gè)原則”,故將其放在首位,以期收提綱挈領(lǐng)之效。

Software entities like classes,modules and functions should be open for extension but closed for modification
一個(gè)軟件實(shí)體如類、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉

按照慣例,一步一問(wèn)
何為擴(kuò)展
這個(gè)好理解,產(chǎn)品需求變幻莫測(cè),隨著功能迭代,上線已久的模塊也常常面臨改造,“擴(kuò)展”無(wú)處不在。
舉個(gè)簡(jiǎn)單的栗子
項(xiàng)目里已定義了頁(yè)面類型A,現(xiàn)在需要新增一種頁(yè)面類型B,都擁有更新圖片和文字的功能。好,那現(xiàn)在應(yīng)該怎么辦?修改已經(jīng)受過(guò)多輪檢驗(yàn)的原有邏輯,兼容AB類型?還是新增一個(gè)B類?留給下問(wèn)
何為開(kāi)放
顧文思義,即為允許,允許擴(kuò)展。奔著擴(kuò)展的思路,由AB類共有邏輯抽象出接口IA

public interface IA {
    public void updateUI();//更新圖片和整體樣式
    public void changeTipNum(int number);//更新頁(yè)面文字
}

再讓類A實(shí)現(xiàn)該接口,覆寫(xiě)公共方法

public class A implements IA {
···
@Override
public void updateUI() {
···
}
@Override
public void changeTipNum(int number) {
···
}

如此一來(lái),無(wú)論新增多少種頁(yè)卡,只需實(shí)現(xiàn)IA接口,覆寫(xiě)公共方法完成特殊業(yè)務(wù),即可滿足需求。原有邏輯無(wú)需改動(dòng),新增需求完美實(shí)現(xiàn)。
何為關(guān)閉修改
如前文所述,“軟件實(shí)體應(yīng)該通過(guò)擴(kuò)展來(lái)實(shí)現(xiàn)變化,而不是通過(guò)修改已有代碼來(lái)實(shí)現(xiàn)變化”。實(shí)現(xiàn)新需求絕不能以推倒舊功能為代價(jià),編碼的高復(fù)用和可拓展便在于此。可能會(huì)認(rèn)為,“只是改了一個(gè)判斷影響不大”,“不過(guò)就加了個(gè)判斷算不上推倒”······
然而首先,只要做了更改,測(cè)試同學(xué)必須全面回歸,此為人力消耗。再者,如此牽一發(fā)而動(dòng)全身,可見(jiàn)項(xiàng)目結(jié)構(gòu)也處在不穩(wěn)定的有毒環(huán)境中,隱患重重。可見(jiàn)開(kāi)閉原則必須遵循。
開(kāi)閉原則的重要性不妨從以下 幾方面理解(引自《設(shè)計(jì)模式之禪》本篇引用皆源于此)

1.開(kāi)閉原則對(duì)測(cè)試的影響
2.開(kāi)閉原則可以提高復(fù)用性
3.開(kāi)閉原則可以提高可維護(hù)性
4.面向?qū)ο箝_(kāi)發(fā)的要求

開(kāi)閉原則正是對(duì)代碼人熟知的六字箴言---高內(nèi)聚低耦合的理論應(yīng)用。


單一職責(zé)原則(Single Responsebility Principle)

There should never be more than one reason for a class to change.
應(yīng)該有且僅有一個(gè)原因引起類的變更

說(shuō)完開(kāi)閉原則,可以談?wù)剢我宦氊?zé)了。先看看上栗,回到問(wèn)題開(kāi)始的第一個(gè)假設(shè)

修改經(jīng)過(guò)了多次檢驗(yàn)的原有邏輯,兼容AB類型?

修改原有邏輯本身的弊病已經(jīng)分析,這里我們看看,若要兼容,即在updateUI或別的方法中做判斷,邏輯結(jié)構(gòu)如下

public void updateUI() {
        if (type== A類) {
            ···
        } else if (type== B類) {
            ···
        }
}

需求確定后,寫(xiě)好提測(cè),好像也沒(méi)什么問(wèn)題。那么B類若要進(jìn)行修改呢,比如增加一個(gè)控件?B類的邏輯處理完,A類也要相應(yīng)的隱藏或處理該控件。若增加更為復(fù)雜的邏輯呢,要知道計(jì)劃永遠(yuǎn)趕不上變化。那······那就扯來(lái)扯去,一團(tuán)亂麻了。
引一段《設(shè)》對(duì)單一職責(zé)原則優(yōu)勢(shì)的總結(jié)

1.類的復(fù)雜性降低,實(shí)現(xiàn)什么職責(zé)都有清晰明確的定義
2.可讀性提高
3.可維護(hù)性提高
4.變更引起的風(fēng)險(xiǎn)降低

重看“單一職責(zé)”,針對(duì)的正是一類或一方法身兼多職的問(wèn)題。
遵循“單一職責(zé)”,不僅代碼更易讀,邏輯更清晰,拓展性也更高,項(xiàng)目結(jié)構(gòu)自然更為健壯。


里氏替換原則(Liskov Substitution Principle,LSP)

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基類的地方必須能透明地使用其子類對(duì)象

簡(jiǎn)單說(shuō)來(lái)就是,子類能夠代替父類,父類無(wú)法替換子類,白馬是馬,馬不一定是白馬。

有此思想后可推導(dǎo)出以下四點(diǎn)

1.子類必須完全實(shí)現(xiàn)父類的方法
2.子類可以有自己的個(gè)性
3.覆蓋或?qū)崿F(xiàn)父類的方法時(shí)輸入?yún)?shù)可以被放大
4.覆蓋或?qū)崿F(xiàn)父類的方法時(shí)輸出結(jié)果可以被縮小

看完定義,習(xí)慣性反問(wèn)
子類沒(méi)有完全實(shí)現(xiàn)父類的方法又如何
假設(shè)有如下定義。父類A與子類B、C,B類中實(shí)現(xiàn)A類的a、b方法,C類僅實(shí)現(xiàn)b方法。

public abstract class A {
    public abstract void a() {}
    public abstract int b() {
            return 0;
    }
}

public class B extends A {
    @Override
    public void a() {
        ···
    }
    @Override
    public int b() {
        ···
    }
}

public class C extends A {
    @Override
    public void a() {
        ···
    }
    @Override
    public int b() {
        return 0;
    }
}

根據(jù)里氏替換原則,(一個(gè)凡是)凡是父類出現(xiàn)的地方子類皆可出現(xiàn),于是設(shè)想如下編程場(chǎng)景

public String getParam(A object) {
        ···(判空 邊界檢查blabla)
        object.a();
        int retInt = object.b();
        ··· (根據(jù)retInt得到param)···
        return param;
}

很顯然,參數(shù)object可以為任意A的子類,而param的值跟b方法的執(zhí)行結(jié)果息息相關(guān)。那么如果傳入的是沒(méi)有覆寫(xiě)和重載b方法的類C,則默認(rèn)返回0。聽(tīng)起來(lái)好像怪怪的?對(duì)!這意味著getParam邏輯中必須對(duì)返回0的情況甚或需要判斷傳入對(duì)象的類型,牽涉的邏輯越多,調(diào)整的地方就更多。
子類發(fā)揮個(gè)性有何限制?
如果子類過(guò)分個(gè)性,不但父類無(wú)法替代子類(里氏替換原則反過(guò)頭理解),別的子類也無(wú)法替代這個(gè)個(gè)性十足的子類。業(yè)務(wù)邏輯一旦變得復(fù)雜,弊端很容易顯現(xiàn)。畢竟程序員需要目光長(zhǎng)遠(yuǎn),著眼大局。

如果子類不能完整地實(shí)現(xiàn)父類的方法,或者父類的某些方法在子類中已經(jīng)發(fā)生“畸變”,則建議斷開(kāi)父子繼承關(guān)系,采用依賴、聚集、組合等關(guān)系代替繼承。

子類和父類的參數(shù)界定依據(jù)什么原則?
第三點(diǎn)和第四點(diǎn)的通俗理解就是,因?yàn)橄蛳罗D(zhuǎn)型有危險(xiǎn),所有要想讓子類替換父類,則替換時(shí)接受方的參數(shù)類型必須寬于待接入方。


迪米特法則(Law of Demeter)

Only talk to your immediate friends
只與直接的朋友通信

迪米特法則的要求總結(jié)為如下四點(diǎn):

1.只和朋友交流
2.朋友之間也是有距離的
3.是自己的就是自己的
4.謹(jǐn)慎使用Serializable

理解迪米特,謹(jǐn)記高內(nèi)聚,低耦合(又是開(kāi)閉,又是開(kāi)閉),以上四點(diǎn)要求都為了這共同目標(biāo)服務(wù)。這里稍作解釋。
其一,避免A愛(ài)BC,B又愛(ài)C之類剪不斷理還亂的情況(此愛(ài)為廣義之愛(ài),切莫誤會(huì)[無(wú)辜臉])。方法實(shí)現(xiàn)不提倡“博愛(ài)”。
其二,避免A為X戴帽,B為X戴領(lǐng)結(jié),C為X穿上衣,D為穿褲...之類繁瑣冗雜的理事步驟,可以想見(jiàn)一旦其中有一個(gè)環(huán)節(jié)出了問(wèn)題解決起來(lái)有多麻煩(比如戴領(lǐng)結(jié)的跟穿上衣的打架了...)
搬運(yùn)幾段簡(jiǎn)約版代碼以作補(bǔ)充
問(wèn)題代碼1:

public class Teacher { 
public void commond(GroupLeader groupLeader){ //老師對(duì)學(xué)生發(fā)布命令,清一下女生 
    List<Girl> listGirls = new ArrayList(); 
    ···//初始化女生 ···
    groupLeader.countGirls(listGirls); //告訴體育委員開(kāi)始執(zhí)行清查任務(wù) 
} 
} 
public class GroupLeader { 
public void countGirls(List<Girl> listGirls){ //有清查女生的工作 
        ···//清查女生···  
} 
} 

嗯,很顯然犯了“其一”所指的問(wèn)題,Teacher 類怎么還依賴了Girl,“初始化女生”必須換個(gè)地方。
修正問(wèn)題代碼1如下:

public class Teacher { 
public void commond(GroupLeader groupLeader){     
    groupLeader.countGirls(listGirls); //告訴體育委員開(kāi)始執(zhí)行清查任務(wù) 
} 
} 
public class GroupLeader { 
public void countGirls(){ 
    List<Girl> listGirls = new ArrayList<Girl>();     
    ···//初始化女生 ···
    ···//清查女生···  
} 
} 

這樣邏輯線條才夠直嘛。
問(wèn)題代碼2:

public class Wizard { 
public int first(){ //第一步  
    ···
} 
public int second(){ //第二步 
    ···
} 
public int third(){ //第三個(gè)方法 
    ···
} 
}
public class InstallSoftware { 
public void installWizard(Wizard wizard){ 
    int first = wizard.first();   
    if(first>50){ //根據(jù)first返回的結(jié)果,看是否需要執(zhí)行second 
        int second = wizard.second(); 
        if(second>50){ 
            int third = wizard.third(); 
                if(third >50){ 
                    wizard.first(); 
                }  
        }  
    } 
} 
}

很明顯,又犯了“其二”描述的問(wèn)題,耦合關(guān)系如此緊密,豈不是牽一動(dòng)百?
修改問(wèn)題2如下:

public class Wizard { 
private Random rand = new Random(System.currentTimeMillis()); 
private int first(){ //第一步 
    ···
} 
private int second(){ //第二步 
    ···
} 
private int third(){ //第三個(gè)方法 
    ···
} 
//軟件安裝過(guò)程   
public void installWizard(){     
    int first = this.first();   
    //根據(jù)first返回的結(jié)果,看是否需要執(zhí)行second 
    if(first>50){ 
        int second = this.second(); 
            if(second>50){ 
                int third = this.third(); 
                    if(third >50){ 
                        this.first(); 
                    }  
            }  
    } 
} 
}
public class InstallSoftware { 
public void installWizard(Wizard wizard){ 
    wizard.installWizard(); //不廢話,直接調(diào)用     
} 
} 

好嘛,出什么事直接找installWizard(),妙,真是妙。

迪米特法則的核心觀念就是類間解耦,弱耦合,只有弱耦合了以后,類的復(fù)用率才可以提高。但是解耦是有限度的,除非是計(jì)算機(jī)的最小符號(hào)二進(jìn)制的 0 和1,那才是完全解耦,我們?cè)趯?shí)際的項(xiàng)目中時(shí),需要適度的考慮這個(gè)法則,別為了套用法則而做項(xiàng)目,法則只是一個(gè)參考,你跳出了這個(gè)法則,也不會(huì)有人判你刑,項(xiàng)目也未必會(huì)失敗,這就需要大家使用的是考慮如何度量法則了。


接口隔離原則(Interface Segregation Priciple)

Client should not be forced to depend upon interfaces that they don't use.
客戶端不應(yīng)該依賴它不需要的接口
The dependency of one class to another one should depend on the smallest possible interface.
類間的依賴關(guān)系應(yīng)該建立在最小的接口上

引一段對(duì)其含義的精辟總結(jié)

1.接口要盡量小
2.接口要高內(nèi)聚
3.定制服務(wù)
4.接口的設(shè)計(jì)是有限度的

在逐條分析之前,再回憶一遍,“開(kāi)閉原則是最基礎(chǔ)的一個(gè)原則”。可以想見(jiàn),這四點(diǎn)含義的根本目的就是讓接口足夠靈活,可維護(hù)性足夠高,以實(shí)現(xiàn)“對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉”。
解釋了半天,不如直接搬運(yùn)兩段代碼,孰優(yōu)孰劣自有分辨

//美女實(shí)現(xiàn)類
public class PettyGirl implements IPettyGirl{
   private String name;
   public PettyGirl(String name){
        this.name = name;
   }
   public void goodLooking(){
       System.out.println(name + "---有好的面孔");
   }
   public void niceFigure(){
       System.out.println(name + "---有好身材");
   }
   public void goodTemperament(){
       System.out.println(name + "---有好氣質(zhì)");
   }
}
//抽象星探類
public abstract class AbstractSearcher{
    protected IPettyGirl pettyGirl;
    public AbstractSearcher(IPettyGirl pettyGirl){
       this.pettyGirl=pettyGirl;
    }    
    public abstract void show();//顯示美女信息
}
//星探具體實(shí)現(xiàn)類
public class Searcher extends AbstractSearcher{
   public Searcher(IPettyGirl pettyGirl){
       super(pettyGirl);
   }  
   public void show(){  //顯示美女信息
      System.out.println("----美女的信息如下:---");      
      super.pettyGirl.goodLooking();//顯示好的面孔      
      super.pettyGirl.niceFigure();//顯示好身材      
      super.pettyGirl.goodTemperament();//顯示好氣質(zhì)
   }
}
//實(shí)現(xiàn)找美女過(guò)程
public class Client{
   public static void main(Strings[] args){      
      IPettyGirl xiaoHong = new PettyGirl("小紅");//定義一個(gè)美女
      AbstractSearcher searcher = new Searcher(xiaoHong );
      searcher.show();
   }
}

乍一看好像沒(méi)啥問(wèn)題,細(xì)想想,“找美女”這個(gè)標(biāo)準(zhǔn)似乎還能細(xì)分,所謂接口還不夠“單純”不夠“小”
修改后代碼如下:

//兩種類型的美女定義
public interface IGoodBodyGirl{    
    public void goodLooking();//要有好的面孔    
    public void niceFigure();//要有好身材
}
public interface IGoodTemperamentGirl{    
    public void goodTemperament();//要有好氣質(zhì)
}
public class PettyGirl implements IGoodBodyGirl, IGoodTemperamentGirl{
   private String name;
   public PettyGirl(String name){
        this.name = name;
   }
   public void goodLooking(){
       System.out.println(name + "---有好的面孔");
   }
   public void niceFigure(){
       System.out.println(name + "---有好身材");
   }
   public void goodTemperament(){
       System.out.println(name + "---有好氣質(zhì)");
   }
}

看完小栗子,好像對(duì)“接口隔離”這個(gè)抽象概念有那么點(diǎn)抽象了解了。編程還需靠實(shí)踐,經(jīng)驗(yàn)積累是王道,謹(jǐn)記“高內(nèi)聚,低耦合,接口要小還要純”,運(yùn)用到實(shí)際編碼中,定會(huì)收獲奇效。


依賴倒置原則(Dependence Inversion Priciple)

High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstraction should not depend upon details.Details should depend upon abstractions.
1.高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象
2.抽象不應(yīng)該依賴細(xì)節(jié)
3.細(xì)節(jié)應(yīng)該依賴抽象

依賴倒置原則在Java語(yǔ)言中的表現(xiàn)就是:

1.模塊間的依賴通過(guò)抽象發(fā)生,實(shí)現(xiàn)類之間不發(fā)生直接的 依賴關(guān)系,其依賴關(guān)系是通過(guò)接口或抽象類產(chǎn)生的;
2.接口或抽象類不依賴于實(shí)現(xiàn)類
3.實(shí)現(xiàn)類依賴接口或抽象類

簡(jiǎn)而言之就是“面向接口編程(OOD)”----接口只跟接口交流,實(shí)現(xiàn)類通過(guò)依賴接口或抽象類實(shí)現(xiàn)擴(kuò)展。
依賴倒置和其他五條原則一脈相承,讓代碼結(jié)構(gòu)更包容,“擁抱變化”。其實(shí),即便不知道這條規(guī)則,我們也更傾向?qū)懗龇弦蕾嚨怪玫某绦颉_€是舉一個(gè)簡(jiǎn)單的栗子
沒(méi)有接口時(shí)這樣實(shí)現(xiàn)功能

public class A{
    public void a(B b){
        ···
    }
}
public static void main(String[] args){
        A objectA = new A();
        B objectB = new B();
        objectA .a(objectB );
 }

AB類間的緊耦合呼之欲出。這時(shí)候,若需要a方法對(duì)C類對(duì)象作同樣的操作,要怎么寫(xiě)呢?

public class A{
    public void a(B b){
        ···
    }
    public void a2(C c){
        ···
    }
}

天了嚕,太麻煩了!還是接口高屋建瓴,感受一下

public interface IA{
    public void a(IB b);
}
public class A implements IA{
    public void a(IB b){
        ···
    }
}
public interface IB{
    ···
}
public class B implements IB{
    ···
}
public class C implements IB{
    ···
}

這么一來(lái),每次擴(kuò)展,只需要增加一個(gè)實(shí)現(xiàn)類,靈不靈巧機(jī)不機(jī)智
引總結(jié)依賴的三種寫(xiě)法

1.構(gòu)造函數(shù)傳遞依賴對(duì)象
2.Setter方法傳遞依賴對(duì)象
3.接口聲明依賴對(duì)象

還是那句話,沒(méi)必要為了原則而原則,畢竟過(guò)猶不及。再次默念“對(duì)擴(kuò)展開(kāi)發(fā),對(duì)修改封閉”,“高內(nèi)聚,低耦合”,抓住根本原則才是編碼之本。


最后

編碼這事兒,雖立意創(chuàng)新,仍是規(guī)則之治,優(yōu)秀的結(jié)構(gòu)設(shè)計(jì)能助我們應(yīng)對(duì)變化時(shí)游刃有余。當(dāng)然,理論歸理論,應(yīng)用到實(shí)際問(wèn)題中還需具體問(wèn)題具體分析,不宥于形,融會(huì)貫通。
另外,關(guān)于寫(xiě)代碼,渾渾噩噩地寫(xiě)叫搬磚,條理清晰地寫(xiě)是技術(shù)。我們當(dāng)然希望隨著日復(fù)一日辛勤coding,編碼技能不斷增長(zhǎng),綜合素養(yǎng)逐漸深厚,以對(duì)得起所付出精力與汗水的成長(zhǎng)效率不斷逼近技術(shù)圣殿。這些理論原則如同內(nèi)功心法,武俠世界里,凡欲為大師,苦練內(nèi)功勢(shì)在必行。
進(jìn)擊吧程序員~coding不輟,祝我們的代碼清晰靈動(dòng),堅(jiān)不可摧,真正SOLID. 老哥,穩(wěn)!
學(xué)藝尚淺,歡迎各路大佬多多指正


參考書(shū)目:秦小波《設(shè)計(jì)模式之禪第2版》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 目錄: 設(shè)計(jì)模式六大原則(1):?jiǎn)我宦氊?zé)原則 設(shè)計(jì)模式六大原則(2):里氏替換原則 設(shè)計(jì)模式六大原則(3):依賴倒...
    加油小杜閱讀 739評(píng)論 0 1
  • 設(shè)計(jì)模式之六大原則(轉(zhuǎn)載) 關(guān)于設(shè)計(jì)模式的六大設(shè)計(jì)原則的資料網(wǎng)上很多...
    霄霄霄霄閱讀 911評(píng)論 0 1
  • 設(shè)計(jì)模式匯總 一、基礎(chǔ)知識(shí) 1. 設(shè)計(jì)模式概述 定義:設(shè)計(jì)模式(Design Pattern)是一套被反復(fù)使用、多...
    MinoyJet閱讀 3,970評(píng)論 1 15
  • 單一職責(zé)原則 (SRP) 全稱 SRP , Single Responsibility Principle 單一職...
    米莉_L閱讀 1,788評(píng)論 2 5
  • 本周的課程主要講了兩個(gè)方面的內(nèi)容:動(dòng)畫(huà)和網(wǎng)絡(luò) 動(dòng)畫(huà)方面,分別講了UIKit、Transition和CoreAnim...
    護(hù)林員閱讀 448評(píng)論 0 0