开闭准则
开闭准则的英文全称是 OpenClosePrinciple,缩写是 OCP,它是 Java 世界里最基础的设计准则,它辅导咱们怎么建立一个安稳的、灵敏的体系。开闭准则的界说是:软件中的对象(类、模块、函数等)应该关于扩展是敞开的,可是,关于修正是封闭的。在软件的生命周期内,由于改动、晋级和保护等原因需求对软件原有代码进行修正时,或许会将过错引入本来现现已过测试的旧代码中,损坏原有体系。因而,当软件需求改动时,咱们应该尽量经过扩展的方式来完结改动,而不是经过修正已有的代码来完结。当然,在现实开发中,只经过承继的方式来晋级、保护原有体系仅仅一个理想化的愿景,因而,在实际的开发进程中,修正原有代码、扩展代码往往是一起存在的。
软件开发进程中,最不会改动的便是改动自身。产品需求不断地晋级、保护,没有一个产品从第一版本开发完就再没有改动了,除非在下个版本诞生之前它现已被停止。而产品需求晋级,修正本来的代码就或许会引发其他的问题。那么,怎么确保原有软件模块的正确性,以及尽量少地影响原有模块,答案便是,尽量遵守开闭准则。
勃兰特•梅耶在1988年出版的《面向对象软件结构》一书中提出这一准则 —— 开闭准则。这一主意以为,程序一旦开发完结,程序中一个类的完结只应该因过错而被修正,新的或者改动的特性应该经过新建不同的类完结,新建的类能够经过承继的方式来重用原类的代码。明显,梅耶的界说发起完结承继,己存在的完结类关于修正是封闭的,可是新的完结类能够经过覆写父类的接口应对改动。
比如
持续运用 # Android 优化代码的第一步 —— 单一职责准则 这里面 ImageLoader 的比如。
优化前
经过内存缓存解决了每次从网络加载图片的问题,可是,Android 运用的内存很有限,且具有易失性,即当运用重新启动之后,本来现已加载过的图片将会丢掉,这样重启之后就需求重新下载。这又会导致加载缓慢、消耗用户流量的问题。于是很自然会引入 SD 卡缓存,这样下载过的图片就会缓存到本地,即便重启运用也不需求重新下载了。
DiskCache.java 类,将图片缓存到 SD 卡中。
public class DiskCache {
static String cacheDir = "sdcard/cache/";
// 从缓存中获取图片
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
// 将图片缓存到内存中
public void put(String url, Bitmap bmp) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
CloseUtils.closeQuietly(fileOutputStream);
}
}
}
/**
* 图片加载类
*/
public class ImageLoader {
// 内存缓存
ImageCache mImageCache = new ImageCache();
// sd卡缓存
DiskCache mDiskCache = new DiskCache();
// 运用sd卡缓存
boolean isUseDiskCache = false;
// 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors());
private Handler mUiHandler = new Handler();
public void displayImage(final String url, final ImageView imageView) {
// 判别运用哪种缓存
Bitmap bmp = null;
if (isUseDiskCache) {
bmp = mDiskCache.get(url);
} else {
bmp = mImageCache.get(url);
}
if (bmp != null) {
imageView.setImageBitmap(bmp);
return;
}
// 没有缓存,则提交给线程池进行异步下载
}
public void useDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache;
}
}
从上述的代码中能够看到,仅仅新增了一个 DiskCache 类和往 ImageLoader 类中参加了少数代 码就添加了 SD 卡缓存的功用,用户能够经过 useDiskCache 办法来对运用哪种缓存进行设置,例如:
ImageLoader imageLoader = new ImageLoader() ;
// 运用sd卡缓存
imageLoader.useDiskCache(true);
// 运用内存缓存
imageLoader.useDiskCache(false);
可是这么做会有一个问题,便是运用内存缓存时用户就不能运用 SD 卡缓存。类似地,运用 SD 卡缓存时用户就不能运用内存缓存。用户需求这两种战略的综合,首要优先运用内存缓存,假如内存缓存没有图片再运用 SD 卡缓存,假如 SD 卡中也没有图片,最后才从网络上获取,这才是最好的缓存战略。
新建一个双缓存类 DoubleCache,详细代码如下。
/**
* 双缓存。获取图片时先从内存缓存中获取,假如内存中没有缓存该图片再从sd卡中获取。 缓存图片也是也是在内存和sd卡中都缓存一份。
*/
public class DoubleCache {
MemoryCache mMemoryCache = new MemoryCache();
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 bmp) {
mMemoryCache.put(url, bmp);
mDiskCache.put(url, bmp);
}
}
咱们再看看最新的 ImagcLoader 类,代码更新也不多。
/**
* 图片加载类
*/
public class ImageLoader {
// 内存缓存
ImageCache mImageCache = new ImageCache();
// sd卡缓存
DiskCache mDiskCache = new DiskCache();
// 双缓存
DoubleCache mDoubleCache = new DoubleCache() ;
// 运用sd卡缓存
boolean isUseDiskCache = false;
// 运用双缓存
boolean isUseDoubleCache = false;
// 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors());
private Handler mUiHandler = new Handler() ;
public void displayImage(final String url, final ImageView imageView) {
Bitmap bmp = null;
if (isUseDoubleCache) {
bmp = mDoubleCache.get(url);
} else if (isUseDiskCache) {
bmp = mDiskCache.get(url);
} else {
bmp = mImageCache.get(url);
}
if ( bmp != null ) {
imageView.setImageBitmap(bmp);
return;
}
// 没有缓存,则提交给线程池进行异步下载
}
public void useDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache ;
}
public void useDoubleCache(boolean useDoubleCache) {
isUseDoubleCache = useDoubleCache ;
}
}
可是每次参加新的缓存办法时都要修正本来的代码,这样很或许会引入 Bug,并且会使本来的代码逻辑变得越来越复杂。依照这样的办法完结,用户也不能自界说缓存完结。
咱们来分析一下上面的程序。每次在程序中参加新的缓存完结时都需求修正ImageLoader类,然后经过一个布尔变量来让用户选择运用哪种缓存,因而,就使得在 ImageLoader 中存在各种 if-else 判别语句,经过这些判别来确认运用哪种缓存。跟着这些逻辑的引入,代码变得越来越复杂、软弱,假如一不小心写错了某个计条件(条件太多,这是很容易出现的),那就需求更多的时问来扫除,整个 ImageLoader 类也会变得越来越雕肿。最重要的是,用户不能白己完结缓存注入到 ImageLoader 中,可扩展性差,可扩展性可是结构的最重要特性之一。
软件中的对象(类、模块、函数等)应该关于扩展是敞开的,可是关于修正是封闭的,这便是敞开一封闭准则。也便是说,当软件需求改动时,咱们应该尽量经过扩展的方式来完结改动,而不是经过修正己有的代码来完结。
优化后
再一次重构该代码:
/**
* 图片加载类
*/
public class ImageLoader {
// 图片缓存
ImageCache mImageCache = new MemoryCache();
// 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors());
private Handler mUiHandler = new Handler() ;
public void setImageCache(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 imageUrl, final ImageView imageView) {
imageView.setTag(imageUrl);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(imageUrl);
if (bitmap == null) {
return;
}
if (imageView.getTag().equals(imageUrl)) {
updateImageView(imageView, bitmap);
}
mImageCache.put(imageUrl, bitmap);
}
});
}
private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap); ;
}
});
}
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;
}
}
经过这次重构,没有了那么多的 if-else 语句,没有了各种各样的缓存完结对象、布尔变量,代码的确清晰、简略了很多。需求注意的是,这里的 ImageCache 类并不是本来的那个 ImageCache,这次重构程序,把它提取成一个图片缓存的接口,用来抽象图片缓存的功用,咱们看看该接口的声明:
/**
* 图片缓存接口
*
* @author mrsimple
*/
public interface ImageCache {
public Bitmap get(String url);
public void put(String url, Bitmap bmp);
}
ImageCache 接口简略界说了获取、缓存图片两个函数,缓存的 key 是图片的 url,值是图片自身。内存缓存、SD 卡缓存、双缓存都完结了该接口,咱们看看这几个缓存完结。
/**
* 内存缓存
*/
public class MemoryCache implements ImageCache {
private LruCache<String, Bitmap> mMemeryCache;
public MemoryCache() {
// 核算可运用的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 取4分之一的可用内存作为缓存
final int cacheSize = maxMemory / 4;
mMemeryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
@Override
public Bitmap get(String url) {
return mMemeryCache.get(url);
}
@Override
public void put(String url, Bitmap bmp) {
mMemeryCache.put(url, bmp);
}
}
/**
* 本地(sd卡)图片缓存
*/
public class DiskCache implements ImageCache {
@Override
public Bitmap get(String url) {
return null/* 从本地文件中获取该图片 */;
}
private String imageUrl2MD5(String imageUrl) {
// 对imgeUrl进行md5运算, 省掉
String md5 = imageUrl;
return md5;
}
@Override
public void put(String url, Bitmap bmp) {
// 将Bitmap写入文件中
FileOutputStream fos = null;
try {
// 构建图片的存储途径 ( 省掉了对url取md5)
fos = new FileOutputStream("sdcard/cache/" + imageUrl2MD5(url));
bmp.compress(CompressFormat.JPEG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if ( fos != null ) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 双缓存。获取图片时先从内存缓存中获取,假如内存中没有缓存该图片再从sd卡中获取。 缓存图片也是也是在内存和sd卡中都缓存一份。
*/
public class DoubleCache implements ImageCache{
ImageCache mMemoryCache = new MemoryCache();
ImageCache 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 bmp) {
mMemoryCache.put(url, bmp);
mDiskCache.put(url, bmp);
}
}
ImageLoader 类中增加了一个 setImageCache(ImageCachecache) 函数,用户能够经过该函数设置缓存完结,也便是通常说的依赖注入。下面就看看用户是怎么设置缓存完结的。
ImageLoader imageLoader = new ImageLoader() ;
// 运用内存缓存
imageLoader.setImageCache(new MemoryCache());
// 运用sd卡缓存
imageLoader.setImageCache(new DiskCache());
// 运用双缓存
imageLoader.setImageCache(new DoubleCache());
// 运用自界说的图片缓存完结
imageLoader.setImageCache(new ImageCache() {
@Override
public void put(String url, Bitmap bmp) {
// 缓存图片
}
@Override
public Bitmap get(String url) {
return null /*从自界说的缓存完结中获取图片*/;
}
});
在上述代码中,经过 setlmageCache(ImageCachecache) 办法注入不同的缓存完结,这样不只能够使 ImageLoader 更简略、健壮,也使得 ImageLoader 的可扩展性、灵敏性更高。MemoryCache、DiskCache、DoubleCache 缓存图片的详细完结完全不一样,可是,它们的一个特点是,都完结了 ImageCache 接口。当用户需求自界说完结缓存战略时,只需求新建一个完结ImageCache 接口的类,然后结构该类的对象,并且经过 setimageCache(ImageCachecache) 注入到 ImageLoader 中,这样 ImageLoader 就完结了千变万化的缓存战略,且扩展这些缓存战略并不会导致 ImageLoader 类的修正。
总结
开闭准则辅导咱们,当软件需求改动时,应该尽量经过扩展的方式来完结改动,而不是经过修正已有的代码来完结。这里的“应该尽量”4个字阐明 OCP 准则并不是说绝对不能够修正原始类的。当咱们嗅到本来的代码“堕落气味”时,应该尽早地重构,以便使代码康复到正常的“进化”进程,而不是经过承继等方式添加新的完结,这会导致类型的胀大以及历史遗留代码的冗余。咱们的开发进程中也没有那么理想化的状况,完全地不用修正本来的代码,因而,在开发进程中需求自己结合详细情況进行考量,是经过修正旧代码还是经过承继使得软件体系更安稳、更灵敏,在确保去除“代码堕落”的一起,也确保原有模块的正确性。