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

单一职责原则

  • 读《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);
  • 这就是开闭原则,我们的图片加载机制对于扩展式开放的,我们可以任意去扩展我们的缓存机制,而不用去管一点点图片加载的细节,就可以实现代码的开闭原则
  • 这就是六大原则的前两种,预知后面如何,且听下回分解
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容