走向面向对象的六大原则-开闭原则

写在前面#


我来晚了,迟到的祝福,祝各位同行们**中秋快乐! **
今天文章的主题是六大原则中的开闭原则。

面向对象编程的六大原则


让程序像组装机一样稳定灵活--开闭原则

开闭原则的全称是Open Close Principle,简写是OCP,它是Java世界里组基础的设计原则,它将会指导我们如何建立一个稳定并且灵活的系统,让我们的程序可以像组装机一样,可以更新,换配件,但不能修改配件。开闭原则的定义是:软件中的对象(类,模块,函数等)对于拓展是开放的,对于修改时封闭的。在我们开发软件的周期中,我们可能会因为用户需求或者架构设计的变化而需要对原有代码进行大量修改,这时候很可能会将错误引入原本已经经过测试的代码之中,因而产生新的BUG影响开发周期与开发质量,破坏原有的软件设计和代码完整性。此时,我们应该尽量通过拓展的方式去修改代码实现我们要的功能,而不是在原有的基础上去修改代码。我们希望只通过继承的方式去实现代码的更新和修改,但这对于实际开发而言其实只能是我们的一个愿景。因此,在开发过程中,我们通常情况下是会通过修改原有代码并且拓展代码去实现更新和修改的功能。
那么,如何才能尽可能确保原有的软件模块完整性,正确性,并且尽量少地影响到原有代码呢?答案就是:尽量遵守本章讲述的开闭原则。


开始

上一章我们在讲述单一职责原则时,通过ImageLoader的例子去体现了单一职责原则的好处,但是在代码中我们只实现了内存缓存,那么用户在关闭APP后图片依旧是需要重新加载的,出于对用户的流量考虑,我们势必要在代码中进行拓展,添加本地磁盘缓存功能,下面是新添加的DiskCache类。

public class DiskCache {
    private final static String cacheDir = "sdcard/cacheTest";

    // 从磁盘获取图片
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }

    // 缓存图片到内存卡
    public void put(String url,Bitmap bitmap){
        // 创建输出流
        FileOutputStream outputStream = null;
        try {
            // 输出流命名
            outputStream = new FileOutputStream(url);
            // 将传进来的bitmap转为文件
            // 参数一:转换的图片格式,默认支持 JPEG,PNG,WEBP
            // 参数二:转换图片质量,100为最高
            // 参数三:承接转出文件的输出流
            bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            // 进行判断
            if (outputStream!=null){
                try {
                    // 关闭输出流
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

为了将我们的DiskCache也就是我们的磁盘缓存添加进我们的ImageLoader,我对ImageLoader进行了一定更新,不熟悉之前代码的同学请跳转到第一章单一职责原则,修改后代码如下:

public class ImageLoader {
    // 图片缓存
    ImageCache mImageCache = new ImageCache();
    //磁盘缓存
    DiskCache mDiskCache = new DiskCache();
    // 是否使用磁盘缓存
    boolean isUsedDisk = false;
    // 线程池,线程数为CPU所允许的数量
    ExecutorService mExecutorService =   Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public void displayImg(final String url, final ImageView imageView) {
        Bitmap bitmap = isUsedDisk ? mDiskCache.get(url) : mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        // View中的setTag(object)表示给View添加一个格外的数据,以后可以用getTag()将这个数据取出来。
        imageView.setTag(url);
        //在子线程中完成加载图片和缓存图片
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImg(url);
                if (bitmap == null) return;
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(bitmap);
                }
                if (isUsedDisk) mDiskCache.put(url, bitmap);
                else mImageCache.put(url,bitmap);
            }
        });
    }

    private Bitmap downloadImg(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            connection.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    public void setDiskCacheEnable(boolean enable) {
        this.isUsedDisk = enable;
    }
}

从上述代码中我们可以看到,添加了DiskCache后再修改了ImageLoader中的一丁点代码即可达成磁盘缓存的功能,但是眼尖的同学会发现,现在的ImageLoader有个非常大的弊端,就是使用内存缓存时就无法使用磁盘缓存,使用磁盘缓存就无法使用内存缓存。
我们所知的各种网络图片加载框架的正常工作流程应该是:加载图片时首先使用内存缓存,如果内存中没有再去寻找SD卡中的缓存,如果SD卡中依然没有,再从网上获取。这应该是最好的缓存策略了,那么,我们的ImageLoader应该怎么修改呢?我新建了一个类DoubleCache,代码如下:

public class DoubleCache {
    ImageCache mMemoryCache = new ImageCache();
    DiskCache mDiskCache = new DiskCache();

    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap == null) {
            bitmap = mDiskCache.get(url);
        }
        return bitmap;
    }

    public void save(String url, Bitmap bitmap) {
        mMemoryCache.put(url, bitmap);
        mDiskCache.put(url, bitmap);
    }
}

