面向?qū)ο蟮脑瓌t之開閉原則

開閉原則的英文全稱是Open Close Principle,縮寫是OCP,它是Java世界里最基礎(chǔ)的設計原則,它指導我們?nèi)绾谓⒁粋€穩(wěn)定的、靈活的系統(tǒng)。開閉原則的定義是:軟件中的對象(類、模塊、函數(shù)等)應該對于擴展是開放的,但是對于修改是封閉的。

勃蘭特-梅耶在1988年出版的《面向?qū)ο筌浖?gòu)造》一書中提出這一原則---開閉原則。這一想法認為,程序一旦開發(fā)完成,程序中一個類的實現(xiàn)只應該因錯誤而被修改,新的或者改變的特性應該通過新建不同的類實現(xiàn),新建的類可以通過繼承的方式來重用原類的代碼。顯然梅耶的定義提倡實現(xiàn)繼承,已存在的實現(xiàn)類對于修改時封閉的,但是新的類可以通過覆寫父類的接口應對變化。

引入其他緩存方式

說了這么多,下面還是以一個簡單示例來說明開閉原則:

在對ImageLoader進行了一次重構(gòu)之后的ImageLoader職責單一、結(jié)構(gòu)清晰,算是個不錯的開始。隨著用戶的增多,有些問題開始暴露出來:緩存系統(tǒng)是最讓大家吐槽的地方,通過內(nèi)存緩存解決了每次從網(wǎng)絡加載圖片的問題,但是,Android應用的內(nèi)存很有限,且具有易失性,即當應用重新啟動之后,原來已經(jīng)加載過的圖片將會丟失,這樣重啟之后及需要重新下載圖片!而這又會導致加載緩慢、耗費用戶流量的問題。針對以上問題考慮引入SD卡緩存,這樣下載過的圖片就會緩存到本地,即使重啟應用也不需要重新下載了,接下來開始實現(xiàn)SD卡緩存。

DiskCache.java類,將圖片緩存到SD卡中:

package com.deason.library.ocp;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import java.io.FileOutputStream;
import java.io.IOException;

/**
 * SD卡圖片緩存
 * Created by liuguoquan on 2016/3/17.
 */
public class DiskCache {

    public static final String CACHE_DIR = "/sdcard/cache/";

