1. 程式人生 > >面向對象的六大原則

面向對象的六大原則

英文解釋 六大原則 關系 同時存在 開發 迪米特法則 生命 pos output

優化代碼的第一步——單一職責原則

  單一職責原則的英文名稱是Single Responsibility Principle,縮寫是SRP。SRP的定義是:就一個類而言,應該僅有一個引起它變化的原因。簡單來說,一個類中應該是一組相關性很高的函數、數據的封裝。就像秦小波老師在《設計模式之禪》中說的:“這是一個備受爭議卻又及其重要的原則。只要你想和別人爭執、慪氣或者是吵架,這個原則是屢試不爽的”。因為單一職責的劃分界限並不是總是那麽清晰,很多時候都是需要靠個人經驗來界定。當然,最大的問題就是對職責的定義,什麽是類的職責,以及怎麽劃分類的職責。

  對於計算機技術,通常只單純地學習理論知識並不能很好地領會其深意,只有自己動手實踐,並在實際運用中發現問題、解決問題、思考問題,才能夠將知識吸收到自己的腦海中。下面以我的朋友小民的事情說起。

  自從Android系統發布以來,小民就是Android的鐵桿粉絲,於是在大學期間一直保持著對Android的關註,並且利用課余時間做些小項目,鍛煉自己的實戰能力。畢業後,如願地加入了心儀的公司,並且投入到了自己熱愛的Android應用開發行業中。將愛好、生活、事業融為一體,小民的第一份工作也算是順風順水,一切盡在掌握中。

  在經歷過一周的適應期以及熟悉公司的產品、開發規範之後,開發工作就正式開始了。小民的主管是個工作經驗豐富的技術專家,對於小民的工作並不是很滿意,尤其是最薄弱的面向對象設計,而Android開發又是使用Java語言,程序中的抽象、接口、六大原則、23 種設計模式等名詞把小民弄得暈頭轉向,自己也察覺到了自己的問題所在。於是,主管決定先讓小民做一個小項目來鍛煉這方面的能力。正所謂養兵千日用兵一時,磨刀不誤砍柴工。小民的開發之路才剛剛開始。

  在經過一番思考之後,主管挑選了使用範圍廣、難度也適中的圖片加載器(ImageLoader)作為小民的訓練項目。既然要訓練小民的面向對象設計,那麽就必須考慮到可擴展性、靈活性,而檢測這一切是否符合需求的最好途徑就是開源。用戶不斷地提出需求、反饋問題,小民的項目需要不斷升級以滿足用戶需求,並且要保證系統的穩定性、靈活性。在主管跟小民說了這一特殊任務之後,小民第一次感到了壓力。“生活不容易啊!”年僅 22 歲的小民發出了這樣的感嘆!

  挑戰總是要面對的,何況是從來不服輸的小民。主管的要求很簡單,要實現圖片加載,並且要將圖片緩存起來。在分析了需求之後,小民一下就放心下來了,“這麽簡單,原來我還以為很難呢……”小民胸有成足地喃喃自語。在經歷了 10 分鐘的編碼之後,小民寫下了如下代碼。

/**
 * 圖片加載類
 */public  class ImageLoader {
    // 圖片緩存
    LruCache<String, Bitmap> mImageCache;
    // 線程池,線程數量為CPU的數量

ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.   
    getRuntime().availableProcessors());

    // UI Handler
         Handler mUiHandler = new Handler(Looper.getMainLooper()); 
    public  ImageLoader() {      
        initImageCache();
    }    private void initImageCache() {
            // 計算可使用的最大內存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
            // 取四分之一的可用內存作為緩存
        final int cacheSize = maxMemory / 4;
        mImageCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

    public  void displayImage(final String url, final ImageView imageView) {        imageView.setTag(url);    
        mExecutorService.submit(new Runnable() {       
            @Override
            public  void run() {
                Bitmap bitmap = downloadImage(url);
                if (bitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    updateImageView(imageView, bitmap);
                }                mImageCache.put(url, bitmap);            }        });

}  
    private void updateImageView(final ImageView imageView, final Bitmap bmp) {      
        mUiHandler.post(new Runnable() {          
                @Override
                public void run() {
                    imageView.setImageBitmap(bmp); 
            }     
        });  
    }

    public  Bitmap downloadImage(String imageUrl) {     
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());            conn.disconnect();
        } catch (Exception e) {       
            e.printStackTrace();
        }

       return bitmap;
    }
}

