面向對象六大原則----開閉原則

Java 中面向對象編程六大原則:

單一職責原則 英文名稱是Single Responsibility Principle,簡稱SRP

開閉原則 英文全稱是Open Close Principle,簡稱OCP

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

依賴倒置原則  英文全稱是Dependence Inversion Principle,簡稱DIP

接口隔離原則 英文全稱是InterfaceSegregation Principles,簡稱ISP

迪米特原則 英文全稱爲Law of Demeter,簡稱LOD,也稱爲最少知識原則(Least Knowledge Principle)


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

開閉原則的英文全稱是Open Close Principle,簡稱OCP,它是Java世界裏最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟件中的對象(類、模塊、函數等)應該對於擴展是開放的,但是,對於修改是封閉的。軟件開發過程中,最不會變化的就是變化本身。產品需要不斷地升級、維護,沒有一個產品從第一版本開發完就再沒有變化了,如果因爲變化、升級和維護等原因需要對軟件原有代碼進行修改時,這就可能會將錯誤引入原本已經經過測試的舊代碼中,破壞原有系統。因此,當軟件需要變化時,我們應該儘量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。那麼如何確保原有軟件模塊的正確性,以及儘量少地影響原有模塊,答案就是儘量遵守本章要講述的開閉原則

繼續上一章對ImageLoader進行分析,我們在第一輪重構之後的ImageLoader使用了單一原則,讓代碼職責單一、結構清晰,但是我們在使用中發現我們只實現了內存緩存圖片,通過內存緩存解決了每次從網絡加載圖片的問題,但是,Android應用的內存很有限,且具有易失性,即當app重新啓動之後,原來已經加載過的圖片將會丟失,這樣重啓之後就又需要重新下載。這又會導致加載緩慢、耗費用戶流量的問題。所以我們應該引入SD卡緩存,這樣下載過的圖片就會緩存到本地,即使重啓應用也不需要重新下載了。 

根據上一章的單一原則,我們寫一個DiskCache.java類,將圖片緩存到SD卡中:

public class DiskCache {
    private String mCacheDirName = "imageCache";
    private String mCacheDirPath;
    private File mCachDir = null;

    public DiskCache(Context context){
        initDiskCacheDir(context);
    }

