軟件設計原則

前言

在軟件設計和實際的開發過程中,軟件設計應該秉承一些基本的原則。除了須要考慮軟件開發所需的時間、成本、質量以外,還須要考慮到軟件系統的穩定性、可維護性、可擴展性等。本文將對軟件的7大設計原則進行一個講解和配套以實際業務場景,便於讀者更好的理解軟件設計的基本原則。
另外,在軟件設計中,軟件設計原則不是必定要所有遵照,而是須要根據實際的業務場景須要來作一個取捨,達到一個平衡。也許這就是軟件設計的藝術吧。php

開閉原則

定義:一個軟件實體如類、模塊、函數應該對擴展開放,對修改關閉
特色:用抽象構建框架,用實現擴展細節
優勢:提升軟件系統的可複用性和可維護性java

開閉原則是軟件系統設計最基本的原則。實現開閉原則的思想就是面向抽象編程。由於通常來講,抽象是穩定的。實現的細節是不可知的。python

業務場景

某培訓機構分別開設java、python、php課程,每一個課程都包含課程id、課程名、課程價格信息。在使用uml建模的時候,他們的類結構關係以下:
1.png
從模型中能夠看出,ICourse爲頂層父類,JavaCourse,PythonCourse,PhpCourse三個類爲實現類。他們分別實現了父類的獲取課程id、課程信息、課程價格的方法。這是最初的軟件版本。隨着時間的推移,培訓機構爲了擴展業務和吸引更多的學生來辦理培訓課程,須要實現可以對已有的課程進行打折活動,這個時候咱們就須要對軟件進行一個版本的升級。
此時咱們有兩種方法來實現:
第一種方法:好比是對Java課程打折。直接修改JavaCourse的源代碼,在裏面新增一個方法getDiscountPrice()或者使用已有的getPrice()方法進行修改獲取折扣價。phthon和php課程同理。
2.png
第二種方法:採用面向抽象的方法來編程,不直接修改類代碼,而是經過類繼承來實現。
3.png
使用第一種方法的好處是簡單、快速。可是當一個系統很是複雜和龐大的時候,越是對底層的代碼進行修改就越有可能帶來潛在的危機。你不能肯定對類中方法的修改會不會影響到其餘的方法調用,是否會帶來不可思議的災難。
使用第二種方法的壞處是可能須要多花費一點時間和精力。可是優勢也是顯而易見的。即不會影響原有的邏輯。在新的軟件開發版本中,即擴展了功能,又不影響以前的業務邏輯。提升了系統的可維護性和可獲展性。編程

開閉原則,就是在開發過程中,儘可能抽象出框架去進行擴展,杜絕對已有模塊的修改。框架

依賴倒置原則

定義:高層調用不該該依賴底層模塊,兩者都應該依賴其抽象
特色:高層不依賴細節,細節依賴抽象
優勢:模塊之間彼此獨立,互不影響,實現模塊之間的鬆耦合編程語言

業務場景

仍是上面的培訓機構,如今小明報了三門課程,實現的類圖以下
xiaoming.png函數

實現僞代碼以下:學習

XiaoMing{
    public void studyJavaCourse(JavaCourse java);
    public void pythonJavaCourse(pythonCourse java);
    public void phpJavaCourse(phpCourse java);
}


// 假設提供了帶參數構造函數,參數分別爲課程id,課程名稱,課程單價
main{
    XiaoMing xiaoMing = new XiaoMing();
    xiaoMing.studyJavaCourse(new JavaCourse(1,"java",100.0));
    xiaoMing.studyPhpCourse(new PhpCourse(2,"php",200.0));
    xiaoMing.studyPythonCourse(new PythonCourse(3,"python",300.0));
}

這是沒有用到依賴倒置原則時候的寫法。在實際開發中這種寫法極大的下降了代碼之間的耦合度和靈活性。下面看看使用到了依賴倒置原則的寫法。
xiaoming2.pngspa

這個時候,當咱們須要取得學習課程信息的僞代碼以下:設計

XiaoMing{
    public void studyCouser(ICouser couser);
}