並且使用Git軟件進行版本控制,將工程托管到Github上,伴隨著git push命令的完成,ImageLoader 0.1版本就正式發布了!如此短的時間內就完成了這個任務,而且還是一個開源項目,小民暗暗自喜,並幻想著待會兒被主管稱贊。

  在給主管報告了ImageLoader的發布消息的幾分鐘之後,主管就把小民叫到了會議室。這下小民納悶了,怎麽誇人還需要到會議室。“小民,你的ImageLoader耦合太嚴重啦!簡直就沒有設計可言,更不要說擴展性、靈活性了。所有的功能都寫在一個類裏怎麽行呢,這樣隨著功能的增多,ImageLoader類會越來越大,代碼也越來越復雜,圖片加載系統就越來越脆弱……”這簡直就是當頭棒喝,小民的腦海裏已經聽不清主管下面說的內容了,只是覺得自己之前沒有考慮清楚就匆匆忙忙完成任務,而且把任務想得太簡單了。

  “你還是把ImageLoader拆分一下,把各個功能獨立出來,讓它們滿足單一職責原則。”主管最後說道。小民是個聰明人,敏銳地捕捉到了單一職責原則這個關鍵詞,他用Google搜索了一些資料之後,總算是對單一職責原則有了一些認識,於是打算對ImageLoader進行一次重構。這次小民不敢過於草率,也是先畫了一幅UML圖,如圖 1 所示:

技術分享

ImageLoader代碼修改如下所示:

/**
 * 圖片加載類
 */public  class ImageLoader {
    // 圖片緩存
    ImageCache mImageCache = new ImageCache() ;
    // 線程池,線程數量為CPU的數量
ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.   
    getRuntime().availableProcessors());
    // UI Handler
    Handler mUiHandler = new Handler(Looper.getMainLooper());   
    private void updateImageView(final ImageView imageView, final Bitmap bmp) {     
        mUiHandler.post(new Runnable() {        
                @Override
                public void run() {
                    imageView.setImageBitmap(bmp); 
            }     
        }); 
    }

    //加載圖片
    public  void displayImage(final String url, final ImageView imageView) {
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        imageView.setTag(url);
        mExecutorService.submit(new Runnable() {

            @Override
            public  void run() {
            Bitmap bitmap = downloadImage(url);
                if (bitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    updateImageView(imageView, bitmap);
                }
                mImageCache.put(url, bitmap);
            }
        });
    }

    public  Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
           URL url = new URL(imageUrl);
            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }

         return bitmap;
    }}

並且添加了一個ImageCache類用於處理圖片緩存,具體代碼如下:

public  class ImageCache {
    // 圖片LRU緩存
    LruCache<String, Bitmap>mImageCache;

    public  ImageCache() {
        initImageCache();
    }

    private void initImageCache() {
        // 計算可使用的最大內存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 取四分之一的可用內存作為緩存
        final int cacheSize = maxMemory / 4;
        mImageCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

    public  void put(String url, Bitmap bitmap) {
        mImageCache.put(url, bitmap) ;
    }

    public  Bitmap get(String url) {
        return mImageCache.get(url) ;
    }
}

  如圖 1 和上述代碼所示,小民將ImageLoader一拆為二,ImageLoader只負責圖片加載的邏輯,而ImageCache只負責處理圖片緩存的邏輯,這樣ImageLoader的代碼量變少了,職責也清晰了;當與緩存相關的邏輯需要改變時,不需要修改ImageLoader類,而圖片加載的邏輯需要修改時也不會影響到緩存處理邏輯。主管在審核了第一次重構之後,對工作給予了表揚,大致意思是結構變得清晰了許多,但是可擴展性還是比較欠缺。雖然沒有得到主管的完全肯定,但也是頗有進步,再考慮到自己確實有所收獲,原本沮喪的心裏也略微地好轉起來。

  從上述的例子中我們能夠體會到,單一職責所表達出的用意就是“單一”二字。正如上文所說,如何劃分一個類、一個函數的職責,每個人都有自己的看法,這需要根據個人經驗、具體的業務邏輯而定。但是,它也有一些基本的指導原則,例如,兩個完全不一樣的功能就不應該放在一個類中。一個類中應該是一組相關性很高的函數、數據的封裝。工程師可以不斷地審視自己的代碼,根據具體的業務、功能對類進行相應拆分,這是程序員優化代碼邁出的第一步。

讓程序更穩定、更靈活——開閉原則