    /**
     * 從緩存中獲取圖片
     * @param url
     * @return
     */
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(CACHE_DIR + url);
    }

    /**
     * 將圖片存入SD卡中
     * @param url
     * @param bitmap
     */
    public void put(String url,Bitmap bitmap) {

        FileOutputStream out = null;
        try {
            out = new FileOutputStream(CACHE_DIR + url);
            bitmap.compress(Bitmap.CompressFormat.PNG,100,out);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

MemoryCache類,把圖片緩存到內(nèi)存中

package com.deason.library.ocp;

import android.graphics.Bitmap;
import android.util.LruCache;

/**
 * 處理圖片緩存
 * Created by liuguoquan on 2016/3/14.
 */
public class MemoryCache {

    /**
     * 圖片緩存
     */
    LruCache<String, Bitmap> mImageCache;
    public MemoryCache() {
        initImageCache();
    }

    private void initImageCache() {

        //計算可使用的最大內(nèi)存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        //取內(nèi)存的四分之一作為緩存
        final int cacheSize = maxMemory / 4;

        mImageCache = new LruCache<String, Bitmap>(cacheSize) {

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

    /**
     * 將圖片存入緩存
     * @param key
     * @param bitmap
     */
    public void put(String key,Bitmap bitmap) {
        mImageCache.put(key,bitmap);
    }

    /**
     * 取出緩存圖片
     * @param key
     * @return
     */
    public Bitmap get(String key) {
        return mImageCache.get(key);
    }
}

因為需要將圖片緩存到SD卡中,所有ImageLoader代碼有所更新,具體代碼如下:

/**
 * 圖片加載類
 * Created by liuguoquan on 2016/3/14.
 */
public class ImageLoader {

    /**
     * 圖片緩存
     */
    MemoryCache mMemoryCache = new MemoryCache();

    /**
     * SD卡緩存
     */
    DiskCache mDiskCache = new DiskCache();

    /**
     * 是否使用SD卡緩存
     */
    private boolean isUseDiskCache = false;

    /**
     * 線程池,線程數(shù)量未CPU的數(shù)量
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());

    /**
     * 顯示圖片
     * @param url
     * @param imageView
     */
    public void displayImage(final String url, final ImageView imageView) {
        //判斷使用哪種緩存
        Bitmap bitmap =  isUseDiskCache ? mDiskCache.get(url) :  mMemoryCache.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)) {
                    imageView.setImageBitmap(bitmap);
                }
                //將下載的圖片存入內(nèi)存
                mMemoryCache.put(url,bitmap);
                mDiskCache.put(url,bitmap);
            }
        });
    }

    /**
     * 設置是否用SD卡緩存
     * @param isUseDiskCache
     */
    public void useDiskCache(boolean isUseDiskCache) {
        this.isUseDiskCache = isUseDiskCache;
    }

    /**
     * 下載圖片
     * @param url
     * @return
     */
    public Bitmap downloadImage(String imageUrl) {

        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
            int code = mConnection.getResponseCode();

            if (200 == code) {
                bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
            }
            mConnection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

從上述的代碼中可以看到,僅僅新增了一個DiskCache類和往ImageLoader類中加入了少量代碼就添加了SD卡緩存的功能,用戶可以通過useDiskCache方法來對使用哪種緩存進行設置,例如

ImageLoader mImageLoader = new ImageLoader();
//使用SD卡緩存
mImageLoader.useDiskCache(true);
//使用內(nèi)存緩存
mImageLoader.useDisCache(false);

通過useDiskCache方法來讓用戶設置不同的緩存這個思路是對的,但是會有些明顯的問題,就是使用內(nèi)存緩存時用戶不能使用SD卡緩存。類似地,使用SD卡緩存時用戶就不能使用內(nèi)存緩存。

實際上,用戶需要這兩種緩存的綜合,首先緩存優(yōu)先使用內(nèi)存緩存,如果內(nèi)存緩存沒有圖片再使用SD卡緩存,如果SD卡中也沒有圖片最后才從網(wǎng)絡上獲取,這才是最好的緩存策略。

接下來,我們繼續(xù)重構(gòu),新建一個雙緩沖類DoubleCache,具體代碼如下:

package com.deason.library.ocp;


import android.graphics.Bitmap;

/**
 * 雙緩沖。獲取圖片時先從內(nèi)存中獲取,如果內(nèi)存中沒有緩存該圖片,再從SD卡中獲取
 * Created by liuguoquan on 2016/3/17.
 */
public class DoubleCache {

    MemoryCache mMemoryCache = new MemoryCache();
    DiskCache mDiskCache = new DiskCache();

    /**
     * 先從內(nèi)存獲取圖片,沒有再從SD卡獲取
     * @param url
     * @return
     */
    public Bitmap get(String url) {

        Bitmap bitmap = mMemoryCache.get(url);

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

        return bitmap;
    }

    /**
     * 將圖片緩存到內(nèi)存和SD卡中
     * @param url
     * @param bitmap
     */
    public void put(String url, Bitmap bitmap) {
        mMemoryCache.put(url, bitmap);
        mDiskCache.put(url, bitmap);
    }
}

我們再來看看最新的ImageLoader類,代碼更新也不多:

/**
 * 圖片加載類
 * Created by liuguoquan on 2016/3/14.
 */
public class ImageLoader {

    /**
     * 圖片緩存
     */
    MemoryCache mMemoryCache = new MemoryCache();

    /**
     * SD卡緩存
     */
    DiskCache mDiskCache = new DiskCache();
    //雙緩存
    DoubleCache mDoubleCache = new DoubleCache();

    /**
     * 是否使用SD卡緩存
     */
    private boolean isUseDiskCache = false;
    //使用雙緩存
    private boolean isUseDoubleCache = false;

    /**
     * 線程池,線程數(shù)量未CPU的數(shù)量
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());

    /**
     * 顯示圖片
     * @param url
     * @param imageView
     */
    public void displayImage(final String url, final ImageView imageView) {
        //判斷使用哪種緩存
        Bitmap bitmap =  null;
        if (isUseDoubleCache) {
            bitmap = mDoubleCache.get(url);
        } else if (isUseDiskCache) {
            bitmap = mDiskCache.get(url);
        } else {
            bitmap = mMemoryCache.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)) {
                    imageView.setImageBitmap(bitmap);
                }
                //將下載的圖片存入內(nèi)存
                mMemoryCache.put(url,bitmap);
                mDiskCache.put(url,bitmap);
            }
        });
    }

    /**
     * 設置是否用SD卡緩存
     * @param isUseDiskCache
     */
    public void useDiskCache(boolean isUseDiskCache) {

        this.isUseDiskCache = isUseDiskCache;
    }

    /**
     * 設置是否使用雙緩存
     * @param isUseDoubleCache
     */
    public void useDoubleCache(boolean isUseDoubleCache) {

        this.isUseDoubleCache = isUseDoubleCache;
    }

    /**
     * 下載圖片
     * @param url
     * @return
     */
    public Bitmap downloadImage(String imageUrl) {

        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
            int code = mConnection.getResponseCode();

            if (200 == code) {
                bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
            }
            mConnection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}


