写在前面#
我来晚了,迟到的祝福,祝各位同行们**中秋快乐! **
今天文章的主题是六大原则中的开闭原则。
面向对象编程的六大原则
- 单一职责原则
- 开闭原则
- 里氏替换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特原则
让程序像组装机一样稳定灵活--开闭原则
开闭原则的全称是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 mImageCache;
public MemoryCache() {
initImageCache();
}
private void initImageCache() {
// 获取APP可使用的最大内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 获取四分之一大小作为缓存空间
int cacheMemorySize = maxMemory / 4;
mImageCache = new LruCache(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进行讲解,不断通过犯错和改错的过程中完善代码。
- 作者很喜欢和大家一起讨论代码,讨论各种新的框架和设计模式,希望大家踊跃留言相互交流,共同进步。
- 谢谢。