// 假設提供了帶參數構造函數,參數分別爲課程id,課程名稱,課程單價
main{
    XiaoMing xiaoMing = new XiaoMing();
    xiaoMing.study(new JavaCourse(1,"java",100.0));
    xiaoMing.study(new PhpCourse(2,"php",200.0));
    xiaoMing.study(new PythonCourse(3,"python",300.0));
}

不知道你們看明白了沒有(注意看XiaoMing對象),一樣的實現功能,高層直接調用底層代碼,增長了代碼的複雜度。而調用底層的抽象,只提供一個對外接口就能實現功能。

單一職責原則

定義:不要存在致使類變動的多個緣由。
特色:接口、類、方法負責的職責是單一的
優勢:提升代碼的可讀性、可維護性、下降更改代碼的風險

業務場景

依舊是培訓機構的課程,如今要在課程的基礎上新增購買課程和課程退款。接口的非單一職責僞代碼以下:

public interface ICourse{
    getPrice();
    getCourseName();
    
    buyCouse();
    refundCourse();
}

該接口抽象出了「獲取課程信息」,「課程慣例」兩個職責,那麼獲取課程信息相關子類在實現父類的時候,也須要實現課程管理的職責相關方法,即便這徹底沒有必要。
咱們再來看下方法的非單一職責例子,僞代碼以下:

public void getIdOrName(String properties){
    if(properties.equals("id")){
        System.out.println("id");
    }else{
        System.out.println("name");
    }
}

在上面的代碼中,getIdOrName即能輸出課程id,也能輸出課程名稱。固然這裏代碼足夠簡單,看不出什麼。可是若是在複雜點,因爲調用邏輯的不清晰,可能會致使後續開發人員的困惑。影響代碼的可讀性。同時,在對這一塊代碼進行修改的時候,出現錯誤的狀況下可能會致使另外一職能的錯誤。爲了寫出更加清晰的代碼,應該寫成以下方式:

public void getId(){……};
public void getName(){……};

這樣,即便獲取課程id的方法出錯了,也不會影響到獲取課程名稱的相關功能。

接口隔離原則

定義:用單個專門的接口,而不是一個總的接口
特色:

1)一個類對另外一個類依賴的接口儘量簡單
2)不斷細化接口,但也要適度

優勢:提升代碼的靈活性和可讀性,符合高內聚低耦合思想。

業務場景

在動物身上的行爲有走、飛、游泳。設計一個頂層父類的接口,僞代碼以下:

public interface Animals{
    walk();
    fly();
    swimming();
}

如今有一隻鳥和一隻狗,實現了動物類

// 狗
Dog implments Animals{
    walk(){
        
    }
    fly(){
        
    }
    swimming(){
        
    }
}
// 鳥
Bird implments Animals{
    walk(){
        
    }
    fly(){
        
    }
    swimming(){
        
    }
}

能夠看出,不論是狗仍是鳥,都實現了動物走、飛、游泳的行爲。但是狗不會飛,鳥類也不必定會游泳。有些鳥也不必定會飛。那麼針對更加細化的對象來講,徹底沒有實現父類全部接口的必要。這個時候,咱們就須要對接口進行一個細化,也就是接口隔離,增長接口的細粒度。僞代碼以下:

interface Animals{
    
}
interface Walk implments Animals{
    walk();
}
interface Fly implments Animals{
    fly();
}
interface Swimging implments Animals{
    swimging();
}
// 狗
Dog implments Walk,Swimging{
    walk();
    swimging();
}
// 鳥
Bird implments Fly,Walk{
    fly();
    walk();
}

這樣,經過上面細化的接口,就能根據實際的須要去實現專門的接口。須要注意的是,適當的接口細化能提升代碼的靈活性,但也不是越多越好,細化得越多,相應的系統複雜度也會增長。

引伸:單一職責和接口隔離職責差別

單一職責強調得是類、接口、方法實現細節的功能惟一性,接口隔離是對接口的限制,是在抽象層面對接口進行的高內聚低耦合實現。

迪米特法則(最少知道原則)

定義:對一個類的瞭解的越少越好
特色:只對朋友交流,不和陌生人打交道
優勢:下降類之間的耦合度。

