00.2 ANDROID 面向对象的六大原则——开闭原则

返回设计模式博客目录
|
第一篇:单一职责原则
第二篇:开闭原则
第三篇:里氏替换原则
第四篇:依赖倒置原则
第五篇:接口隔离原则
第六篇:迪米特原则

请使用双缓存技术(内存、SD 卡)继续优化图片加载器?

第一篇中已经实现了内存缓存类 ImageCache.java,我们还需要增加一个 SD 卡缓存类 DiskCache.java。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.xxt.xtest;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.text.TextUtils;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class DiskCache {
// 图片缓存 SD 卡目录
private static String CACHE_DIR =
Environment.getExternalStorageDirectory() + "/";
// 从 SD 卡缓存中获取图片
public Bitmap get(String url) {
return BitmapFactory.decodeFile(CACHE_DIR + urlToMd5(url));
}
// 将图片缓存到 SD 卡中
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(CACHE_DIR + urlToMd5(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();
}
}
}
}
private String urlToMd5(String url) {
if (TextUtils.isEmpty(url)) {
return "";
}
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(url.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(Integer.toHexString(b & 0xff));
}
hex.append(".png");
return hex.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("NoSuchAlgorithmException", e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UnsupportedEncodingException", e);
}
}
}

然后修改 ImageLoader.java 源码进行测试,可使用:

1
2
3
4
5
6
7
8
public class ImageLoader {
// 图片缓存
// private ImageCache mImageCache = new ImageCache();
private DiskCache mImageCache = new DiskCache();
...
}

接下来要实现的是:首先使用内存缓存,如果内存缓存没有图片再使用 SD 卡缓存,如果 SD 卡中也没有图片,最后才从网络上获取。

于是新建一个双缓存类 DoubleCache.java,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DoubleCache {
private ImageCache mMemoryCache = new ImageCache();
private 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 put(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
mDiskCache.put(url, bitmap);
}
}

虽然双缓存技术很优秀,但是我们最好提供 API,让使用者可以灵活选择缓存方式:只选内存缓存、只选 SD 卡缓存或者选择双缓存方式。而不合格的程序员则会提供如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ImageLoader {
// 图片缓存
private ImageCache mImageCache = new ImageCache();
// SD 卡缓存
private DiskCache mDiskCache = new DiskCache();
// 双缓存
private DoubleCache mDoubleCache = new DoubleCache();
// true: 单独使用 SD 卡缓存; false: 单独使用内存缓存
private boolean isUseDiskCache = false;
// true: 使用双缓存
private boolean isUseDoubleCache = false;
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);
}
...
}
...
public void useDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache;
}
public void useDoubleCache(boolean useDoubleCache) {
isUseDoubleCache = useDoubleCache;
}
}

上述代码中,要加入新的缓存实现时都需要修改 ImageLoader 类,然后通过一个布尔变量让用户选择使用哪种缓存。因此,就使得在 ImageLoader 中存在各种 if-else 判断语句,通过这些判断来确定使用哪种缓存。随着这些逻辑的引入,代码越来越复杂、脆弱。如果不小心写错了某个 if 条件,那就需要更多的时间来排除,整个 ImageLoader 类也会变得越来越臃肿。最重要的是,用户不能自己实现缓存注入到 ImageLoader 中,可扩展性差。

软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放——关闭原则。

也就是说,当软件需要变化时,我们应该尽量通过扩展的方式实现变化,而不是通过修改已有的代码来实现。根据这一个原则,我们可以画出如下所示的 UML 图。

按照上图进行以下重构:

提取抽象接口,用来抽象图片缓存的功能。其声明如下:

1
2
3
4
public interface ImageCache {
Bitmap get(String url);
void put(String url, Bitmap bitmap);
}

