讀書筆記-面向對象的六大原則(一)

單一職責原則

  • 讀《Android源碼設計模式》
  • 單一職責的定義為:就一個類而言,應該僅有一個引起它變化的原因,簡單來說,一個類中應該是一組相關性很高的函數,數據的封裝
  • 我們從最入門的方式入手

入手

  • 假設現在要實現圖片加載的功能,并且能將圖片緩存,我們可能寫出的代碼是這樣的

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)){
                    updataImageView(imageView,bitmap);
                }
                mImageCache.put(url,bitmap);
            }
        });
    }


    private void updataImageView(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 (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}
  • 代碼很簡單,可是大概分析一下不難看出,我們的所有功能都聚合在一個類里面,當我們的需求增多的時候,所有的代碼都擠在一個類里面,這將給我們的維護帶來了很大的麻煩
  • 那么怎么解決呢?

改進

  • 我們提出的方法是:將ImageLoader類拆分一下,把各個功能獨立出來
  • 各個功能獨立?我們原本的這個ImageLoader類有什么功能?圖片加載和圖片緩存?那好吧,就把圖片緩存提出來吧?我們單獨寫一個圖片緩存的類
public class ImageCache {
    //圖片緩存
    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);
    }
}
  • 可以看到,我們只是將緩存類的put和get方法抽出去而已,
  • 然后看一下我們的圖片加載類怎么改的
public class ImageLoader {

    //圖片緩存
    ImageCache mImageCache = new ImageCache();
    //線程池,線程數量為CPU的數量
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    //UI Handler
    Handler mUIHandler = new Handler(Looper.getMainLooper());

    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)){
                    updataImageView(imageView,bitmap);
                }
                mImageCache.put(url,bitmap);
            }
        });
    }


    private void updataImageView(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;
    }
}

  • 具體也沒什么大的改動,就是用到了緩存類的對象去調用相關方法
  • 這樣拆分之后,每個類的功能很明確,且代碼量也得到了減少,雖然可擴展性還是沒那么好,但是最起碼思路,代碼結構變得清晰許多

總結

  • 其實上面的改進思想就是單一職責的思想:根據不同的功能,合理的劃分一個類,或者一個函數的職責,關于這個劃分倒是沒有一個特別強制的概念,每個人都對功能的劃分有自己的理解,具體項目中的代碼就需要根據個人經驗與具體邏輯而定,

開閉原則

  • 開閉原則的定義:軟件中的對象(類,模塊,函數等)應該對于擴展是開放的,但是對于修改是封閉的,在軟件的生命周期內,因為變化,升級和維護等原因需要對軟件原有代碼進行修改時,可能會將錯誤引入原本經過測試的舊代碼中,破壞原有系統,因此,當軟件需要變化時,我們應該盡量通過擴展的方式來實現變化,而不是通過破壞已有的代碼來實現
  • 當然,一定的不改變原有代碼是不現實的,不過,我們在開發過程中,應盡量遵循這個開閉原則

入門

  • 還是之前的那個例子,通過使用不難發現,我們雖然寫的這個類具有緩存圖片的功能,但是當程序重啟的時候我們之前的緩存都會丟掉,因為我們的緩存全都是簡單的緩存在運行內存中,這樣不就會影響Android系統的性能,(因為Android手機的運行內存始終有限,我們無法讓一個App占用手機太多運行內存),具有易失性,重啟程序的時候又會重新下載,浪費用戶流量,基于此,我們打算將緩存做成緩存在SD卡當中
  • 先寫緩存到SD卡中的類
public class DiskCache {
    private static final String TAG = "DiskCache";

    static String cacheDir = null;

    public DiskCache() {
        cacheDir = getSDPath() + "/sadsaf";
    }

    public String getSDPath(){
        File sdDir = null;
        boolean sdCardExist = Environment.getExternalStorageState()
                .equals(android.os.Environment.MEDIA_MOUNTED);//判斷sd卡是否存在
        if(sdCardExist) {
            //這里得到的是手機內置存儲空間的根目錄
            sdDir = Environment.getExternalStorageDirectory();
            Log.d(TAG, "getSDPath: " + sdDir.toString());
        }else {
            //而這個得到的是手機外部SD卡的根目錄,但是一般Android 是不允許我們對此目錄下文件進行讀寫操作
            sdDir = Environment.getDataDirectory();
            Log.d(TAG, "getSDPath: " + sdDir.toString());
        }
        return sdDir.toString();
    }

