美文网首页
读书笔记-面向对象的六大原则(一)

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

作者: 小黄人__ | 来源:发表于2018-08-07 19:36 被阅读0次

    单一职责原则

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

    相关文章

      网友评论

          本文标题:读书笔记-面向对象的六大原则(一)

          本文链接:https://www.haomeiwen.com/subject/odhovftx.html