1. 程式人生 > >Java設計模式之——面向物件六大原則

Java設計模式之——面向物件六大原則

面向物件六大原則:

  • 設計模式六大原則(1):單一職責原則
  • 設計模式六大原則(2):開閉原則
  • 設計模式六大原則(3):里氏替換原則
  • 設計模式六大原則(4):依賴倒置原則
  • 設計模式六大原則(5):介面隔離原則
  • 設計模式六大原則(6):迪米特原則

設計模式六大原則(1):單一職責原則

單一職責原則的英文名稱是 Single Responsibility Principle,縮寫為 SRP。SRP 的定義是:就一個類而言,應該僅有一個引起它變化的原因。簡單來說,一個類中應該是一組相關性很高的函式、資料的封裝。因為單一職責的劃分接線並不是總是那麼清晰,很多時候都需要靠個人經驗來界定。當然,最大的問題就是對職責的定義,什麼是類的職責,以及怎麼劃分類的職責。

這裡我們將選用使用範圍廣、難度也適中的圖片載入器(ImageLoader)作為訓練專案。

要想實現 ImageLoader,那麼我們就需要從最簡單的做起,首先需要實現 ImageLoader 的圖片載入,並且要將圖片快取起來。咦!貌似功能也不是很多麼,對我們來說也就是幾分鐘就搞定的事,然後就寫出瞭如下程式碼:

public class ImageLoader {
    //圖片快取
    LruCache<String, Bitmap> mImageCache;
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public
ImageLoader() { initImageCache(); } private void initImageCache() { //計算機可使用的最大記憶體 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 取 八分之一的可用記憶體作為快取 final int cacheSize = maxMemory / 8; mImageCache = new LruCache<String, Bitmap>(cacheSize) { @Override
protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight() / 1024; } }; } public void displayImage(final String url, final ImageView imageView) { imageView.setTag(url); final Bitmap bitmap = mImageCache.get(url); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } mExecutorService.submit(new Runnable() { @Override public void run() { Bitmap downLoadBitmap = downLoadImage(url); if (downLoadBitmap == null) { return; } if (imageView.getTag().equals(url)) { imageView.setImageBitmap(downLoadBitmap); } mImageCache.put(url, downLoadBitmap); } }); } private Bitmap downLoadImage(String imageUrl) { Bitmap bitmap = null; try { URL url = new URL(imageUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); InputStream inputStream = connection.getInputStream(); bitmap = BitmapFactory.decodeStream(inputStream); connection.disconnect(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return bitmap; } }

上面的程式碼已經實現了我們想要的效果,並且已經可以基本正常使用了。那麼,現在我們來考慮一個問題:比如你在現實開發中,你的主管接到了產品提出的新需求,這次的需求呢大概分為 2 個模組的開發,其實 2 個模組說多不多,畢竟一般公司單位中的標配都是 2 個開發人員,2 個模組對於兩個開發人員來說是小 case 了。但是如果你的主管硬是把這 2 個模組的開發任務都交給你來完成,並且時間很趕的話,這時候你會有什麼想法呢?會不會心裡不痛快?明明有兩個開發人員,我把另一個夥伴的任務做了,他不是就沒事情可做了麼?肯定當然會相當不憤了。其實這種情況放到我們上面的程式碼中也是一樣的,雖然說把所有任務都交由你來完成,照樣可以完成的很漂亮。但是明明兩個人合作開發即省時又省力,卻交給一個人來加班加點這樣是不是風險有點高?例子不是很恰當,大家有這個概念就可以。上面的例子也一樣,ImageLoader 類不僅要完成圖片的顯示還要管理記憶體快取以及順帶著還要去請求網路下載圖片,這樣是不是太不夠意思了?我們來修改一下程式碼在來看一下:

ImageLoader 程式碼修改如下:

public class ImageLoader {
    //圖片快取
    LruImageCache mImageCache = null;
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        mImageCache = new LruImageCache();
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        final Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = DownLoadImage.downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }
}

新增一個 LruImageCache 類用於處理圖片的快取,具體程式碼如下:

public class LruImageCache {
    //圖片快取
    LruCache<String, Bitmap> mImageCache;

    public LruImageCache() {
        //計算機可使用的最大記憶體
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 取 八分之一的可用記憶體作為快取
        final int cacheSize = maxMemory / 8;
        mImageCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
    }

    public void put(String url, Bitmap bitmap) {
        if (mImageCache != null) {
            mImageCache.put(url, bitmap);
        }
    }

