六大設計原則淺析

一、設計在軟件開發中的重要性

重要性

在上大學的時候我們總是不理解爲什麼要講這麼理論性的東西,當時就一個感覺就是沒什麼用,我們更想去學習一些可以看到結果的東西,當你畢業之後就會發現基礎的知識是多麼重要,而這些知識都有一個共性就是可以脫離具體的技術或者問題而存在,是一種可以長期指導我們學習和進步的重要思想,設計原則和模式就是軟件開發中的這種思想。

設計原則

我們先來思考一個問題:

怎麼樣的軟件纔算一個好的軟件或者說對於程序員我們如何評價他(她)的編碼技術?

我們來假設一個項目是由某個程序員獨立去完成的,他做的事情不僅僅編碼這麼簡單,編寫實現功能的代碼只佔整個項目的30%都不到,他首先做的就應該是對整個項目的技術選擇和框架設計(需求的學習和理解暫且不考慮),接下來纔是正式編寫代碼,在實現過程中又需要多次的測試和修改(重構),這樣就夠了嗎?如果能做到這些是可以開發出一個完整的軟件,但是還不夠。軟件工程和蓋房子有所區別的地方就在這裏,房子蓋好就不需要拆了重蓋了,軟件開發中我們還需要考慮到日後的迭代和變更,所以我們要做到整個結構有一個好的可維護性,設計原則是什麼?設計原則就是指導我們實現這種結構的理論基礎(也可以說是思想)。

設計模式

好了,設計原則有了,我也知道了應該遵循什麼原則了,接下來怎麼辦?我該如何去在實際工程中運用這樣的原則?設計模式就是爲了解決這些問題而出現的,說白了設計模式就是我們智慧的老前輩們總結出來一些遵循六大設計原則的面向對象的實際應用方式。我們學會了這些設計模式可以使我們更加理解到設計原則的重要性,而設計原則也能幫助我們記憶和靈活應用各種設計模式。

二、六大設計原則

單一職責原則

單一職責原則的英文名稱Single Responsibility Principle,簡稱SRP,單一職責的定義是:There should never be more than one reason for a class to change.(應該有且僅有一個原因引起類的變更)。

這個原則就是知道我們如何去封裝一個對象(或者說如何去劃分和定義類),“萬物皆對象”這句話表明,任何事物都是對象,如何定義一類事物,完全由我們自己的需要決定。單一職責原則就是說明我們在定義(或者叫劃分)一個類的時候應該儘可能的讓他做一件事情,看起來這個原則很簡單,但是問題恰恰就出在這個簡單的“一件事情”上,如何來區分是一件事情就是一個問題?舉個例子來說

/**
 * 電話接口
 * @author PeggyTong
 *
 */
public interface IPhone {

    //撥打電話
    public void dial(String phoneNumber);

    //通話
    public void chat(Object obj);

    //掛斷電話
    public void hangup();
}

上面這接口看起來沒有什麼問題,它就是做一件事情打電話(看成我們的手機),但是如果我們這裏把一件事情定義成單純的撥打電話,那麼它又不是在做一件事情,所以說這裏的單一職責要根據我們具體的業務邏輯來定,而不是越小越好的。
單一職責有什麼作用呢?

1、類的複雜性降低,實現什麼職責都有清晰明確的定義。
2、可讀性提高,複雜性降低所以提高了可讀性。
3、可維護性提高。
4、變更引起的風險降低,一個接口的修改只對相應的實現類有影響,對其他接口無影響。

其實在軟件開發中單一職責不僅僅對於類和接口是這樣,對於方法的定義也應該見名知其意(做一件事)。

里氏替換原則

(本文出自水寒的CSDN博客:http://blog.csdn.net/dawanganban

第一種定義:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果對每一個類型爲S的對象o1,都有類型爲T的對象o2,使得以T定義的所有程序P在所有的對象o1都代換成o2時,程序P的行爲沒有發生變化,那麼類型S是類型T的子類型)

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

