走向面向?qū)ο蟮牧笤瓌t-開(kāi)閉原則

寫(xiě)在前面#


我來(lái)晚了,遲到的祝福,祝各位同行們**中秋快樂(lè)! **
今天文章的主題是六大原則中的開(kāi)閉原則。

面向?qū)ο缶幊痰牧笤瓌t


讓程序像組裝機(jī)一樣穩(wěn)定靈活--開(kāi)閉原則

開(kāi)閉原則的全稱是Open Close Principle,簡(jiǎn)寫(xiě)是OCP,它是Java世界里組基礎(chǔ)的設(shè)計(jì)原則,它將會(huì)指導(dǎo)我們?nèi)绾谓⒁粋€(gè)穩(wěn)定并且靈活的系統(tǒng),讓我們的程序可以像組裝機(jī)一樣,可以更新,換配件,但不能修改配件。開(kāi)閉原則的定義是:軟件中的對(duì)象(類(lèi),模塊,函數(shù)等)對(duì)于拓展是開(kāi)放的,對(duì)于修改時(shí)封閉的。在我們開(kāi)發(fā)軟件的周期中,我們可能會(huì)因?yàn)橛脩粜枨蠡蛘呒軜?gòu)設(shè)計(jì)的變化而需要對(duì)原有代碼進(jìn)行大量修改,這時(shí)候很可能會(huì)將錯(cuò)誤引入原本已經(jīng)經(jīng)過(guò)測(cè)試的代碼之中,因而產(chǎn)生新的BUG影響開(kāi)發(fā)周期與開(kāi)發(fā)質(zhì)量,破壞原有的軟件設(shè)計(jì)和代碼完整性。此時(shí),我們應(yīng)該盡量通過(guò)拓展的方式去修改代碼實(shí)現(xiàn)我們要的功能,而不是在原有的基礎(chǔ)上去修改代碼。我們希望只通過(guò)繼承的方式去實(shí)現(xiàn)代碼的更新和修改,但這對(duì)于實(shí)際開(kāi)發(fā)而言其實(shí)只能是我們的一個(gè)愿景。因此,在開(kāi)發(fā)過(guò)程中,我們通常情況下是會(huì)通過(guò)修改原有代碼并且拓展代碼去實(shí)現(xiàn)更新和修改的功能。
那么,如何才能盡可能確保原有的軟件模塊完整性,正確性,并且盡量少地影響到原有代碼呢?答案就是:盡量遵守本章講述的開(kāi)閉原則。


開(kāi)始

上一章我們?cè)谥v述單一職責(zé)原則時(shí),通過(guò)ImageLoader的例子去體現(xiàn)了單一職責(zé)原則的好處,但是在代碼中我們只實(shí)現(xiàn)了內(nèi)存緩存,那么用戶在關(guān)閉APP后圖片依舊是需要重新加載的,出于對(duì)用戶的流量考慮,我們勢(shì)必要在代碼中進(jìn)行拓展,添加本地磁盤(pán)緩存功能,下面是新添加的DiskCache類(lèi)。

public class DiskCache {
    private final static String cacheDir = "sdcard/cacheTest";