    public Bitmap get(String url) {
        Bitmap bitmap = null;
        if (mImageCache != null) {
            bitmap = mImageCache.get(url);
        }
        return bitmap;
    }
}

新增 DownLoadImage 類用於從網路下載圖片,具體程式碼如下:

public class DownLoadImage {
    public static Bitmap downLoadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            InputStream inputStream = connection.getInputStream();
            bitmap = BitmapFactory.decodeStream(inputStream);
            connection.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

這裡將 ImageLoader 一拆為三,ImageLoader 只負責圖片載入的邏輯,而 LruImageCache 只負責處理圖片的快取邏輯,最後的 DownLoadImage 用來負責網路圖片的獲取,這樣 ImageLoader 的程式碼量就變少了,職責也清晰了;當與快取相關的邏輯需要改變時,不需要修稿 ImageLoader 類的邏輯,而圖片載入的邏輯需要修改是也不會影響到快取處理邏輯。

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

設計模式六大原則(2):開閉原則

開閉原則的英文全稱是 Open Close Principle,縮寫是 OCP,它是 Java 世界裡最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟體中的物件(類、模組、函式等)應該對於擴充套件是開放的,但是,對於修改是封閉的。在軟體的生命週期內,因為變化、升級和維護等原因需要對軟體原有程式碼進行修改時,可能會將錯誤引入原本已經經過測試的舊程式碼中,破壞原有系統。因此,當軟體需要變化時,我們應該儘量通過擴充套件的方式來實現變化,而不是通過修改已有的程式碼來實現。也就是說,程式一旦開發完成,程式中的一個類的實現只應該因錯誤而被修改,新的或者改變的特性應該通過新建不同的類實現,新建的類可以通過繼承的方式來重用原類的程式碼。已存在的實現類對於修改是封閉的,但是新的實現類可以通過複寫父類的介面應對變化。當然在現實開發中,只通過繼承的方式來升級、維護原有系統只是一個理想化的願景,因此,在實際的開發過程中,修改原有程式碼、擴充套件程式碼往往是同時存在的。

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

經過我們採用單一原則重構後的 ImageLoader 職責單一、結構清晰,但是隨著需求的變更,我們的 ImageLoader 也暴露出了一些問題。最嚴重的問題就是我們的快取系統,通過記憶體快取解決了每次從網路載入圖片的問題,但是,Android 應用的記憶體很有限,且具有易失性,即當應用重新啟動之後,原來已經載入過的圖片將會丟失,這樣重啟之後就需要重新下載!這又會導致載入緩慢、耗費使用者流量的問題。經過考慮我們決定引入 SD 卡快取,這樣下載過的圖片就會快取到本地,即使重啟應用也不需要重新下載了。下面是我們修改後的程式碼:

DiskCache.Java 類,將圖片快取到 SD 卡中::

public class DiskCache {
    static String cacheDir = "sdcard/cache";

    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }

    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (fileOutputStream !=null){
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

因為需要將圖片快取到 SD 卡中,所以,ImageLoader 程式碼有所更新,具體程式碼如下:

public class ImageLoader {
    //圖片快取
    LruImageCache mImageCache = null;
    //SD 卡快取
    DiskCache mDiskCache = null;
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        mImageCache = new LruImageCache();
        mDiskCache = new DiskCache();
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        bitmap = mDiskCache.get(url);

        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }


        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = DownLoadImage.downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mDiskCache.put(url, downLoadBitmap);
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }
}

從上述程式碼可以看出,僅僅新增了一個 DIskCache 類 和 往 ImageLoader 類中加入了少量程式碼就實現了 SD 卡快取功能。

到這裡為止不知道大家發現了問題沒有,上面的程式碼我們要想加入 SD 卡快取需要更改 ImageLoader 類的邏輯,這就不是一個好的地方。考慮這麼一種情況:比如說上面我們已經加入了 SD 卡快取,那麼有沒有這麼一種情況呢?現在我又不想要 SD 卡 快取了怎麼辦?我們是不是還需要改 ImageLoader 的邏輯? 如果之後我又不想要記憶體快取了呢?我們是不是改完之後還要改?先不說來回更改個人煩不煩的問題,就是這樣頻繁的修改 ImageLoader 的邏輯會不會有失誤的時候?如果那天心煩意亂了,改錯了咋整?這些都是問題。到了這裡我們是不是可以考慮一些我們剛剛提到的“開閉原則”呢?對於擴充套件是開放的,但是對於修改是封閉的。那麼我們應該怎麼實現呢?先不要著急寫程式碼,我們先來試著畫一個類圖出來:

