概念
开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是
封闭的。简单说就是,对于经过测试已经稳定的代码禁止修改,如果有新的需求只允许扩展原有代
码。因为修改可能将错误引入到已经经过测试稳定的代码,破坏原有系统。当然这只是理想中的状
态,在实际开发中,修改原有代码和扩展代码都同时存在的,我们要做的就是不断的去接近这种理
想的状态。开闭原则是面向对象编程世界里最基础的设计原则,它指导我们如何构建一个稳定的、
灵活的系统。
那么我们怎么扩展代码,怎么封闭代码?还是用上一篇文章中的例子来说明。
在单一职责里面的例子(六大设计原则之单一职责),似乎是完美的,通过内存缓存解决了每次都
从网络加载图片的问题,但是当内存被回收或者应用重启,原来加载过的图片就消失,需要重新加
载,这样耗流量耗时间,体验很差。于是,现在提出了新的缓存策略:用SD卡缓存。文章有点长,
代码有点多,是为了引出问题,然后解决问题,总之专研技术需要耐心,最好自己敲一遍代码,体
会其中的不同,通过层层优化,最后达到较理想的效果。优化之前的代码逻辑
sd卡缓存:
package com.study.demo.designdemo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Created by WKC on 2017/8/8.
*/
public class DiskCache {
//路径
private static String cacheDir = "sdcard/cache/";
//从缓存中获取图片
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
//将图片缓存到内存中
public void put(String url,Bitmap bitmap){
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fileOutputStream!=null){
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
因为需要将图片缓存到SD卡中,所以需要更改ImageLoader的代码,具体代码如下
package com.study.demo.designdemo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 图片加载类
* Created by WKC on 2017/7/8.
*/
public class ImageLoader {
private Bitmap bitmap;
//是否使用SD卡缓存
private boolean isUseDiskCache = false;
//内存缓存
private ImageCache mImageCache = new ImageCache();
//SD卡缓存
private DiskCache mDiskCache = new DiskCache();
//线程池,线程数量为CUP的数量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().
availableProcessors());
//UI Handler
Handler mUiHandler = new Handler(Looper.getMainLooper());
/**
* 提供外部调用显示图片的函数
*
* @param url 图片的路径
* @param imageview 显示图片的view
*/
public void displayImage(final String url, final ImageView imageview) {
bitmap = getUseDiskCache() ? mDiskCache.get(url) : 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)) {
updateImageview(imageview, bitmap);
}
mImageCache.put(url, bitmap);
}
});
}
/**
* 在UI线程显示图片
*
* @param imageview
* @param bitmap
*/
private void updateImageview(final ImageView imageview, final Bitmap bitmap) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageview.setImageBitmap(bitmap);
}
});
}
/**
* 下载图片
*
* @param imegeUrl
* @return
*/
private Bitmap downloadImage(String imegeUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imegeUrl);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
public boolean getUseDiskCache() {
return isUseDiskCache;
}
public void setUseDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache;
}
}
通过以上代码,仅增加了DiskCache类和往imageLoader类中加入少量的代码就增加了SD卡缓存的
功能,且我们可以直接设置useDiskCache的值来决定是否使用SD卡缓存,很是方便。但是聪明的
你一定发现了上面的例子中,当用户使用了内存就不能用SD卡缓存,反之亦然。能不能再优化下,
两种缓存策略都用,即优先使用内存缓存,如果没有图片再使用SD卡缓存,如果SD卡中也没有图片,
最后才从网络中获取。于是,我们新建一个双缓存类:
双缓存类
package com.study.demo.designdemo;
import android.graphics.Bitmap;
/**
* Created by WKC on 2017/7/8.
*/
public class DoubleCache {
//内存
ImageCache mMemoryCache = new ImageCache();
//SD卡
DiskCache mDiskCache = new DiskCache();
//先从内存中获取图片,如果没有,再从SD卡中获取
public Bitmap get(String url) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap == null) {
bitmap = mDiskCache.get(url);
}
return bitmap;
}
//将图片缓存
public void put(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
mDiskCache.put(url, bitmap);
}
}
同样,ImageLoader类也需要更新,代码如下
package com.study.demo.designdemo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 图片加载类
* Created by WKC on 2017/7/8.
*/
public class ImageLoader {
private Bitmap bitmap;
//内存缓存
private ImageCache mImageCache = new ImageCache();
//SD卡缓存
private DiskCache mDiskCache = new DiskCache();
//双缓存
private DoubleCache mDoubleCache = new DoubleCache();
//是否使用SD卡缓存
private boolean isUseDiskCache = false;
//是否使用双缓存
private boolean isDoubleCache = false;
//线程池,线程数量为CUP的数量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().
availableProcessors());
//UI Handler
Handler mUiHandler = new Handler(Looper.getMainLooper());
/**
* 提供外部调用显示图片的函数
*
* @param url 图片的路径
* @param imageview 显示图片的view
*/
public void displayImage(final String url, final ImageView imageview) {
if (getDoubleCache()) {
bitmap = mDoubleCache.get(url);
} else if (getUseDiskCache()) {
bitmap = mDiskCache.get(url);
} else {
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)) {
updateImageview(imageview, bitmap);
}
mImageCache.put(url, bitmap);
}
});
}
/**
* 在UI线程显示图片
*
* @param imageview
* @param bitmap
*/
private void updateImageview(final ImageView imageview, final Bitmap bitmap) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageview.setImageBitmap(bitmap);
}
});
}
/**
* 下载图片
*
* @param imegeUrl
* @return
*/
private Bitmap downloadImage(String imegeUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imegeUrl);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
public boolean getUseDiskCache() {
return isUseDiskCache;
}
public void setUseDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache;
}
public boolean getDoubleCache() {
return isDoubleCache;
}
public void setDoubleCache(boolean doubleCache) {
isDoubleCache = doubleCache;
}
}
有没有发现,每当我们新增加一个需求,我们所有的类都需要改动,麻烦不说,万一有其他类正在
调用原有的代码,是不是可能会惹来“麻烦”?而且使用了很多的判断逻辑导致代码很复杂,这是
很容易出现bug的,而且是不容易找出.再者,用户不能自己实现/定义缓存注入到ImageLoader中,可
扩展性差,背离了面向对象的思想。
“软件中的对象(类。模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开
放关闭原则。”也就是说当需求变化时,我们应该尽量通过扩展方式来实现变化,而不是通过修改
已有的代码来实现。为了实现这样的效果,我们需要对前面的代码来一次重构,对上面的代码重构
中涉及的几个类的代码都发生了不同程度的改变,所以我打算把所有的各类的代码都贴出来。
- 优化之后的代码逻辑
我们先看下UML图,对UML图不了解的这篇文章深入浅出UML类图对你有帮助。
代码中的注释已经很详细了,就不在详细解释。
1.ImageLoader类
package com.study.demo.designdemo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 图片加载类
* Created by WKC on 2017/7/8.
*/
public class ImageLoader {
//默认的缓存方式
private ImageCache mImageCache = new MemoryCache();
//线程池,线程数量为CUP的数量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().
availableProcessors());
//UI Handler
Handler mUiHandler = new Handler(Looper.getMainLooper());
/**
* 在UI线程显示图片
*
* @param imageview
* @param bitmap
*/
private void updateImageview(final ImageView imageview, final Bitmap bitmap) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageview.setImageBitmap(bitmap);
}
});
}
/**
* 注入缓存实现
*
* @param cache
*/
public void setImageCache(ImageCache cache) {
mImageCache = cache;
}
/**
* 提供外部调用显示图片的函数
*
* @param url 图片的路径
* @param imageview 显示图片的view
*/
public void displayImage(final String url, final ImageView imageview) {
Bitmap bitmap = mImageCache.get(url);
if (bitmap != null) {
imageview.setImageBitmap(bitmap);
return;
}
submitLoadRequest(url, imageview);
}
/**
* 没有缓存,则从网络中获取
*
* @param url
* @param 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)) {
updateImageview(imageview, bitmap);
}
mImageCache.put(url, bitmap);
}
});
}
/**
* 下载图片
*
* @param imegeUrl
* @return
*/
private Bitmap downloadImage(String imegeUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imegeUrl);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}
经过这次重构,没有了那么多的 if-else 语句了,没有了各种各样的缓存实现对象、布尔变量,代码
也清晰简单了很多。不过这里用到的ImageCache 已经不是之前的那个ImageCache类了,而是把
他提取成图片缓存接口,抽象缓存的功能。我们来看看该接口中的声明:
2.图片缓存接口
public interface ImageCache {
void put(String url, Bitmap bitmap);
Bitmap get(String url);
}
该接口定义了获取和缓存图片的两个函数,缓存的key是图片的url,值是图片本身。然后让内存缓
存、SD卡缓存或者
双缓存都实现该接口。
3.内存缓存
package com.study.demo.designdemo;
import android.graphics.Bitmap;
import android.util.LruCache;
/**
* Created by WKC on 2017/7/8.
*/
public class MemoryCache implements ImageCache {
private LruCache<String, Bitmap> mMemoryCache;
public MemoryCache() {
//初始化缓存
initImageCache();
}
@Override
public Bitmap get(String url) {
return mMemoryCache.get(url);
}
@Override
public void put(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
}
//初始化缓存内存的参数
private void initImageCache() {
//计算可使用的最大内存
final long maxMemory = Runtime.getRuntime().maxMemory() / 1024;
//取四分之一的可用内存作为缓存
final int cacheSize = (int) (maxMemory / 4);
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
}
4.SD卡缓存
package com.study.demo.designdemo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Created by WKC on 2017/7/8.
*/
public class DiskCache implements ImageCache {
private static String cacheDir = "sdcard/cache/";
/**
* 从SD卡缓存中获取图片
* @param url
* @return
*/
@Override
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
@Override
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
//从内存中写入SD卡
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
5.双缓存
package com.study.demo.designdemo;
import android.graphics.Bitmap;
/**
* Created by WKC on 2017/7/8.
*/
public class DoubleCache implements ImageCache {
//内存
private ImageCache mMemoryCache = new MemoryCache();
//SD卡
private 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);
}
}
6.最后是调用
细心的读者可能注意到了,在ImageLoader中增加了SetImageCache(ImageCache cache)函
数,用户可以通过该函数设置缓存实现,不再需要去更改Imageloader函数中的代码了,也就
是常说的依赖注入。这样也就达到了我们的最初目的。
我们来看下用户如何设置缓存的
imageView = (ImageView) findViewById(R.id.imageView);
//使用内存缓存策略
imageLoader.setImageCache(new MemoryCache());
//使用SD卡缓存策略
imageLoader.setImageCache(new DiskCache());
//使用双缓存策略
imageLoader.setImageCache(new DoubleCache());
//使用自定义的缓存
imageLoader.setImageCache(new ImageCache() {
@Override
public Bitmap get(String url) {
//获取图片,返回Bitmap对象
return null;
}
@Override
public void put(String url, Bitmap bitmap) {
// 缓存图片
}
});
imageLoader.displayImage(url,imageView);
最后来下总结:
1.需求的变更
内存缓存实现图片缓存–>磁盘缓存实现图片缓存–>内存和磁盘的双缓存实现图片缓存。
2.如何优化
从开始的每次需求的变更都需要去更改原先的代码,而且代码中有了不少的逻辑判断,容易
出现失误。–>我们定义了一个规则(ImageCache接口),当我们的需求变化时,只需要更改
实现这个接口,并把它注入缓存类(ImageLoader类),就能在不改变原有代码的条件下实
现了我们新的需求,也没有了那么多的逻辑判断。