第二個定義是最清晰明確的,通俗點講,只要父類能出現的地方子類就可以出現,而且替換爲子類也不會產生任何錯誤或者異常,使用者可能根本不需要知道父類還是子類。但是反過來就不行了,有子類出現的地方,父類未必就能適應。
看到這個原則你可能會想,爲什麼要有這個原則,這個原則有什麼用?單一職責原則的作用還能想明白,這個完全搞不懂有何用。
要理解這個原則的作用,首先你得知道面向對象的一個重要特徵叫多態,多態的好處也可以說是里氏替換原則的好處,多態可以把不同的子類當作同一個父類來看待,這樣就可以屏蔽子類之間的差異,寫出通用的代碼,做出通用的編程,以適應需求的不斷變化。
里氏替換原則只是告訴我們不應該怎麼做,並沒有告訴我們一個可行的繼承方式,於是,工程師們開始關注如何確保對象的行爲。1988年,B. Meyer提出了Design by Contract(契約式設計)理論。DbC從形式化方法中借鑑了一套確保對象行爲和自身狀態的方法,其基本概念很簡單:

Pre-condition:
每個方法調用之前,該方法應該校驗傳入參數的正確性,只有正確才能執行該方法,否則認爲調用方違反契約,不予執行。這稱爲前置條件(Pre-condition)。
Post-Condition:
每個方法調用之前,該方法應該校驗傳入參數的正確性,只有正確才能執行該方法,否則認爲調用方違反契約,不予執行。這稱爲前置條件(Pre-condition)。
Invariant:
對象本身有一套對自身狀態進行校驗的檢查條件,以確保該對象的本質不發生改變,這稱之爲不變式(Invariant)。

我們在Java中很容易看到繼承的這些特點,如果我們不符合這些特徵,編譯器會告訴我們錯誤或警告。

依賴倒置原則

依賴倒置原則(Dependence Inversion Principle, DIP),定義:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
1、高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象。
2、抽象不應該依賴於細節。
3、細節應該依賴於抽象。

高層模塊和低層模塊容易理解,每一個邏輯的實現都是由原子邏輯組成的,不可分割的原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊。那麼什麼是抽象?什麼又是細節呢?在Java語言中,抽象就是接口或者抽象類,兩者都是不能直接被實例化的;細節就是實現類,實現接口或者抽象類而產生的類就是細節,其特點就是可以直接被實例化,也就是可以加上一個關鍵字new產生一個對象。依賴倒置原則在java語言中的表現是:
1、模塊間的依賴通過抽象發送,實現類之間不發生直接的依賴關係,其依賴關係是通過接口或者抽象類產生的。
2、接口或者抽象類不依賴於實現類。
3、實現類依賴於接口或者抽象類。
採用依賴倒置原則可以減少類之間的耦合性,提供系統的穩定性,降低並行開發引起的風險,提高代碼的可讀性和可維護性。
依賴有三種方式:
1、構造函數傳遞依賴對象

package cn.org.sunhome.yldz;
//具體的司機
public class Driver implements IDriver{

    private ICar mCar;

    public Driver(ICar car){
        mCar = car;
    }

    @Override
    public void drive() {
        mCar.run();
    }
}

2、Setter方法傳遞依賴對象

package cn.org.sunhome.yldz;
//具體的司機
public class Driver implements IDriver{

    private ICar mCar;

    public void setCar(ICar car){
        mCar = car;
    }

    @Override
    public void drive() {
        if(mCar == null) return;
        mCar.run();
    }
}

3、接口聲明依賴對象(也叫接口注入)也就是上面的方式

package cn.org.sunhome.yldz;
//司機接口
public interface IDriver {
    public void drive(ICar car);
}