同时修改了ImageLoader中的部分内容

public class ImageLoader {
    // 图片缓存
    ImageCache mImageCache = new ImageCache();
    //磁盘缓存
    DiskCache mDiskCache = new DiskCache();
    //双缓存
    DoubleCache mDoubleCache = new DoubleCache();
    // 是否使用磁盘缓存
    boolean isUsedDisk = false;
    // 是否使用双缓存
    boolean isUsedDoubleCache = false;
    // 线程池,线程数为CPU所允许的数量
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public void displayImg(final String url, final ImageView imageView) {
        Bitmap bitmap = null;
        if (isUsedDoubleCache){
            bitmap = mDoubleCache.get(url);
        }else if (isUsedDisk){
            bitmap = mDiskCache.get(url);
        }else{
            bitmap = mImageCache.get(url);
        }
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        // 如若没有则开启线程从网络加载图片
    }

    private Bitmap downloadImg(String imageUrl) {
        // 从网络加载图片
    }

    public void setDiskCacheEnable(boolean enable) {
        this.isUsedDisk = enable;
    }

    public void setmDoubleCache(DoubleCache enable) {
        this.mDoubleCache = enable;
    }
}

以上代码实现了双缓存功能了,但是从它修改了太多已有代码,并且旧代码与新代码相互耦合,可读性也很差,不管从维护角度或者说安全性上来说,都是不适合的,而且这样的图片加载功能几乎不提供给用户任何自定义的空间,并且if判断条件过多,一步错步步错,那么,究竟要怎样才能实现出我们需要的并且漂亮合格的代码呢?这里的代码进行了大量修改。

  • 图片缓存接口
public interface ImageCache {
    Bitmap get(String url);

    void put(String url,Bitmap bitmap);
}
  • MemoryCache 内存缓存
public class MemoryCache implements ImageCache{
    // 图片缓存
    private LruCache<String, Bitmap> mImageCache;

    public MemoryCache() {
        initImageCache();
    }

