面向对象的原则之开闭原则

开闭原则的英文全称是Open Close Principle,缩写是OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。

勃兰特-梅耶在1988年出版的《面向对象软件构造》一书中提出这一原则---开闭原则。这一想法认为,程序一旦开发完成,程序中一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类实现,新建的类可以通过继承的方式来重用原类的代码。显然梅耶的定义提倡实现继承,已存在的实现类对于修改时封闭的,但是新的类可以通过覆写父类的接口应对变化。

引入其他缓存方式

说了这么多,下面还是以一个简单示例来说明开闭原则:

在对ImageLoader进行了一次重构之后的ImageLoader职责单一、结构清晰,算是个不错的开始。随着用户的增多,有些问题开始暴露出来:缓存系统是最让大家吐槽的地方,通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用的内存很有限,且具有易失性,即当应用重新启动之后,原来已经加载过的图片将会丢失,这样重启之后及需要重新下载图片!而这又会导致加载缓慢、耗费用户流量的问题。针对以上问题考虑引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载了,接下来开始实现SD卡缓存。

DiskCache.java类,将图片缓存到SD卡中:

package com.deason.library.ocp;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import java.io.FileOutputStream;
import java.io.IOException;

/**
 * SD卡图片缓存
 * Created by liuguoquan on 2016/3/17.
 */
public class DiskCache {

    public static final String CACHE_DIR = "/sdcard/cache/";