依賴倒置原則的本身就是通過抽象(接口或者抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的耦合,我們怎麼在項目中使用這個規則呢?只要遵循以下幾個規則就可以:
1、每個類儘量都有接口或者抽象類,或者抽象類和接口兩者都具備。
2、變量的表面類型儘量是接口或者抽象類。
3、任何類都不應該從具體類派生。
4、儘量不要覆寫基類的方法。
5、結合里氏替換原則使用。

接口隔離原則

定義一:Client should not be forced to depend upon interface that they don’t use.(客戶端不應該依賴它不需要的接口)
定義二:The dependency of one class to another one should depend on the smallest possible interface(類間的依賴關係應該建立在最小接口上)

上面兩個定義其實可以這樣理解,我們在建立接口的時候要儘量細化接口,同時接口中的方法要儘量的少。
接口隔離原則是對接口進行規範約束,其包含以下4個含義:
1、接口要儘量小。
2、接口要高內聚。
3、定製服務。
有時候我們的接口需要爲各個客戶端定製,這個時候應該將接口拆分。
4、接口設計是有限度的。
接口的設計顆粒度越小,系統越靈活,這個是一個不用爭論的事實,但是靈活的同時卻容易帶來結構上的複雜化,開發難度增加,可維護性降低,所以接口的設計一定要注意適度。
接口隔離原則是對接口的定義,同時也是對類的定義,接口和類儘可能使用原子接口或原子類來組裝。但是,這個原子該怎麼劃分是一個設計模式中的難題,在實踐中可以根據以下幾個規則來衡量:
1、一個接口只服務於一個子模塊或業務邏輯。
2、通過業務邏輯壓縮接口中的public方法。
3、已經被污染的接口,儘量去修改,若變更風險較大,則採用適配器模式進行轉化處理。
4、瞭解環境,拒絕盲從。每個項目或產品都有特定的環境因素,環境不同,接口拆分的標準就不同,最好的接口設計的前提是深入理解業務邏輯。

迪米特法則

迪米特法則(Law of Demeter, LoD)也稱爲最少知識原則(Leak Knowledge Principle, LKP),雖然名字不同,但描述的是同一個規則:一個對象應該對其他對象有最少的瞭解。通俗的講,一個類應該對自己需要耦合或調用的類知道的最少。

如果一個系統符合迪米特法則,那麼當其中某一個模塊發生修改時,就會盡量少地影響其他模塊,擴展會相對容易,這是對軟件實體之間通信的限制,迪米特法則要求限制軟件實體之間通信的寬度和深度。迪米特法則可降低系統的耦合度,使類與類之間保持鬆散的耦合關係。
迪米特法則還有幾種定義形式,包括:不要和“陌生人”說話、只與你的直接朋友通信等,在迪米特法則中,對於一個對象,其朋友包括以下幾類:
(1) 當前對象本身(this);
(2) 以參數形式傳入到當前對象方法中的對象;
(3) 當前對象的成員對象;
(4) 如果當前對象的成員對象是一個集合,那麼集合中的元素也都是朋友;
(5) 當前對象所創建的對象。
任何一個對象,如果滿足上面的條件之一,就是當前對象的“朋友”,否則就是“陌生人”。在應用迪米特法則時,一個對象只能與直接朋友發生交互,不要與“陌生人”發生直接交互,這樣做可以降低系統的耦合度,一個對象的改變不會給太多其他對象帶來影響。
迪米特法則要求我們在設計系統時,應該儘量減少對象之間的交互,如果兩個對象之間不必彼此直接通信,那麼這兩個對象就不應當發生任何直接的相互作用,如果其中的一個對象需要調用另一個對象的某一個方法的話,可以通過第三者轉發這個調用。簡言之,就是通過引入一個合理的第三者來降低現有對象之間的耦合度。
在將迪米特法則運用到系統設計中時,要注意下面的幾點:在類的劃分上,應當儘量創建鬆耦合的類,類之間的耦合度越低,就越有利於複用,一個處在鬆耦合中的類一旦被修改,不會對關聯的類造成太大波及;在類的結構設計上,每一個類都應當儘量降低其成員變量和成員函數的訪問權限;在類的設計上,只要有可能,一個類型應當設計成不變類;在對其他類的引用上,一個對象對其他對象的引用應當降到最低。

開閉原則

開閉原則的定義:Software entities like classes,modules and functions should be open for extension but closed for modifications(一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。)

開閉原則的定義已經非常明確地告訴我們軟件實體應該對擴展開放,對修改關閉,其含義是說一個軟件實體應該通過擴展來實現變化,而不是通過修改已有的代碼來實現變化。什麼是軟件實體?軟件實體包括以下幾個部分:
1、項目或軟件產品中按照一定的邏輯規則劃分的模塊。
2、抽象和類。
3、方法。
一個軟件產品只要在生命期內,都會發生變化,既然變化是一個既定是事實,我們就應該在設計時儘量適應這種變化,以提高項目的穩定性和靈活性,真正實現“擁抱變化”。開閉原則告訴我們應該儘量通過擴展軟件實體的行爲來實現變化,而不是通過修改已有的代碼來完成變化,它是爲軟件實體的未來事件而制定的對現行開發設計進行約束的一個原則。
我們
實現開閉原則的關鍵就在於“抽象”。把系統的所有可能的行爲抽象成一個抽象底層,這個抽象底層規定出所有的具體實現必須提供的方法的特徵。作爲系統設計的抽象層,要預見所有可能的擴展,從而使得在任何擴展情況下,系統的抽象底層不需修改;同時,由於可以從抽象底層導出一個或多個新的具體實現,可以改變系統的行爲,因此系統設計對擴展是開放的。
下面我們來看一個例子

public interface IBook {

    //書籍有名稱
    public String getName();
    //書籍有售價
    public int getPrice();
    //書籍有作者
    public String getAuthor();
}

//小說
public class NovelBook implements IBook{

    private String name;
    private int price;
    private String author;

    public NovelBook(String name, int price, String author){
        this.name = name;
        this.price = price;
        this.author = author;
    }

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

    @Override
    public int getPrice() {
        return price;
    }

    @Override
    public String getAuthor() {
        return author;
    }
}

//書店銷售
public class BookStore {

    private final static ArrayList bookList = new ArrayList();

    static{
        bookList.add(new NovelBook("天龍八部", 3200, "金庸"));
        bookList.add(new NovelBook("巴黎聖母院", 5600, "雨果"));
        bookList.add(new NovelBook("悲催世界", 3500, "雨果"));
        bookList.add(new NovelBook("金瓶梅", 4300, "蘭陵笑笑生"));
    }

    public static void main(String[] args){
        NumberFormat formatter = NumberFormat.getCurrencyInstance();
        formatter.setMaximumFractionDigits(2);
        System.out.println("----------------書店賣出去的書籍記錄如下------------------");
        for(IBook book : bookList){
            System.out.println("書籍名稱:" + book.getName() + 
                    "\t書籍作者" + book.getAuthor() + "\t書籍價格" + 
                    formatter.format(book.getPrice() / 100.0) + "元");
        }
    }
}

如果現在書店開始打折銷售:所有40元以上的書籍9折銷售,其他8折銷售,對於這個變化,我們應該如何應對,有三種方法可以解決:
1、修改接口
在IBook上新增一個方法getOffPrice(),專門用於進行打折處理,所有實現類實現該方法。但是這樣修改的後果就是,實現類NovelBook要修改,BookStroe中的main方法也修改,同時IBook作爲接口應該是穩定且可靠的,不應該經常發生變化,否則接口作爲契約的作用失去了效能,因此,該方案否定。
2、修改實現類
修改NovelBook類中的方法,直接在getPrice()中實現打折,但是這個不是好辦法,因爲如果採購人員要看實際價格就沒辦法查看了。
3、通過擴展實現變化
增加一個子類OffNovelBook,覆寫getPrice()方法,高層次的模塊通過OffNovelBook類產生對象,完成業務變化對系統的最小開發,好辦法!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章