    // 從磁盤(pán)獲取圖片
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }

    // 緩存圖片到內(nèi)存卡
    public void put(String url,Bitmap bitmap){
        // 創(chuàng)建輸出流
        FileOutputStream outputStream = null;
        try {
            // 輸出流命名
            outputStream = new FileOutputStream(url);
            // 將傳進(jìn)來(lái)的bitmap轉(zhuǎn)為文件
            // 參數(shù)一:轉(zhuǎn)換的圖片格式,默認(rèn)支持 JPEG,PNG,WEBP
            // 參數(shù)二:轉(zhuǎn)換圖片質(zhì)量,100為最高
            // 參數(shù)三:承接轉(zhuǎn)出文件的輸出流
            bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            // 進(jìn)行判斷
            if (outputStream!=null){
                try {
                    // 關(guān)閉輸出流
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

為了將我們的DiskCache也就是我們的磁盤(pán)緩存添加進(jìn)我們的ImageLoader,我對(duì)ImageLoader進(jìn)行了一定更新,不熟悉之前代碼的同學(xué)請(qǐng)?zhí)D(zhuǎn)到第一章單一職責(zé)原則,修改后代碼如下:

public class ImageLoader {
    // 圖片緩存
    ImageCache mImageCache = new ImageCache();
    //磁盤(pán)緩存
    DiskCache mDiskCache = new DiskCache();
    // 是否使用磁盤(pán)緩存
    boolean isUsedDisk = false;
    // 線程池,線程數(shù)為CPU所允許的數(shù)量
    ExecutorService mExecutorService =   Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public void displayImg(final String url, final ImageView imageView) {
        Bitmap bitmap = isUsedDisk ? mDiskCache.get(url) : mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        // View中的setTag(object)表示給View添加一個(gè)格外的數(shù)據(jù),以后可以用getTag()將這個(gè)數(shù)據(jù)取出來(lái)。
        imageView.setTag(url);
        //在子線程中完成加載圖片和緩存圖片
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImg(url);
                if (bitmap == null) return;
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(bitmap);
                }
                if (isUsedDisk) mDiskCache.put(url, bitmap);
                else mImageCache.put(url,bitmap);
            }
        });
    }

    private Bitmap downloadImg(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            connection.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    public void setDiskCacheEnable(boolean enable) {
        this.isUsedDisk = enable;
    }
}

從上述代碼中我們可以看到,添加了DiskCache后再修改了ImageLoader中的一丁點(diǎn)代碼即可達(dá)成磁盤(pán)緩存的功能,但是眼尖的同學(xué)會(huì)發(fā)現(xiàn),現(xiàn)在的ImageLoader有個(gè)非常大的弊端,就是使用內(nèi)存緩存時(shí)就無(wú)法使用磁盤(pán)緩存,使用磁盤(pán)緩存就無(wú)法使用內(nèi)存緩存。
我們所知的各種網(wǎng)絡(luò)圖片加載框架的正常工作流程應(yīng)該是:加載圖片時(shí)首先使用內(nèi)存緩存,如果內(nèi)存中沒(méi)有再去尋找SD卡中的緩存,如果SD卡中依然沒(méi)有,再?gòu)木W(wǎng)上獲取。這應(yīng)該是最好的緩存策略了,那么,我們的ImageLoader應(yīng)該怎么修改呢?我新建了一個(gè)類(lèi)DoubleCache,代碼如下:

public class DoubleCache {
    ImageCache mMemoryCache = new ImageCache();
    DiskCache mDiskCache = new DiskCache();

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

    public void save(String url, Bitmap bitmap) {
        mMemoryCache.put(url, bitmap);
        mDiskCache.put(url, bitmap);
    }
}

同時(shí)修改了ImageLoader中的部分內(nèi)容

public class ImageLoader {
    // 圖片緩存
    ImageCache mImageCache = new ImageCache();
    //磁盤(pán)緩存
    DiskCache mDiskCache = new DiskCache();
    //雙緩存
    DoubleCache mDoubleCache = new DoubleCache();
    // 是否使用磁盤(pán)緩存
    boolean isUsedDisk = false;
    // 是否使用雙緩存
    boolean isUsedDoubleCache = false;
    // 線程池,線程數(shù)為CPU所允許的數(shù)量
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public void displayImg(final String url, final ImageView imageView) {
        Bitmap bitmap = null;
        if (isUsedDoubleCache){
            bitmap = mDoubleCache.get(url);
        }else if (isUsedDisk){
            bitmap = mDiskCache.get(url);
        }else{
            bitmap = mImageCache.get(url);
        }
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        // 如若沒(méi)有則開(kāi)啟線程從網(wǎng)絡(luò)加載圖片
    }

    private Bitmap downloadImg(String imageUrl) {
        // 從網(wǎng)絡(luò)加載圖片
    }

    public void setDiskCacheEnable(boolean enable) {
        this.isUsedDisk = enable;
    }

    public void setmDoubleCache(DoubleCache enable) {
        this.mDoubleCache = enable;
    }
}