ImageCache 接口简单定义了获取、缓存图片两个函数,缓存的 key 是图片的 url,值是图片本身。内存缓存、SD 卡缓存、双缓存都实现了该接口,我们看看这几个缓存实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 内存缓存 MemoryCache 类
public class MemoryCache implements ImageCache {
private LruCache<String, Bitmap> mImageCache;
public MemoryCache() {
initImageCache();
}
private void initImageCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
return ImageUtil.getBitmapSize(value) / 1024;
}
};
}
public void put(@NonNull String url, @NonNull Bitmap bitmap) {
mImageCache.put(url, bitmap);
}
public Bitmap get(@NonNull String url) {
return mImageCache.get(url);
}
}
// SD 卡缓存 DiskCache 类
public class DiskCache implements ImageCache {
private static String CACHE_DIR =
Environment.getExternalStorageDirectory() + "/";
public Bitmap get(String url) {
return BitmapFactory.decodeFile(CACHE_DIR
+ ImageUtil.urlToMd5(url));
}
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(CACHE_DIR
+ ImageUtil.urlToMd5(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();
}
}
}
}
}
// 双缓存 DoubleCache 类
public class DoubleCache implements ImageCache {
private MemoryCache mMemoryCache = new MemoryCache();
private 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 put(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
mDiskCache.put(url, bitmap);
}
}

然后重构 ImageLoader,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ImageLoader {
// 图片缓存,默认内存缓存
private ImageCache mImageCache = new MemoryCache();
// 线程池,线程数量为 CPU 的数量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
// UI Handler
private Handler mUiHandler = new Handler(Looper.getMainLooper());
// 注入缓存实现
public void setImageCache(ImageCache cache) {
mImageCache = cache;
}
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 = ImageUtil.downloadImage(url);
if (bitmap == null) return;
if (url.equals(imageView.getTag())) {
updateImageView(imageView, bitmap);
}
mImageCache.put(url, bitmap);
}
});
}
// 通知界面更新显示图片
private void updateImageView(final ImageView imageView,
final Bitmap bitmap) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
}

附工具类 ImageUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class ImageUtil {
/**
* 下载图片
* @param imageUrl 图片链接
* @return Bitmap
*/
public static Bitmap downloadImage(String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
return null;
}
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
/**
* 计算图片大小
* @param bitmap 图片
* @return int
*/
public static int getBitmapSize(Bitmap bitmap) {
if (bitmap == null) {
return 0;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return bitmap.getAllocationByteCount();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
return bitmap.getRowBytes() * bitmap.getHeight();
}
/**
* 将图片 url 转换成字符串,用作文件名称
* @param url
* @return
*/
public static String urlToMd5(String url) {
if (TextUtils.isEmpty(url)) {
return "";
}
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(url.getBytes("UTF-8"));
StringBuilder builder = new StringBuilder();
for (byte b : bytes) {
builder.append(Integer.toHexString(b & 0xff));
}
builder.append(".png");
return builder.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("NoSuchAlgorithmException", e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UnsupportedEncodingException", e);
}
}
}

经过此次重构,没有了那么多的 if-else 语句,没有了各种各样的缓存实现对象、布尔变量,代码确实清晰简洁。用户可以通过setImageCache(ImageCache cache) 函数设置缓存实现,也就是通常说的依赖注入。具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ImageLoader imageLoader = new ImageLoader();
// 使用内存缓存
imageLoader.setImageCache(new MemoryCache());
// 使用 SD 卡缓存
imageLoader.setImageCache(new DiskCache());
// 使用双缓存
imageLoader.setImageCache(new DoubleCache());
// 使用自定义的图片缓存实现
imageLoader.setImageCache(new ImageCache() {
@Override
public Bitmap get(String url) {
return null; // 从缓存中获取图片
}
@Override
public void put(String url, Bitmap bitmap) {
// 缓存图片
}
});

在上述代码中,通过 setImageCache(ImageCache cache) 方法注入不同的缓存实现,这样不仅能够使 ImageLoader 更简单、健壮,也使得 ImageLoader 的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache 缓存图片的具体实现完全不一样,但是,它们的一个特点是,都实现了 ImageCache 接口。当用户需要自定义实现缓存策略时,只需要新建一个实现 ImageCache 接口的类,然后构造该类的对象,并且通过 setImageCache 函数注入到 ImageLoader 中,这样 ImageLoader 就实现了千变万化的缓存策略,且扩展这些缓存策略并不会导致 ImageLoader 类的修改。

开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。但是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。在开发过程中需要自己结合具体情况进行考量。


开闭原则概述


软件实体应当对扩展开放,对修改关闭 (Software entities should be open for extension, but closed for modification)。

开闭原则是面向对象设计中“可复用设计”的基石,是设计模式最基本的法则。其他五大设计原则和 23 种设计模式都可以看做是开闭原则的实现方法和手段。

说的通俗一点就是,已经开发好的软件实体(如类、模块、函数),在升级迭代引入新功能时,不应该修改已有的代码,而是在已有代码的基础上,添加新代码来实现。