這裡寫圖片描述

其實通過類圖已經給的很明確了,我們只需要給出程式碼就可以了

先看 ImageCache 介面,ImageCache 介面簡單定義了獲取、快取圖片兩個函式,快取的 key 是圖片的url,值是圖片本身。記憶體快取、SD卡快取已經可能純在的其他快取方式都實現了該介面,具體程式碼如下所示:

public interface ImageCache {
    public void put(String url, Bitmap bitmap);

    public Bitmap get(String url);
}

LruImageCache 和 DiskCache 的程式碼幾乎沒修改,就是在類上實現了 ImageCache 介面:

//DiskCache
public class DiskCache implements ImageCache{

//LruImageCache
public class LruImageCache implements ImageCache {

我們再來看看 ImageLoader 的程式碼,是不是簡化了很多:

public class ImageLoader {
    //圖片快取
    ImageCache mImageCache = null;

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

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        mImageCache = new LruImageCache();
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = DownLoadImage.downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }

    public void setImageCache(ImageCache imageCache) {
        this.mImageCache = imageCache;
    }
}

細心的朋友可能已經注意到了,我們在 ImageLoader 類中增加了一個 setImageCache(ImageCache cache) 函式,使用者可以通過該函式設定快取實現,也就是通常說的依賴注入。下面看使用者是如何設定快取實現的:

ImageLoader imageLoader = new ImageLoader();
imageLoader.setImageCache(new DiskCache());
imageLoader.setImageCache(new LruImageCache());

在上述程式碼中,通過 setImageCache(ImageCache imageCache)方法注入不同的快取實現,這樣不僅能夠使 ImageLoader 更簡單、健壯,也使得 ImageLoader 的可擴充套件性、靈活性更高。通過我們對 ImageLoader 的再次重構,已經可以完全實現快取模式的自由切換,想使用 記憶體快取就使用記憶體快取,想使用SD卡快取就使用SD卡快取。可能還有人會說:如果我想使用三級快取呢?那也好說,我們只需要定義一個類繼承 ImageCache,在類的內部實現三級快取即可。

三級快取實現如下:

public class ThreeLevelCache implements ImageCache {

    private LruImageCache mLruImageCache;
    private DiskCache mDiskCache;

    public ThreeLevelCache() {
        this.mLruImageCache = new LruImageCache();
        this.mDiskCache = new DiskCache();
    }

    @Override
    public void put(String url, Bitmap bitmap) {

    }

    @Override
    public Bitmap get(String url) {
        Bitmap bitmap = null;
        bitmap = mLruImageCache.get(url);
        if (bitmap != null) {
            return bitmap;
        }

        bitmap = mDiskCache.get(url);
        if (bitmap != null) {
            return bitmap;
        }

        bitmap = DownLoadImage.downLoadImage(url);
        if (bitmap != null) {
            mDiskCache.put(url, bitmap);
            mLruImageCache.put(url, bitmap);
            return bitmap;
        }
        return null;
    }
}

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

設計模式六大原則(3):里氏替換原則

里氏替換原則英文全稱是 Liskov Substitution Principle,縮寫是 LSP。

LSP 的第一種定義是:如果對每一個型別為 S 的物件 O1,都有型別為 T 的物件 O2,使得以 T 定義的所有程式 P 在所有的物件 O1 都替換成 O2 時,程式 P 的行為沒有發生變化,那麼型別 S 是型別 T 的子型別。

上面這種描述確實不太好理解,我們再看另一個直接了當的定義:

里氏替換原則第二種定義:所有引用基類的地方必須能透明地使用其子類物件。

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

為了方便理解里氏替換原則,這裡我們寫一個例子:

//視窗類
public class Window {
    public void show(View child) {
        child.draw();
    }
}

//建立檢視抽象,測量檢視的寬高為公用程式碼,繪製實現交由具體的子類
public abstract class View {
    public abstract void draw();