此時程序越來越靈活了,通過增加短短幾行代碼就能夠完成如此重要的功能。

人們總會把美好的事情講在前面,最后給你來個“但是”,我們先來分析一下上面的程序:每次在程序中加入新的緩存實現(xiàn)都需要修改ImageLoader類,然后通過一個布爾類型的變量來讓用戶選擇使用哪種緩存,因此,就使得ImageLoader中存在各種if-else判斷語句,通過這些判斷語句來確定使用哪種緩存。隨著大量邏輯的引用,代碼變得越來越復雜脆弱,如果一不小心寫錯某個if條件,勢必會花費大量的時間去調(diào)試程序,整個ImageLoader類也會顯得臃腫。最重要的是,用戶不能自己實現(xiàn)緩存注入到ImageLoader類中,可擴展性較差,而可擴展性是框架的最重要的特性之一。

注入方式設置緩存方式

“軟件中的對象(類、模塊、函數(shù)等)應該對于擴展是開放的,但是對于修改則是封閉的,這就是開放-關(guān)閉原則。也就是說,我們應該盡量通過擴展的方式來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)”。現(xiàn)在,我們來實現(xiàn)終極解決方案,遵循開閉原則。

首先,我們先畫出UML圖,如下

開閉原則.png

然后,實現(xiàn)UML圖上的類和接口,先重構(gòu)ImageLoader類:

/**
 * Created by liuguoquan on 2016/3/17.
 */
public interface ImageCache {

    /**
     * 獲取圖片
     * @param url
     * @return
     */
    public Bitmap get(String url);

    /**
     * 緩存圖片
     * @param url
     * @param bitmap
     */
    public void put(String url, Bitmap bitmap);
}

經(jīng)過重構(gòu)后,少了if-else語句,沒有了各種各樣的緩存實現(xiàn)對象、布爾變量,代碼確實清晰、簡單了很多。需要注意的是,這次重構(gòu)的ImageCache把它提取成一個圖片緩存的接口,用來抽象圖片緩存的功能,我們來看看該接口的聲明:

/**
 * 圖片加載類
 * Created by liuguoquan on 2016/3/14.
 */
public class ImageLoader {

    /**
     * 圖片緩存
     */
    ImageCache mImageCache = new MemoryCache();

    /**
     * 注入緩存實現(xiàn)
     * @param cache
     */
    public void setIamgeCache(ImageCache cache) {
        mImageCache = cache;
    }

    /**
     * 線程池,線程數(shù)量未CPU的數(shù)量
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());

    /**
     * 顯示圖片
     * @param url
     * @param imageView
     */
    public void displayImage(final String url, final ImageView imageView) {
        Bitmap bitmap =  mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return ;
        }