    /**
     * 从缓存中获取图片
     * @param url
     * @return
     */
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(CACHE_DIR + url);
    }

    /**
     * 将图片存入SD卡中
     * @param url
     * @param bitmap
     */
    public void put(String url,Bitmap bitmap) {

        FileOutputStream out = null;
        try {
            out = new FileOutputStream(CACHE_DIR + url);
            bitmap.compress(Bitmap.CompressFormat.PNG,100,out);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

MemoryCache类,把图片缓存到内存中

package com.deason.library.ocp;

import android.graphics.Bitmap;
import android.util.LruCache;

/**
 * 处理图片缓存
 * Created by liuguoquan on 2016/3/14.
 */
public class MemoryCache {

    /**
     * 图片缓存
     */
    LruCache<String, Bitmap> mImageCache;
    public MemoryCache() {
        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 value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
    }

    /**
     * 将图片存入缓存
     * @param key
     * @param bitmap
     */
    public void put(String key,Bitmap bitmap) {
        mImageCache.put(key,bitmap);
    }

    /**
     * 取出缓存图片
     * @param key
     * @return
     */
    public Bitmap get(String key) {
        return mImageCache.get(key);
    }
}

因为需要将图片缓存到SD卡中,所有ImageLoader代码有所更新,具体代码如下:

/**
 * 图片加载类
 * Created by liuguoquan on 2016/3/14.
 */
public class ImageLoader {

    /**
     * 图片缓存
     */
    MemoryCache mMemoryCache = new MemoryCache();

    /**
     * SD卡缓存
     */
    DiskCache mDiskCache = new DiskCache();

    /**
     * 是否使用SD卡缓存
     */
    private boolean isUseDiskCache = false;

    /**
     * 线程池,线程数量未CPU的数量
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());

    /**
     * 显示图片
     * @param url
     * @param imageView
     */
    public void displayImage(final String url, final ImageView imageView) {
        //判断使用哪种缓存
        Bitmap bitmap =  isUseDiskCache ? mDiskCache.get(url) :  mMemoryCache.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)) {
                    imageView.setImageBitmap(bitmap);
                }
                //将下载的图片存入内存
                mMemoryCache.put(url,bitmap);
                mDiskCache.put(url,bitmap);
            }
        });
    }

    /**
     * 设置是否用SD卡缓存
     * @param isUseDiskCache
     */
    public void useDiskCache(boolean isUseDiskCache) {
        this.isUseDiskCache = isUseDiskCache;
    }

    /**
     * 下载图片
     * @param url
     * @return
     */
    public Bitmap downloadImage(String imageUrl) {

        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
            int code = mConnection.getResponseCode();

            if (200 == code) {
                bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
            }
            mConnection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

从上述的代码中可以看到,仅仅新增了一个DiskCache类和往ImageLoader类中加入了少量代码就添加了SD卡缓存的功能,用户可以通过useDiskCache方法来对使用哪种缓存进行设置,例如

ImageLoader mImageLoader = new ImageLoader();
//使用SD卡缓存
mImageLoader.useDiskCache(true);
//使用内存缓存
mImageLoader.useDisCache(false);

通过useDiskCache方法来让用户设置不同的缓存这个思路是对的,但是会有些明显的问题,就是使用内存缓存时用户不能使用SD卡缓存。类似地,使用SD卡缓存时用户就不能使用内存缓存。

实际上,用户需要这两种缓存的综合,首先缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络上获取,这才是最好的缓存策略。

接下来,我们继续重构,新建一个双缓冲类DoubleCache,具体代码如下:

package com.deason.library.ocp;


import android.graphics.Bitmap;

/**
 * 双缓冲。获取图片时先从内存中获取,如果内存中没有缓存该图片,再从SD卡中获取
 * Created by liuguoquan on 2016/3/17.
 */
public class DoubleCache {

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

    /**
     * 先从内存获取图片,没有再从SD卡获取
     * @param url
     * @return
     */
    public Bitmap get(String url) {

        Bitmap bitmap = mMemoryCache.get(url);

        if (bitmap == null) {
            bitmap = mDiskCache.get(url);
        }

        return bitmap;
    }

    /**
     * 将图片缓存到内存和SD卡中
     * @param url
     * @param bitmap
     */
    public void put(String url, Bitmap bitmap) {
        mMemoryCache.put(url, bitmap);
        mDiskCache.put(url, bitmap);
    }
}

我们再来看看最新的ImageLoader类,代码更新也不多:

/**
 * 图片加载类
 * Created by liuguoquan on 2016/3/14.
 */
public class ImageLoader {

    /**
     * 图片缓存
     */
    MemoryCache mMemoryCache = new MemoryCache();

    /**
     * SD卡缓存
     */
    DiskCache mDiskCache = new DiskCache();
    //双缓存
    DoubleCache mDoubleCache = new DoubleCache();

    /**
     * 是否使用SD卡缓存
     */
    private boolean isUseDiskCache = false;
    //使用双缓存
    private boolean isUseDoubleCache = false;

    /**
     * 线程池,线程数量未CPU的数量
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());

    /**
     * 显示图片
     * @param url
     * @param imageView
     */
    public void displayImage(final String url, final ImageView imageView) {
        //判断使用哪种缓存
        Bitmap bitmap =  null;
        if (isUseDoubleCache) {
            bitmap = mDoubleCache.get(url);
        } else if (isUseDiskCache) {
            bitmap = mDiskCache.get(url);
        } else {
            bitmap = mMemoryCache.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)) {
                    imageView.setImageBitmap(bitmap);
                }
                //将下载的图片存入内存
                mMemoryCache.put(url,bitmap);
                mDiskCache.put(url,bitmap);
            }
        });
    }

    /**
     * 设置是否用SD卡缓存
     * @param isUseDiskCache
     */
    public void useDiskCache(boolean isUseDiskCache) {

        this.isUseDiskCache = isUseDiskCache;
    }

    /**
     * 设置是否使用双缓存
     * @param isUseDoubleCache
     */
    public void useDoubleCache(boolean isUseDoubleCache) {

        this.isUseDoubleCache = isUseDoubleCache;
    }

    /**
     * 下载图片
     * @param url
     * @return
     */
    public Bitmap downloadImage(String imageUrl) {

        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
            int code = mConnection.getResponseCode();

            if (200 == code) {
                bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
            }
            mConnection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}


此时程序越来越灵活了,通过增加短短几行代码就能够完成如此重要的功能。

人们总会把美好的事情讲在前面,最后给你来个“但是”,我们先来分析一下上面的程序:每次在程序中加入新的缓存实现都需要修改ImageLoader类,然后通过一个布尔类型的变量来让用户选择使用哪种缓存,因此,就使得ImageLoader中存在各种if-else判断语句,通过这些判断语句来确定使用哪种缓存。随着大量逻辑的引用,代码变得越来越复杂脆弱,如果一不小心写错某个if条件,势必会花费大量的时间去调试程序,整个ImageLoader类也会显得臃肿。最重要的是,用户不能自己实现缓存注入到ImageLoader类中,可扩展性较差,而可扩展性是框架的最重要的特性之一。

注入方式设置缓存方式

“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改则是封闭的,这就是开放-关闭原则。也就是说,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现”。现在,我们来实现终极解决方案,遵循开闭原则。

首先,我们先画出UML图,如下

开闭原则.png

然后,实现UML图上的类和接口,先重构ImageLoader类:

/**
 * Created by liuguoquan on 2016/3/17.
 */
public interface ImageCache {

    /**
     * 获取图片
     * @param url
     * @return
     */
    public Bitmap get(String url);