  開閉原則的英文全稱是Open Close Principle,縮寫是OCP,它是Java世界裏最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟件中的對象(類、模塊、函數等)應該對於擴展是開放的,但是,對於修改是封閉的。在軟件的生命周期內,因為變化、升級和維護等原因需要對軟件原有代碼進行修改時,可能會將錯誤引入原本已經經過測試的舊代碼中,破壞原有系統。因此,當軟件需要變化時,我們應該盡量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。當然,在現實開發中,只通過繼承的方式來升級、維護原有系統只是一個理想化的願景,因此,在實際的開發過程中,修改原有代碼、擴展代碼往往是同時存在的。

  軟件開發過程中,最不會變化的就是變化本身。產品需要不斷地升級、維護,沒有一個產品從第一版本開發完就再沒有變化了,除非在下個版本誕生之前它已經被終止。而產品需要升級,修改原來的代碼就可能會引發其他的問題。那麽,如何確保原有軟件模塊的正確性,以及盡量少地影響原有模塊,答案就是,盡量遵守本文講述的開閉原則。

  勃蘭特·梅耶在 1988 年出版的《面向對象軟件構造》一書中提出這一原則——開閉原則。這一想法認為,程序一旦開發完成,程序中一個類的實現只應該因錯誤而被修改,新的或者改變的特性應該通過新建不同的類實現,新建的類可以通過繼承的方式來重用原類的代碼。顯然,梅耶的定義提倡實現繼承,已存在的實現類對於修改是封閉的,但是新的實現類可以通過覆寫父類的接口應對變化。

  開閉原則指導我們,當軟件需要變化時,應該盡量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。這裏的“應該盡量”4 個字說明OCP原則並不是說絕對不可以修改原始類的。當我們嗅到原來的代碼“腐化氣味”時,應該盡早地重構,以便使代碼恢復到正常的“進化”過程,而不是通過繼承等方式添加新的實現,這會導致類型的膨脹以及歷史遺留代碼的冗余。我們的開發過程中也沒有那麽理想化的狀況,完全地不用修改原來的代碼,因此,在開發過程中需要自己結合具體情況進行考量,是通過修改舊代碼還是通過繼承使得軟件系統更穩定、更靈活,在保證去除“代碼腐化”的同時,也保證原有模塊的正確性。

構建擴展性更好的系統——裏氏替換原則

  裏氏替換原則英文全稱是Liskov Substitution Principle,縮寫是LSP。LSP的第一種定義是:如果對每一個類型為S的對象 O1 ,都有類型為T的對象 O2 ,使得以T定義的所有程序P在所有的對象 O1 都代換成 O2 時,程序P的行為沒有發生變化,那麽類型S是類型T的子類型。上面這種描述確實不太好理解,我們再看看另一個直截了當的定義。裏氏替換原則第二種定義:所有引用基類的地方必須能透明地使用其子類的對象。

  我們知道,面向對象的語言的三大特點是繼承、封裝、多態,裏氏替換原則就是依賴於繼承、多態這兩大特性。裏氏替換原則簡單來說就是,所有引用基類的地方必須能透明地使用其子類的對象。通俗點講,只要父類能出現的地方子類就可以出現,而且替換為子類也不會產生任何錯誤或異常,使用者可能根本就不需要知道是父類還是子類。但是,反過來就不行了,有子類出現的地方,父類未必就能適應。說了那麽多,其實最終總結就兩個字:抽象。

  裏氏替換原則的核心原理是抽象,抽象又依賴於繼承這個特性,在OOP當中,繼承的優缺點都相當明顯。優點有以下幾點:

  • 代碼重用,減少創建類的成本,每個子類都擁有父類的方法和屬性;
  • 子類與父類基本相似,但又與父類有所區別;
  • 提高代碼的可擴展性。

繼承的缺點:

  • 繼承是侵入性的,只要繼承就必須擁有父類的所有屬性和方法;
  • 可能造成子類代碼冗余、靈活性降低,因為子類必須擁有父類的屬性和方法。

  事物總是具有兩面性,如何權衡利與弊都是需要根據具體情況來做出選擇並加以處理。裏氏替換原則指導我們構建擴展性更好的軟件系統裏氏替換原則就是建立抽象,通過抽象建立規範,具體的實現在運行時替換掉抽象,保證系統的擴展性、靈活性。開閉原則和裏氏替換原則往往是生死相依、不棄不離的,通過裏氏替換來達到對擴展開放,對修改關閉的效果。然而,這兩個原則都同時強調了一個OOP的重要特性——抽象,因此,在開發過程中運用抽象是走向代碼優化的重要一步。

讓項目擁有變化的能力——依賴倒置原則