以上代碼實(shí)現(xiàn)了雙緩存功能了,但是從它修改了太多已有代碼,并且舊代碼與新代碼相互耦合,可讀性也很差,不管從維護(hù)角度或者說(shuō)安全性上來(lái)說(shuō),都是不適合的,而且這樣的圖片加載功能幾乎不提供給用戶任何自定義的空間,并且if判斷條件過(guò)多,一步錯(cuò)步步錯(cuò),那么,究竟要怎樣才能實(shí)現(xiàn)出我們需要的并且漂亮合格的代碼呢?這里的代碼進(jìn)行了大量修改。

  • 圖片緩存接口
public interface ImageCache {
    Bitmap get(String url);

    void put(String url,Bitmap bitmap);
}
  • MemoryCache 內(nèi)存緩存
public class MemoryCache implements ImageCache{
    // 圖片緩存
    private LruCache<String, Bitmap> mImageCache;

    public MemoryCache() {
        initImageCache();
    }

    private void initImageCache() {
        // 獲取APP可使用的最大內(nèi)存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 獲取四分之一大小作為緩存空間
        int cacheMemorySize = maxMemory / 4;
        mImageCache = new LruCache<String, Bitmap>(cacheMemorySize) {
            /**
             * Sizeof方法的作用只要是定義緩存中每項(xiàng)的大小,當(dāng)我們緩存進(jìn)去一個(gè)數(shù)據(jù)后,
             * 當(dāng)前已緩存的Size就會(huì)根據(jù)這個(gè)方法將當(dāng)前加進(jìn)來(lái)的數(shù)據(jù)也加上,便于統(tǒng)計(jì)當(dāng)
             * 前使用了多少內(nèi)存,如果已使用的大小超過(guò)maxSize就會(huì)進(jìn)行清除動(dòng)作;
             */
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

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

    @Override
    public Bitmap get(String url) {
        return mImageCache.get(url);
    }
}
  • DiskCache 磁盤(pán)緩存
public class DiskCache implements ImageCache{
    private final static String cacheDir = "sdcard/cacheTest";

    // 從磁盤(pán)獲取圖片
    @Override
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }

    // 緩存圖片到內(nèi)存卡
    @Override
    public void put(String url,Bitmap bitmap){
        // 創(chuàng)建輸出流
        FileOutputStream outputStream = null;
        try {
            // 輸出流命名
            outputStream = new FileOutputStream(url);
            // 將傳進(jìn)來(lái)的bitmap轉(zhuǎn)為文件
            // 參數(shù)一:轉(zhuǎn)換的圖片格式,默認(rèn)支持 JPEG,PNG,WEBP
            // 參數(shù)二:轉(zhuǎn)換圖片質(zhì)量,100為最高
            // 參數(shù)三:承接轉(zhuǎn)出文件的輸出流
            bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            // 進(jìn)行判斷
            if (outputStream!=null){
                try {
                    // 關(guān)閉輸出流
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • DoubleCache 雙緩存
public class DoubleCache implements ImageCache {
    MemoryCache mMemoryCache = new MemoryCache();
    DiskCache 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);
    }
}
  • ImageLoader 圖片加載類(lèi)
 public class ImageLoader {
    // 圖片緩存
    ImageCache mImageCache = new MemoryCache();
    // 線程池,線程數(shù)為CPU可用數(shù)
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    // 注入緩存實(shí)現(xiàn)
    public void setmImageCache(ImageCache cache){
        mImageCache = cache;
    }

    public void displayImage(String imageUrl,ImageView imageView){
        Bitmap bitmap = mImageCache.get(imageUrl);
        if (bitmap!=null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        //如果沒(méi)有緩存,開(kāi)始網(wǎng)絡(luò)獲取圖片的過(guò)程
        submitLoadRequest(imageUrl,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);
                }
                mImageCache.put(url,bitmap);
            }
        });
    }

    private Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
            conn.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

上述代碼中,我寫(xiě)了一個(gè)接口類(lèi)ImageCache去定義獲取和緩存兩個(gè)函數(shù),內(nèi)存緩存,磁盤(pán)緩存,雙緩存都實(shí)現(xiàn)了該接口。細(xì)心的朋友應(yīng)該可以看見(jiàn)在ImageLoader類(lèi)中多了一個(gè)方法 setmImageCache(ImageCache cache),通過(guò)此方法可以讓用戶選擇需要的緩存類(lèi)型甚至實(shí)現(xiàn)自定義的緩存,這樣就是通常說(shuō)的依賴注入。用戶可以通過(guò)如下代碼實(shí)現(xiàn)緩存設(shè)置:

//實(shí)例化
ImageLoader imageLoader = new ImageLoader();
//設(shè)置為磁盤(pán)緩存
imageLoader.setmImageCache(new DiskCache());
//設(shè)置為內(nèi)存緩存
imageLoader.setmImageCache(new MemoryCache());
//設(shè)置為雙緩存
imageLoader.setmImageCache(new DoubleCache());
//設(shè)置為自定義緩存
imageLoader.setmImageCache(new ImageCache() {
    @Override
    public Bitmap get(String url) {
        return null;
    }

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

    }
});

以上代碼就體現(xiàn)了本章的主題:開(kāi)閉原則。
開(kāi)閉原則指導(dǎo)我們,當(dāng)我們的軟件要發(fā)生變化時(shí),應(yīng)該盡量通過(guò)拓展的方式實(shí)現(xiàn)變化,而不是通過(guò)修改原有代碼的形式進(jìn)行。請(qǐng)注意此處加黑應(yīng)該盡量四字,在開(kāi)發(fā)過(guò)程中OCP原則并不能保證所有情況下都可以不修改原有代碼去實(shí)現(xiàn)修改。
而在開(kāi)發(fā)過(guò)程中,當(dāng)我們發(fā)現(xiàn)我們的代碼慢慢地耦合家中或者看起來(lái)雜亂無(wú)章,慢慢開(kāi)始變成臭不可聞的代碼時(shí),就應(yīng)該及時(shí)重構(gòu)代碼,以便我們的代碼能夠滿足軟件或者系統(tǒng)的迭代更新,而不是通過(guò)繼承的方式去無(wú)限制的添加新代碼,這樣會(huì)導(dǎo)致類(lèi)不斷變大并造成代碼冗余。
在實(shí)際開(kāi)發(fā)中需要自己結(jié)合實(shí)際情況進(jìn)行決策,保證代碼的穩(wěn)定性和靈活性,同時(shí)在保證我們的代碼是清新迷人的同時(shí),也要保證代碼的正確性。


寫(xiě)在結(jié)尾#

