读书笔记-面向对象的六大原则(一)

单一职责原则

  • 读《Android源码设计模式》
  • 单一职责的定义为:就一个类而言,应该仅有一个引起它变化的原因,简单来说,一个类中应该是一组相关性很高的函数,数据的封装
  • 我们从最入门的方式入手

入手

  • 假设现在要实现图片加载的功能,并且能将图片缓存,我们可能写出的代码是这样的

public class ImageLoader {

    //图片缓存
    LruCache 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(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 mImageCache;

    public ImageCache() {
        initImageCache();
    }

    private void initImageCache(){
        //计算可使用的最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024);

        //取四分之一的可用内存作为缓存
        final int cacheSize = maxMemory / 4;

        mImageCache = new LruCache(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);
  • 这就是开闭原则,我们的图片加载机制对于扩展式开放的,我们可以任意去扩展我们的缓存机制,而不用去管一点点图片加载的细节,就可以实现代码的开闭原则
  • 这就是六大原则的前两种,预知后面如何,且听下回分解

你可能感兴趣的:(读书笔记-面向对象的六大原则(一))