  依賴倒置原則英文全稱是Dependence Inversion Principle,縮寫是DIP。依賴倒置原則指代了一種特定的解耦形式,使得高層次的模塊不依賴於低層次模塊的實現細節的目的,依賴模塊被顛倒了。這個概念有點不好理解,這到底是什麽意思呢?

  依賴倒置原則有以下幾個關鍵點:

  • 高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;
  • 抽象不應該依賴細節;
  • 細節應該依賴抽象。

  在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被實例化的;細節就是實現類,實現接口或繼承抽象類而產生的類就是細節,其特點就是,可以直接被實例化,也就是可以加上一個關鍵字new產生一個對象。高層模塊就是調用端,低層模塊就是具體實現類。依賴倒置原則在 Java語言中的表現就是:模塊間的依賴通過抽象發生,實現類之間不發生直接的依賴關系,其依賴關系是通過接口或抽象類產生的。這又是一個將理論抽象化的實例,其實一句話就可以概括:面向接口編程,或者說是面向抽象編程,這裏的抽象指的是接口或者抽象類。面向接口編程是面向對象精髓之一,也就是上面兩節強調的抽象。

  如果類與類直接依賴於細節,那麽它們之間就有直接的耦合,當具體實現需要變化時,意味著要同時修改依賴者的代碼,這限制了系統的可擴展性。要想讓系統更為靈活,抽象似乎成了我們唯一的手段。

系統有更高的靈活性——接口隔離原則

  接口隔離原則英文全稱是InterfaceSegregation Principles,縮寫是ISP。ISP的定義是:客戶端不應該依賴它不需要的接口。另一種定義是:類間的依賴關系應該建立在最小的接口上。接口隔離原則將非常龐大、臃腫的接口拆分成更小的和更具體的接口,這樣客戶將會只需要知道他們感興趣的方法。接口隔離原則的目的是系統解開耦合,從而容易重構、更改和重新部署。

  接口隔離原則說白了就是,讓客戶端依賴的接口盡可能地小,這樣說可能還是有點抽象,我們還是以一個示例來說明一下。在此之前我們來說一個場景,在Java 6 及之前的JDK版本,有一個非常討厭的問題,那就是在使用了OutputStream或者其他可關閉的對象之後,我們必須保證它們最終被關閉了,

  Bob大叔(Robert C Martin)在 21 世紀早期將單一職責、開閉原則、裏氏替換、接口隔離以及依賴倒置(也稱為依賴反轉)5 個原則定義為SOLID原則,作為面向對象編程的 5 個基本原則。當這些原則被一起應用時,它們使得一個軟件系統更清晰、簡單,最大程度地擁抱變化。SOLID被典型地應用在測試驅動開發上,並且是敏捷開發以及自適應軟件開發基本原則的重要組成部分。在經過學習之後,我們發現這幾大原則可以化為這幾個關鍵詞:抽象、單一職責、最小化。那麽,在實際開發過程中如何權衡、實踐這些原則,大家需要在實踐中多思考與領悟,正所謂“學而不思則罔,思而不學則殆”,只有不斷地學習、實踐、思考,才能夠在積累的過程中有一個質的飛越。

更好的可擴展性——迪米特原則

  迪米特原則英文全稱為Law of Demeter,縮寫是LOD,也稱為最少知識原則(Least Knowledge Principle)。雖然名字不同,但描述的是同一個原則:一個對象應該對其他對象有最少的了解。通俗地講,一個類應該對自己需要耦合或調用的類知道得最少,類的內部如何實現與調用者或者依賴者沒關系,調用者或者依賴者只需要知道它需要的方法即可,其他的可一概不用管。類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。

  迪米特法則還有一個英文解釋是Only talk to your immedate friends,翻譯過來就是:只與直接的朋友通信。什麽叫做直接的朋友呢?每個對象都必然會與其他對象有耦合關系,兩個對象之間的耦合就成為朋友關系,這種關系的類型有很多,如組合、聚合、依賴等。

小結

  在應用開發過程中,最難的不是完成應用的開發工作,而是在後續的升級、維護過程中讓應用系統能夠擁抱變化。擁抱變化也就意味著在滿足需求且不破壞系統穩定性的前提下保持高可擴展性、高內聚、低耦合,在經歷了各版本的變更之後依然保持清晰、靈活、穩定的系統架構。當然,這是一個比較理想的情況,但我們必須要朝著這個方向去努力,那麽遵循面向對象六大原則就是我們走向靈活軟件之路所邁出的第一步。

(本文節選自何紅輝,關愛民作品《Android 源碼設計模式解析與實戰》(第2版))

面向對象的六大原則