    public Bitmap get(String url){
        Log.d(TAG, "get: 在這里" + url);
        String fileName = creatFileName(url);
        return BitmapFactory.decodeFile(cacheDir + fileName);
    }

    public void put(String url, Bitmap bmp){
        FileOutputStream fileOutputStream = null;
        try{

            File file = new File(cacheDir);
            if(!file.exists()){
                Log.d(TAG, "put: 文件夾不存在,先創建出文件夾");
                if(!file.mkdirs()){
                    Log.d(TAG, "put: 文件夾創建失敗");
                }
                if (file.exists()){
                    Log.d(TAG, "put: 文件夾已經存在");
                }
            }
            String s = cacheDir + creatFileName(url);
            Log.d(TAG, "put: 準備打開文件流 " + s);
            fileOutputStream = new FileOutputStream(s);
            bmp.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (fileOutputStream != null){
                try{
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //因為我們直接獲取網絡圖片的話,圖片的地址會含有斜杠,而這個斜杠在寫文件的時候會被當成文件夾目錄,故此會出錯,所以這里我將
    //文件的url處理,讓他的名字取網絡圖片url的最后一個斜杠之后的東西
    private String creatFileName(String url){
        StringBuilder builder = new StringBuilder(url);
        String s;
        if(url.contains("/")){
            int i = builder.lastIndexOf("/");
            s = builder.substring(i, builder.length());
        }else {
            s = builder.toString();
        }
        return s;
    }
  • 那么接下來改一下我們ImageLoader,讓他具有設置SD卡緩存的能力
  • :這里的SD卡寫入問題,以及權限問題,就不在這里細說了,如果在這里有問題的話,自行百度
  • 看ImageLoader改動的內容
//圖片 內存 緩存
    ImageCache mImageCache = new ImageCache();
    //圖片SD卡或手機內存緩存
    DiskCache mDiskCache = new DiskCache();

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


public void displayImage(final String url, final ImageView imageView){
        //判斷使用的是哪種緩存,并將緩存中的東西取出來(如果有的話)
        Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) : mImageCache.get(url);
        if(bitmap != null){
            Log.d(TAG, "displayImage: 獲取到緩存");
            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)){
                    updataImageView(imageView,bitmap);
                }
                if(isUseDiskCache){
                    mDiskCache.put(url,bitmap);
                }else {
                    mImageCache.put(url,bitmap);
                }
            }
        });
    }


//是否使用SD卡緩存
    public void useDiskCache(boolean useDiskCache){
        isUseDiskCache = useDiskCache;
    }
  • 這里我們在Activity里面設置使用SD卡緩存就ok啦
        String url = "http://img2.imgtn.bdimg.com/it/u=4060216298,1329589408&fm=27&gp=0.jpg";
        mImageView = findViewById(R.id.main_IV);
        ImageLoader loader = new ImageLoader();
        loader.useDiskCache(true);//設置用SD卡緩存
        loader.displayImage(url,mImageView);
  • 寫成這樣,我們就可以很方便的選擇緩存方式,非常方便
  • 但是不知道大家思考過沒?如果我想兩種緩存都使用呢?
  • 如果這樣的話,用目前的代碼去達到這種要求是不是太過復雜?那怎么辦?
  • 我們可以提供這樣一個思路,當要獲取圖片的時候,我們先看看內存緩存里面有沒有,如果沒有,再看看SD卡緩存里面有沒有,如果都沒有,再去網絡上獲取是不是更加人性化一些呢?

繼續探索

  • 這里有兩種方案,一種是我們直接在原來代碼上面改,一種是創建一個新的可以實現同時兩種緩存都支持的類
  • 那么想想看?我們剛才說的開閉原則?好吧,直接選擇第二種方案
  • 來看看我們這個雙緩存類(DoubleCache)的實現

public class DoubleCache {

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

    //先從內存緩存中獲取,如果沒有,再從SD中獲取
    public Bitmap get(String url){
        Bitmap bitmap = mMemoryCache.get(url);
        if(bitmap == null){
            bitmap = mDiskCache.get(url);
        }
        return bitmap;
    }

    //把圖片緩存到內存和SD中
    public void put(String url,Bitmap bitmap){
        mMemoryCache.put(url,bitmap);
        mDiskCache.put(url,bitmap);
    }

}
  • 代碼沒有任何難度,當提供了雙緩存機制之后,我們就可以去修改下我們的加載類了(ImageLoader)