這裏的朋友是指具備組合、依賴關係和類。對於其餘沒有關係的類不要引入類中。具體詳細瞭解請看下面的業務場景。

業務場景

老闆去查看團隊的員工數量。僞代碼以下:

Boss{
    checkTeam(Team t){
        List<People> p = new ArrayList<People>();
        for(int i=0;i<10;i++){
            p.add(new People());
        }
        t.countPeople(List<People> p); // 輸出團隊成員人數
    }
}

Team{
    countPeople(List<People> p){
    System.out.println(p.length());
    }
}

在本例中的類關係如圖:
dimite1.png
老闆和員工是依賴關係(員工類是方法的參數),可是老闆的檢查員工方法中卻出現和老闆無關的People類,這裏陌生人依賴(People類)也依賴咱們的老闆類。這就違背了咱們的最少知道原則。

按照最少知道原則,他們之間的關係應該是People類是Team類的依賴,Boss類是Team類的依賴。
dimite2.png
僞代碼以下:

Boss{
    checkTeam(Team t){
        t.countPeople(); // 輸出團隊成員人數
    }
}

Team{
    countPeople(List<People> p){
    List<People> p = new ArrayList<People>();
        for(int i=0;i<10;i++){
            p.add(new People());
        }
    System.out.println(p.length());
    }
}

最少知道原則下降了類之間的耦合度。在實際開發中,應該遵循使用。

里氏替換原則

定義:父類出現的地方,子類也能夠出現,而且子類替換父類後,不會產生任何問題。
特色:里氏替換原則包含了四層含義,以下

1) 子類必須徹底實現父類的方法
2)子類能夠擁有本身的個性
3)覆蓋或實現父類方法時,輸入參數能夠被放大
4)覆蓋或實現父類方法時,返回結果能夠被縮小

組合/複用原則

定義:在一個新的對象裏使用已有對象,經過委派該對象來實現對已有功能的服用。
特色:符合 Has—A 關係
優勢:提升了代碼的封裝性,相比較於繼承,隱蔽了代碼的細節。

爲何須要使用組合/複合原則?在什麼狀況下使用?

實現代碼功能的複用,有兩種方法,一種是繼承,一種是組合。使用繼承的時候,須要遵循里氏替換原則,咱們在修改父類基類的時候,相關的子類也須要作一個修改。這無疑提升的代碼的維護性。使用組合的方法,經過委派成員變量複用對象功能,該對象功能的調整,並不會影響到委託類的代碼。代碼可維護性更高。咱們經過下面人的屬性來比較如下繼承和組合的差別。

做爲一個社會人,有多重屬性,繼承僞代碼以下

Person{
    public Person(){}
}
Teacher extends Person{
    System.out.println("teacher");
}
Student extends People{
    System.out.println("student");
}

經過以上方式,能夠看到,咱們新建立一個Person其子類對象的時候,這個子類只能是老師或者只能是學生。可是實際生活當中,一我的的屬性是多樣的,因此在建立對象的時候,要可以實現他多是老師,也有多是學生的要求。這個時候就得用組合方式了。僞代碼以下:

Person{
    private Role role;
    setRole(){};
    getRole(){};
}
interface Role{}
Teacher implements Role{
    
}
Student implements Role{
    
}

在上面方式中,動態應用的時候,經過Person的getPerson方法調用對象身份屬性。在setPerson時用的是那個對象就是哪一個身份屬性。那麼咱們如何判斷在什麼狀況下合理使用繼承或者組合呢,其實很簡單,咱們若是不能斷定一個子類在將來是否會變成別的子類的時候,就是用組合的方式。若是一個子類的對象信息是固定不變的,就是用繼承。還有一種方式,就是使用抽象的思惟,看是is-a關係,仍是has-a關係。is-a使用繼承方式。has-a使用組合方式

總結

縱觀這些原則,針對的是面向對象編程的思想,關注點是編程語言的封裝性、繼承性、多態性。經過這三大特性,是軟件框架變得更加清晰,可維護性更好,可擴展性更強。但願自身在實際應用中,可以合理使用這些原則。

相關文章
相關標籤/搜索