        //沒有緩存則提交線程池下載
        submitLoadRequest(url, imageView);

    }

    private void submitLoadRequest(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)) {
                    imageView.setImageBitmap(bitmap);
                }
                //將下載的圖片存入內(nèi)存
                mImageCache.put(url,bitmap);
            }
        });
    }

    /**
     * 下載圖片
     * @param url
     * @return
     */
    public Bitmap downloadImage(String imageUrl) {

        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
            int code = mConnection.getResponseCode();

            if (200 == code) {
                bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
            }
            mConnection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

ImageCache接口簡單定義了獲取、緩存圖片兩個方法,緩存的可以是圖片的url,值是圖片的本身。內(nèi)存緩存、SD卡緩存、爽緩存都實現(xiàn)該接口,我們看看幾個緩存的實現(xiàn):

MemoryCache.java

public class MemoryCache implements ImageCache {

    /**
     * 圖片緩存
     */
    LruCache<String, Bitmap> mMemoryCache;
    public MemoryCache() {
        initImageCache();
    }

    /**
     * 初始化LRU緩存
     */
    private void initImageCache() {

        //計算可使用的最大內(nèi)存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        //取內(nèi)存的四分之一作為緩存
        final int cacheSize = maxMemory / 4;

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
    }
    @Override
    public Bitmap get(String url) {
        return mMemoryCache.get(url);
    }

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

        mMemoryCache.put(url, bitmap);
    }
}

DiskCache.java

/**
 * Created by liuguoquan on 2016/3/17.
 */
public class DiskCache implements  ImageCache {

    public static final String CACHE_DIR = "/sdcard/cache/";
    @Override
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(CACHE_DIR + url);
    }

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

        FileOutputStream out = null;

        try {
            out = new FileOutputStream(CACHE_DIR + url);
            bitmap.compress(Bitmap.CompressFormat.PNG,100,out);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Double.java

/**
 * Created by liuguoquan on 2016/3/17.
 */
public class DoubleCache implements ImageCache {

    ImageCache mMemoryCache = new MemoryCache();
    ImageCache mDiskCache = new DiskCache();

    @Override
    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryCache.get(url);

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

        return bitmap;
    }

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

        mMemoryCache.put(url, bitmap);
        mDiskCache.put(url, bitmap);
    }
}

上述重構(gòu)后的ImageLoader類中增加了一個setImageCache(ImageCache cache)方法,用戶可以通過該函數(shù)設置緩存實現(xiàn),也就是通常所說的依賴注入。

下面看看用戶如何通過使用ImageLoader來設置緩存實現(xiàn)的:

ImageLoader mImageLoader = new ImageLoader();

//使用內(nèi)存緩存
mImageLoader.setIamgeCache(new MemoryCache());

//使用SD卡緩存
mImageLoader.setIamgeCache(new DiskCache());

//使用雙緩存
mImageLoader.setIamgeCache(new DoubleCache());

//使用自定義的圖片緩存實現(xiàn)
mImageLoader.setIamgeCache(new ImageCache() {
    @Override
    public Bitmap get(String url) {
        return null;
    }

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

    }
});

在上述代碼中,通過setImageCache(ImageCache cache)方法注入不同的緩存實現(xiàn),這樣不僅能夠使ImageLoader更簡單、健壯,也使得ImageLoader可課擴展性、靈活性更高。

MemeryCache、DiskCache、DoubleCache緩存圖片的具體實現(xiàn)完全不一樣,但是,它們有一個特點是,都實現(xiàn)了ImageCache接口。當用戶需要自定義緩存實現(xiàn)時,只需要新建一個實現(xiàn)ImageCache接口的類,然后構(gòu)造該類的對象,并且通過setImageCache(ImageCache cache)注入到ImageLoader中就可以了,這樣ImageLoader就實現(xiàn)了千變?nèi)f化的緩存策略,且擴展這些緩存策略并不會導致ImageLoader類的修改。經(jīng)過這次重構(gòu),ImageLoader在設計模式上基本已經(jīng)合格了。

開閉原則指導我們,當軟件需要變化時,應該盡量通過擴展的方式來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)。這里的“應該盡量”4個字說明OCP原則并不是說絕對不可以修改原始類的。當確定原來的代碼已經(jīng)“腐化”時,應該盡早的重構(gòu),以便讓代碼恢復帶正常的狀態(tài),而不是通過繼承等方式添加新的代碼。因此,在開發(fā)過程中需要自己結(jié)合具體情況進行考量,是通過修改舊代碼還是通過繼承使得軟件系統(tǒng)更穩(wěn)當、更靈活,在保證去除“腐化代碼”的同時,也保證原有模塊的正確性。

優(yōu)點

  • 增加穩(wěn)定性
  • 可擴展性提高
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容