    private void initImageCache() {
        // 获取APP可使用的最大内存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 获取四分之一大小作为缓存空间
        int cacheMemorySize = maxMemory / 4;
        mImageCache = new LruCache<String, Bitmap>(cacheMemorySize) {
            /**
             * Sizeof方法的作用只要是定义缓存中每项的大小,当我们缓存进去一个数据后,
             * 当前已缓存的Size就会根据这个方法将当前加进来的数据也加上,便于统计当
             * 前使用了多少内存,如果已使用的大小超过maxSize就会进行清除动作;
             */
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

    @Override
    public void put(String url, Bitmap bitmap) {
        mImageCache.put(url, bitmap);
    }

    @Override
    public Bitmap get(String url) {
        return mImageCache.get(url);
    }
}
  • DiskCache 磁盘缓存
public class DiskCache implements ImageCache{
    private final static String cacheDir = "sdcard/cacheTest";

    // 从磁盘获取图片
    @Override
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }

    // 缓存图片到内存卡
    @Override
    public void put(String url,Bitmap bitmap){
        // 创建输出流
        FileOutputStream outputStream = null;
        try {
            // 输出流命名
            outputStream = new FileOutputStream(url);
            // 将传进来的bitmap转为文件
            // 参数一:转换的图片格式,默认支持 JPEG,PNG,WEBP
            // 参数二:转换图片质量,100为最高
            // 参数三:承接转出文件的输出流
            bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            // 进行判断
            if (outputStream!=null){
                try {
                    // 关闭输出流
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • DoubleCache 双缓存
public class DoubleCache implements ImageCache {
    MemoryCache mMemoryCache = new MemoryCache();
    DiskCache mDiskCache = new DiskCache();

    @Override
    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap == null) {
            bitmap = mDiskCache.get(url);
        }
        return bitmap;
    }

    @Override
    public void put(String url, Bitmap bitmap) {
        mMemoryCache.put(url, bitmap);
        mDiskCache.put(url, bitmap);
    }
}
  • ImageLoader 图片加载类
 public class ImageLoader {
    // 图片缓存
    ImageCache mImageCache = new MemoryCache();
    // 线程池,线程数为CPU可用数
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    // 注入缓存实现
    public void setmImageCache(ImageCache cache){
        mImageCache = cache;
    }

    public void displayImage(String imageUrl,ImageView imageView){
        Bitmap bitmap = mImageCache.get(imageUrl);
        if (bitmap!=null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        //如果没有缓存,开始网络获取图片的过程
        submitLoadRequest(imageUrl,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){
                    return;
                }
                if (imageView.getTag().equals(url)){
                    imageView.setImageBitmap(bitmap);
                }
                mImageCache.put(url,bitmap);
            }
        });
    }

    private Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
            conn.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

上述代码中,我写了一个接口类ImageCache去定义获取和缓存两个函数,内存缓存,磁盘缓存,双缓存都实现了该接口。细心的朋友应该可以看见在ImageLoader类中多了一个方法 setmImageCache(ImageCache cache),通过此方法可以让用户选择需要的缓存类型甚至实现自定义的缓存,这样就是通常说的依赖注入。用户可以通过如下代码实现缓存设置:

//实例化
ImageLoader imageLoader = new ImageLoader();
//设置为磁盘缓存
imageLoader.setmImageCache(new DiskCache());
//设置为内存缓存
imageLoader.setmImageCache(new MemoryCache());
//设置为双缓存
imageLoader.setmImageCache(new DoubleCache());
//设置为自定义缓存
imageLoader.setmImageCache(new ImageCache() {
    @Override
    public Bitmap get(String url) {
        return null;
    }

    @Override
    public void put(String url, Bitmap bitmap) {

    }
});

以上代码就体现了本章的主题:开闭原则。
开闭原则指导我们,当我们的软件要发生变化时,应该尽量通过拓展的方式实现变化,而不是通过修改原有代码的形式进行。请注意此处加黑应该尽量四字,在开发过程中OCP原则并不能保证所有情况下都可以不修改原有代码去实现修改。
而在开发过程中,当我们发现我们的代码慢慢地耦合家中或者看起来杂乱无章,慢慢开始变成臭不可闻的代码时,就应该及时重构代码,以便我们的代码能够满足软件或者系统的迭代更新,而不是通过继承的方式去无限制的添加新代码,这样会导致类不断变大并造成代码冗余。
在实际开发中需要自己结合实际情况进行决策,保证代码的稳定性和灵活性,同时在保证我们的代码是清新迷人的同时,也要保证代码的正确性。


写在结尾#

  • 本章内容示例代码引用自《安卓源码设计模式》(何红辉,关爱名著)。
  • 走向面向对象-六大原则将会一直使用ImageLoader进行讲解,不断通过犯错和改错的过程中完善代码。
  • 作者很喜欢和大家一起讨论代码,讨论各种新的框架和设计模式,希望大家踊跃留言相互交流,共同进步。
  • 谢谢。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容