  • 本章內(nèi)容示例代碼引用自《安卓源碼設(shè)計(jì)模式》(何紅輝,關(guān)愛(ài)名著)。
  • 走向面向?qū)ο?六大原則將會(huì)一直使用ImageLoader進(jìn)行講解,不斷通過(guò)犯錯(cuò)和改錯(cuò)的過(guò)程中完善代碼。
  • 作者很喜歡和大家一起討論代碼,討論各種新的框架和設(shè)計(jì)模式,希望大家踴躍留言相互交流,共同進(jìn)步。
  • 謝謝。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 本文出自《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》中的第一章。 1、優(yōu)化代碼的第一步——單一職責(zé)原則 單一職責(zé)原則的...
    MrSimp1e0閱讀 1,914評(píng)論 1 13
  • 最新在閱讀《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》一書(shū),我覺(jué)得寫(xiě)的很清晰,每一個(gè)知識(shí)點(diǎn)都有示例,通過(guò)示例更加容易理...
    慕涵盛華閱讀 2,460評(píng)論 0 3
  • 開(kāi)閉原則的英文全稱是Open Close Principle,縮寫(xiě)是OCP,它是Java世界里最基礎(chǔ)的設(shè)計(jì)原則,它...
    劉滌生閱讀 1,402評(píng)論 0 3
  • 前言最近同事買(mǎi)了本Android設(shè)計(jì)模式的書(shū),借來(lái)看看,感覺(jué)還不錯(cuò),做一下筆記唄。有興趣的同學(xué)可以買(mǎi)原書(shū)看看:《A...
    肖丹晨閱讀 1,328評(píng)論 0 6
  • 寫(xiě)在前言:久了沒(méi)寫(xiě)文章,就會(huì)變得越來(lái)越懶,還會(huì)給自己找各種理由,說(shuō)自己忙,沒(méi)時(shí)間寫(xiě)文章.其實(shí)最近由于受一個(gè)很熱門(mén)的...
    Simple_Dev閱讀 489評(píng)論 0 4

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