    public void measure(int width, int height) {
        //測量檢視的大小
    }
}

//按鈕類的具體實現
public class Button extends View {
    @Override
    public void draw() {
        //繪製按鈕
    }
}

//TextView 的具體實現
public class TextView extends View {
    @Override
    public void draw() {
        //繪製文字
    }
}

上述示例中,Window 依賴於 View,而 View 定義了一個檢視抽象,measure 是各個子類共享的方法,子類通過覆寫 View 的 draw 方法實現具體各自特色的功能,在這裡,這個功能就是繪製自身內容。任何繼承自 View 的子類都可以設定給 show 方法,就是所說的里氏替換。通過里氏替換,就可以自定義各式各樣、千變萬化的 View,然後傳遞給 Window,Window 負責組織 View,並且將 View 顯示到螢幕上。

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

  • (1)程式碼重用,減少建立類的成本,每個子類都擁有父類的方法和屬性;
  • (2)子類與父類基本相似,但又與父類有所區別;
  • (3)提高程式碼的可擴充套件性。

繼承的缺點:

  • (1)繼承是侵入性的,只要繼承就必須擁有父類的所有屬性和方法;
  • (2)可能造成子類程式碼冗餘、靈活性降低,因為子類必須用於父類的屬性和方法。

事物總是具有兩面性,如何權衡利與弊都是需要根據具體情況來做出選擇加以處理。里氏替換原則指導我們構建擴充套件性更好的軟體系統,我們還是接著上面的 ImageLoader 來做說明:

上面我們所寫的 ImageLoader 中的 LruImageCache、DiskCache 以及 ThreeLevelCache 都可以替換 ImageCache 的工作,並且能夠保證行為的正確性。ImageCache 建立了獲取快取圖片、儲存快取圖片的介面規範,LruImageCache 等根據介面規範實現了相應的功能,使用者只需要在使用時指定具體的快取物件就可以動態地替換 ImageLoader 中的快取策略。這就使得 ImageLoader 的快取系統具有了無限的可能性,也就是保證了可擴充套件性。
想象一種情況,當 ImageLoader 中的 setImageCache(ImageCache imageCache) 中的 imageCache物件不能夠被子類所替換,那麼使用者如何設定不同的快取物件,以及使用者如何自定義自己的快取實現。里氏替換原則就是為這類問題提供了指導原則,也就是建立抽象,通過抽象建立規範,具體的實現在執行時替換掉抽象,保證系統的擴充套件性、靈活性。開閉原則和里氏替換原則往往是生死相依、不離不棄的,通過里氏替換來達到對擴充套件開放,對修改關閉的效果。然而,這兩個原則都同時強調了一個 OOP 的重要特性——抽象,因此,在開發過程中運用抽象是走向程式碼優化的重要一步。

設計模式六大原則(4):依賴倒置原則

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

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

  • (1)高層模組不應該依賴低層模組,兩者都應該依賴其抽象;
  • (2)抽象不應該依賴細節;
  • (3)細節應該依賴抽象。

在 Java 語言中,抽象就是指介面或抽象類,兩者都是不能直接被例項化的;細節就是實現類,實現介面或繼承抽象類而產生的類就是細節,其特點就是,可以直接被例項化,也就是可以加上一個關鍵字 new 產生一個物件。高層模組就是呼叫端,低層模組就是具體實現類。依賴倒置原則在 Java 語言中的表現就是:模組間的依賴通過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是通過介面或者抽象類產生的。

*如果類與類直接依賴於細節,那麼它們之間就會有直接的耦合,當具體實現需要變化時,意味著要同時修改依賴者的程式碼,這限制了系統的可擴充套件性。比如下面的這麼一個例子:ImageLoader 直接依賴於 LruImageCache ,這個 LruImageCache 是一個具體實現,而不是一個抽象類或者介面。這導致了 ImageLoader 直接依賴了具體細節,當 LruImageCache 不能滿足 ImageLoader 而需要被其他快取實現替換時,此時就必須修改ImageLoader 的程式碼,程式碼如下所示:

public class ImageLoader {

    //記憶體快取(直接依賴於細節)
    LruImageCache mImageCache;

隨著產品的升級,使用者發現 LruImageCache 已經不能滿足需求,使用者需要我們的 ImageLoader 可以將圖片同時快取到記憶體和 SD 卡中,或者可以讓使用者自定義實現快取。此時,我們的 LruImageCache 這個類名不經不能夠表達記憶體快取和 SD 卡快取的意義,也不能夠滿足功能。另外使用者需要自定義快取實現時還必須繼承自 LruImageCache,而使用者的快取實現可不一定與記憶體快取有關,這在命名上的限制也讓使用者體驗不好。

但是當我們使用 開閉原則 對 ImageLoader 進行重構後,建立起來的 ImageCache 抽象中增加了 get 和 put 方法用以實現圖片的存取。每種快取實現都必須實現這個介面,並且實現自己的存取方法。當用戶需要使用不同的快取實現時,直接通過依賴注入即可,保證了系統的靈活性。我們再來簡單的回顧一下相關程式碼:

ImageCache 快取抽象:

public interface ImageCache {
    public void put(String url, Bitmap bitmap);