//雙緩存
    DoubleCache mDoubleCache = new DoubleCache();
    //是否使用雙緩存
    boolean isUseDoubleCache = false;

public void displayImage(final String url, final ImageView imageView){
        //判斷使用的是哪種緩存,并將緩存中的東西取出來(如果有的話)
        Bitmap bitmap;
        if(isUseDoubleCache){
            bitmap = mDoubleCache.get(url);
        }else if(isUseDiskCache){
            bitmap = mDiskCache.get(url);
        }else {
            bitmap = mImageCache.get(url);
        }


        if(bitmap != null){
            Log.d(TAG, "displayImage: 獲取到緩存");
            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)){
                    updataImageView(imageView,bitmap);
                }
                if(isUseDiskCache){
                    mDiskCache.put(url,bitmap);
                }else {
                    mImageCache.put(url,bitmap);
                }
            }
        });
    }


//是否使用雙緩存
    public void UseDoubleCache(boolean useDoubleCache) {
        isUseDoubleCache = useDoubleCache;
    }

  • 貌似?蠻好的?好像是符合開閉原則來著?
  • 來,回過頭想一下,我們剛才為了添加雙緩存機制,修改了幾個類的代碼?好像基本上都修改了吧
  • 問題:每次加入新的緩存方法都要修改原來的代碼,這樣可能引來新的bug,而且,照這樣的實現方法,用戶是不能自己實現自定義緩存實現的
  • 這里基于這個問題,我們再來看一下開閉原則的定義:軟件中的對象(類,模塊,函數等)應該對于擴展是開放的,但是對于修改是封閉的,也就是說,當軟件需要變化時,我們應該盡量通過擴展的方式來實現變化,而不是通修改已有的代碼來實現
  • 如果要實現用戶自定義緩存機制實現的話,我們是不是應該抽出一個緩存接口?
public interface IImageCache {
    public Bitmap get(String url);
    public void put(String url,Bitmap bitmap);
}
  • 然后讓我們之前寫的三個緩存類實現這個接口,這里就不貼代碼,接下來我們去看看圖片加載類怎么做的(ImageLoader)
public class ImageLoader {

    private final static String TAG = "ImageLoader";
    
    //默認為內存緩存
    IImageCache mImageCache =  new MemeryCache();
    
    //線程池,線程數量為CPU的數量
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    //UI Handler
    Handler mUIHandler = new Handler(Looper.getMainLooper());
    
    //外部注入緩存
    public void setImageCache(IImageCache mImageCache){
        this.mImageCache = mImageCache;
    }
    
    

    public void displayImage(final String url, final ImageView imageView){
        //直接來獲取緩存
        Bitmap bitmap = mImageCache .get(url);
        
        if(bitmap != null){
            Log.d(TAG, "displayImage: 獲取到緩存");
            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){
                    Log.d(TAG, "run: 網絡圖片下載失敗");
                    return;
                }
                if (imageView.getTag().equals(url)){
                    updataImageView(imageView,bitmap);
                }
                //設置緩存
                mImageCache.put(url,bitmap);
            }
        });
        
    }


    //更新UI
    private void updataImageView(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;
    }
}

  • 從這個ImageLoader里面我們可以看出來,這個類已經相當成熟了,里面的東西我們基本不需要再去改變,想用哪種緩存,哪怕是我們自己的緩存方式,只要我們實現了那個接口,然后調用set方法將我們的緩存注入進去即可,
  • 現在想想看?如果我們現在需要使用一種新的緩存方式該怎么做呢?只需實現我們自己的緩存邏輯,然后在調用一下set方法,即可完美使用,如下:
String url = "http://img2.imgtn.bdimg.com/it/u=4060216298,1329589408&fm=27&gp=0.jpg";
        mImageView = findViewById(R.id.main_IV);
        ImageLoader loader = new ImageLoader();
        DoubleCache doubleCache = new DoubleCache();
        loader.setImageCache(doubleCache);
        loader.displayImage(url,mImageView);
  • 這就是開閉原則,我們的圖片加載機制對于擴展式開放的,我們可以任意去擴展我們的緩存機制,而不用去管一點點圖片加載的細節,就可以實現代碼的開閉原則
  • 這就是六大原則的前兩種,預知后面如何,且聽下回分解
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容