    //初始化SD卡緩存的目錄位置
    public void initDiskCacheDir(Context context) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            //獲取外部存儲卡的位置,/sdcard/Android/data/xxxx(應用包名)/cache
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            //如果無法獲取外部存儲卡的位置,則使用應用自己的私用空間,/data/data/xxxx(應用包名)/cache
            cachePath = context.getCacheDir().getPath();
        }
        mCacheDirPath = cachePath+File.separator+mCacheDirName;
        mCachDir = new File(mCacheDirPath);
        if(!mCachDir.exists()){
            mCachDir.mkdirs();
        }
    }
    //將下載的圖片流緩存到sd卡里
    public void put(String key, InputStream inputStream){
        if(!mCachDir.exists()){
            return;
        }
        //對傳入的key值進行MD5處理
        String savedFilePath = mCacheDirPath+File.separator+hashKeyForDisk(key);
        try {
            FileOutputStream f = new FileOutputStream(new File(savedFilePath));
            BufferedInputStream in = new BufferedInputStream(inputStream);
            BufferedOutputStream out = new BufferedOutputStream(f);
            int n;
            byte[] buf = new byte[4096];
            while ((n = in.read(buf)) != -1) {
                out.write(buf,0,n);
            }
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //根據傳入的key值返回緩存的文件流
    public InputStream get(String key){
        if(!mCachDir.exists()){
            return null;
        }
        String savedFilePath = mCacheDirPath+File.separator+hashKeyForDisk(key);
        File bitmapFile = new File(savedFilePath);
        if(bitmapFile.exists()){
            try {
                InputStream in = new FileInputStream(bitmapFile);
                return in;
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    /**
     * 將Key用MD5碼編碼
     * @param key
     * @return 返回MD5碼後的編碼
     */
    public String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}

然後我們修改ImageLoader類加入我們的DiskCache:

public void setDiskCahe(Context context){
        mDiskCache = new DiskCache(context);
    }
再修改下載和顯示邏輯,加入判斷SD卡緩存裏有沒有緩存過要顯示的圖片,有緩存則直接取緩存顯示,避免再下載

public  void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        //先從cache中取圖片
        if(mMemImageCache.get(url)!=null){
            imageView.setImageBitmap(mMemImageCache.get(url));
            return;
        }
        if(mDiskCache != null){
            InputStream in = mDiskCache.get(url);
            if(in != null){
                Bitmap bitmap = BitmapFactory.decodeStream(in);
                imageView.setImageBitmap(bitmap);
                return;
            }
        }
        mExecutorService.submit(new Runnable() {
            @Override
            public  void run() {
                Bitmap bitmap = downloadImage(url);
                if (bitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    Message msg = mHandler.obtainMessage();
                    imageView.setTag(bitmap);
                    msg.obj = imageView;
                    mHandler.sendMessage(msg);
                }
                mMemImageCache.put(url, bitmap);
            }
        });
    }

    public  Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection conn = (HttpURLConnection)url.openConnection();
            conn.setDoInput(true); //允許輸入流,即允許下載
            conn.setUseCaches(false); //不使用緩衝
            conn.setRequestMethod("GET"); //使用get請求
            InputStream is = conn.getInputStream();   //獲取輸入流,此時才真正建立鏈接
            if(mDiskCache!=null){  //緩存到硬盤上
                mDiskCache.put(imageUrl,is);
            }
            bitmap = BitmapFactory.decodeStream(mDiskCache.get(imageUrl));
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }

通過上面的修改,現在我們只需在使用ImageLoader時,調用一下setDiskCahe(Context context)方法就能使用SD卡緩存圖片,這是不是很方便,但是我們在添加一個DiskCache緩存時修改了許多ImageLoader代碼,如果我們需要更多緩存方式或更多緩存策略,比如只使用內存緩存,或內存緩存和SD卡緩存同時使用等等需求,每次加新的緩存方法時都要修改原來的代碼,這樣很可能會引入Bug,而且會使原來的代碼邏輯變得越來越複雜,按照上面這樣的方法實現,用戶也不能自定義緩存實現。

軟件中的對象(類、模塊、函數等)應該對於擴展是開放的,但是對於修改是封閉的,這就是開放-關閉原則。也就是說,當軟件需要變化時,我們應該儘量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。如何實現ImageLoader類需要的cache能擴展呢?我們可以定義一個ImageCache接口:

public interface ImageCache {
    void put(String key, Bitmap bitmap);
    Bitmap get(String key);
}
然後所有的具體Cache類實現這個接口,而我們的ImageLoader類只需操作這個ImageCache類就可以了.這就實現了用抽象類或接口來實現原功能的擴展。下面看一下UML圖:


如上圖,所有具體的Cache類實現了ImageCahe這個接口,而ImageLoader類只依賴於ImageCache類,這樣就達到了可以不修改原有代碼,而可以無限通過實現ImageCache這個接口來擴展ImageLoader類的緩存圖片功能了。
於是ImageLoader類裏增加設置ImageCache:

  public void setImageCache(ImageCache cache){
        mImageCache = cache;
    }

通過setImageCache(ImageCache cache)方法注入不同的緩存實現,這樣不僅能夠使ImageLoader更簡單、健壯,也使得ImageLoader的可擴展性、靈活性更高。MemoryCache、DiskCache、DoubleCache緩存圖片的具體實現完全不一樣,但是,它們的一個特點是都實現了ImageCache接口。當用戶需要自定義實現緩存策略時,只需要新建一個實現ImageCache接口的類,然後構造該類的對象,並且通setImageCache(ImageCache cache)注入到ImageLoader中,這樣ImageLoader就實現了變化萬千的緩存策略,而擴展這些緩存策略並不會導致ImageLoader類的修改。這就是開閉原則!

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

代碼github地址:點擊打開鏈接

更多精彩Android技術可以關注我們的微信公衆號,掃一掃下方的二維碼或搜索關注公共號: Android老鳥

                                                





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