    public Bitmap get(String url);
}

ImageLoader 類:

public class ImageLoader {
    //圖片快取類,依賴於抽象,並且有一個預設的實現
    ImageCache mImageCache = null;

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

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        mImageCache = new LruImageCache();
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = DownLoadImage.downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }

    //設定快取策略,依賴於抽象
    public void setImageCache(ImageCache imageCache) {
        this.mImageCache = imageCache;
    }
}

在這裡,我們建立了 ImageCache 抽象,並且讓 ImageLoader 依賴於抽象而不是具體細節。當需求發生變化時,我們只需要實現 ImageCache 類或者繼承其他已有的 ImageCache 子類完成相應的快取功能,然後將具體的實現注入到 ImageLoader 即可實現快取功能的替換,這就保證了快取系統的高可擴充套件性,有了擁抱變化的能力,這就是依賴倒置原則。

設計模式六大原則(5):介面隔離原則

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

*介面隔離原則說白了就是,讓客戶端依賴的介面儘可能地小,這樣說可能還是有點抽象,我們還是以一個示例來說明一下。在此之前我們來說一個場景,在 Java 6 以及之前的 JDK 版本,有個非常討厭的問題,那就是使用了 OutputStream 或者其他可關閉的物件之後,我們必須保證它們最終被關閉了,我們的 SD卡 快取類中就有這樣的程式碼:

    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (fileOutputStream !=null){
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

我們看到的這段程式碼可讀性非常差,各種 try…catch 巢狀都是些簡單的程式碼,但是會嚴重影響程式碼的可讀性,並且多層級的大括號很容易將程式碼寫到錯誤的層級中。大家應該對這類程式碼也非常反感,那我們看看如何解決這類問題。
我們可能知道 Java 中有一個 Closeable 介面,該介面標識了一個可關閉的物件,它只有一個close 方法,我們來看一個這個類:

這裡寫圖片描述

我們要將的 FileOutputStream 類就實現了這個介面。我們從圖中可以看出,還有 100 多個類實現了 Closeable 這個介面,這意味著,在關閉這 100 多個型別的物件時,都需要寫出向 put 方法中 finally 程式碼段那樣的程式碼。這還了得!既然都是實現了 Closeable 介面,那麼我們能不能建立一個類專門用來管理 Stream 的 close 方法呢? 來試著寫一下:

public class CloseUtils {
    private CloseUtils() {
    }

    /**
     * 關閉 Closeable 物件
     */
    public static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

我們再看看吧這段程式碼運用到上述的 put 方法中的效果如何:

    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            CloseUtils.close(fileOutputStream);
        }
    }

程式碼簡潔了很多!而且這個 close 方法可以運用到各類可關閉的物件中,保證了程式碼的重要性。CloseUtils 的 close 方法的基本原理就是依賴於 Closeable 抽象而不是具體實現(這就是前面所講的依賴倒置原則),並且建立在最小化依賴原則的基礎上,它只需要知道這個物件是可關閉的,其他的一概不關心,也就是這裡的介面隔離原則。

試想一下,如果在只是需要關閉一個物件時,卻暴露出了其他的介面函式,如 OutputStream 的 write 方法,這就使得更多的細節暴露在客戶端程式碼面前,不僅沒有很好地隱藏實現,還增加了介面的使用難度。而通過 Closeable 介面可將關閉的物件抽象起來,這樣只需要客戶端依賴於 Closeable 就可以對客戶端隱藏其他的介面資訊,客戶端程式碼只需要知道這個物件可關閉(只可呼叫 close 方法)即可。咱們前面設計的 ImageLoader 中的 ImageCache 就是介面隔離原則的運用, ImageLoader 只需要知道該快取物件有存、取快取圖片的介面即可,其他的一概不管,這就使得快取功能的具體實現對 ImageLoader 隱藏。這就是用最小化介面隔離了實現類的細節,也促使我們將龐大的介面拆分到更細粒度的介面當中,這使得我們的系統具有更低的耦合性、更高的靈活性。

單一職責、開閉職責、里氏替換、介面隔離以及依賴倒置 5 個原則被定義為 SOLID 原則,作為面向物件程式設計的 5 個基本原則。當這些原則被一起應用時,它們使得一個軟體系統更清晰、簡單,最大程度地擁抱變化。

設計模式六大原則(6):迪米特原則

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

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

下面我們就以租房為例來講講迪米特原則的應用。

“北漂”的朋友比較瞭解,在北京租房絕大多數都是通過中介找房。我們設定的情況為:我只要求房間的面積和租金,其他的一概不管,中介將符合我要求的房子提供給我就可以。下面我們看看這個示例:

//房間
public class Room {
    public float area;
    public float price;