    /**
     * 缓存图片
     * @param url
     * @param bitmap
     */
    public void put(String url, Bitmap bitmap);
}

经过重构后,少了if-else语句,没有了各种各样的缓存实现对象、布尔变量,代码确实清晰、简单了很多。需要注意的是,这次重构的ImageCache把它提取成一个图片缓存的接口,用来抽象图片缓存的功能,我们来看看该接口的声明:

/**
 * 图片加载类
 * Created by liuguoquan on 2016/3/14.
 */
public class ImageLoader {

    /**
     * 图片缓存
     */
    ImageCache mImageCache = new MemoryCache();

    /**
     * 注入缓存实现
     * @param cache
     */
    public void setIamgeCache(ImageCache cache) {
        mImageCache = cache;
    }

    /**
     * 线程池,线程数量未CPU的数量
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());

    /**
     * 显示图片
     * @param url
     * @param imageView
     */
    public void displayImage(final String url, final ImageView imageView) {
        Bitmap bitmap =  mImageCache.get(url);
        if (bitmap != null) {
            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) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(bitmap);
                }
                //将下载的图片存入内存
                mImageCache.put(url,bitmap);
            }
        });
    }

    /**
     * 下载图片
     * @param url
     * @return
     */
    public Bitmap downloadImage(String imageUrl) {

        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
            int code = mConnection.getResponseCode();

            if (200 == code) {
                bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
            }
            mConnection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

ImageCache接口简单定义了获取、缓存图片两个方法,缓存的可以是图片的url,值是图片的本身。内存缓存、SD卡缓存、爽缓存都实现该接口,我们看看几个缓存的实现:

MemoryCache.java

public class MemoryCache implements ImageCache {

    /**
     * 图片缓存
     */
    LruCache<String, Bitmap> mMemoryCache;
    public MemoryCache() {
        initImageCache();
    }

    /**
     * 初始化LRU缓存
     */
    private void initImageCache() {

        //计算可使用的最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        //取内存的四分之一作为缓存
        final int cacheSize = maxMemory / 4;

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
    }
    @Override
    public Bitmap get(String url) {
        return mMemoryCache.get(url);
    }

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

        mMemoryCache.put(url, bitmap);
    }
}

DiskCache.java

/**
 * Created by liuguoquan on 2016/3/17.
 */
public class DiskCache implements  ImageCache {

    public static final String CACHE_DIR = "/sdcard/cache/";
    @Override
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(CACHE_DIR + url);
    }

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

        FileOutputStream out = null;

        try {
            out = new FileOutputStream(CACHE_DIR + url);
            bitmap.compress(Bitmap.CompressFormat.PNG,100,out);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Double.java

/**
 * Created by liuguoquan on 2016/3/17.
 */
public class DoubleCache implements ImageCache {

    ImageCache mMemoryCache = new MemoryCache();
    ImageCache 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类中增加了一个setImageCache(ImageCache cache)方法,用户可以通过该函数设置缓存实现,也就是通常所说的依赖注入

下面看看用户如何通过使用ImageLoader来设置缓存实现的:

ImageLoader mImageLoader = new ImageLoader();

//使用内存缓存
mImageLoader.setIamgeCache(new MemoryCache());

//使用SD卡缓存
mImageLoader.setIamgeCache(new DiskCache());

//使用双缓存
mImageLoader.setIamgeCache(new DoubleCache());

//使用自定义的图片缓存实现
mImageLoader.setIamgeCache(new ImageCache() {
    @Override
    public Bitmap get(String url) {
        return null;
    }

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

    }
});

在上述代码中,通过setImageCache(ImageCache cache)方法注入不同的缓存实现,这样不仅能够使ImageLoader更简单、健壮,也使得ImageLoader可课扩展性、灵活性更高。

MemeryCache、DiskCache、DoubleCache缓存图片的具体实现完全不一样,但是,它们有一个特点是,都实现了ImageCache接口。当用户需要自定义缓存实现时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通过setImageCache(ImageCache cache)注入到ImageLoader中就可以了,这样ImageLoader就实现了千变万化的缓存策略,且扩展这些缓存策略并不会导致ImageLoader类的修改。经过这次重构,ImageLoader在设计模式上基本已经合格了。

开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的。当确定原来的代码已经“腐化”时,应该尽早的重构,以便让代码恢复带正常的状态,而不是通过继承等方式添加新的代码。因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳当、更灵活,在保证去除“腐化代码”的同时,也保证原有模块的正确性。

优点

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

推荐阅读更多精彩内容