    public Room(float area, float price) {
        this.area = area;
        this.price = price;
    }
}

//中介
public class Mediator {
    List<Room> mRooms = new ArrayList<>();

    public Mediator() {
        for (int i = 0; i < 5; i++) {
            mRooms.add(new Room(14 + i, (14 + i) * 150));
        }
    }

    public List<Room> getAllRooms() {
        return mRooms;
    }
}

//租戶
public class Tenant {
    public float roomArea;
    public float roomPrice;
    public static final float diffPirce = 100.00f;
    public static final float diffArea = 0.0001f;

    public void rentRoom(Mediator mediator) {
        List<Room> rooms = mediator.getAllRooms();
        for (Room room : rooms) {
            if (isSuitable(room)) {
                Log.d("Tenant", "租到房間啦!" + room);
            }
        }
    }

    private boolean isSuitable(Room room) {
        return Math.abs(room.price - roomPrice) < diffPirce
                && Math.abs(room.area - roomArea) < diffArea;
    }
}

上面的程式碼中可以看到,Tenant 不僅依賴了 Mediator 類,還需要頻繁地與 Room 類打交道。“租戶類的要求僅僅是得到一間房子”罷了,我給你要一間房子,你只需要把適合我的房子給我就可以了,有那麼麻煩麼?如果把這些檢測條件都放在 Tenant 類中,那麼中介類的功能就會被弱化,而且導致 Tenant 與 Room 的耦合較高,當 Room 變化時 Tenant 也必須跟著變化。Tenant 又與 Mediator 耦合,這就出現了糾纏不清的關係。這個時候就需要我們分清楚誰才是我們真正的“朋友”,在我們所設定的情況下,顯然是 Mediator。上述程式碼的結構如下圖所示:

這裡寫圖片描述

既然耦合太嚴重,那我們就只能解耦了。首先要明確的是,我們只和我們的朋友通訊,這裡就是指 Mediator 物件(這裡我們找房子是通過中介,一般在現實生活中在未真正的確定要前租聘合同之前基本上是見不著房東的,都是在和中介打交道,通過中介進行溝通,所以這裡中介才是我們最直接的朋友)。必須將 Room 相關的操作從 Tenant 中移除,而這些操作案例應該屬於 Mediator。我們進行如下重構:

//中介
public class Mediator {
    List<Room> mRooms = new ArrayList<>();

    public Mediator() {
        for (int i = 0; i < 5; i++) {
            mRooms.add(new Room(14 + i, (14 + i) * 150));
        }
    }

    public List<Room> getAllRooms() {
        return mRooms;
    }

    public Room getRoom(float roomArea, float roomPrice) {
        if (mRooms != null) {
            for (Room room : mRooms) {
                if (isSuitable(room, roomArea, roomPrice)) {
                    Log.d("Tenant", "租到房間啦!" + room);
                    return room;
                }
            }
        }

        return null;
    }

    private boolean isSuitable(Room room, float roomArea, float roomPrice) {
        return Math.abs(room.price - roomPrice) < Tenant.diffPirce
                && Math.abs(room.area - roomArea) < Tenant.diffArea;
    }
}


//租戶
public class Tenant {
    public float roomArea;
    public float roomPrice;
    public static final float diffPirce = 100.00f;
    public static final float diffArea = 0.0001f;

    public void rentRoom(Mediator mediator) {
        Log.d("Tenant", "租到房間啦!" + mediator.getRoom(roomArea, roomPrice));
    }

}

這樣看上去是不是感覺好多了,我只需要房子麼?你只需要把適合我的房子給我就行了,給我那麼多東西幹嘛?你給我那麼多東西我還得自己處理,要不然我給你那麼多中介費幹嘛?是吧?重構後的結構體如下所示:

這裡寫圖片描述