设计模式之六大原则解析

Posted by Suzeyu on 2016-11-28

列举出设计模式中的六大原则应该如何实现

单一职责原则 SRP

Single Responsibility Principle

场景: 如果让你写出一个图片缓存类, 要求内部实现缓存策略, 并提供方法只需要传递控件图片地址就可以自动设置背景的类.

先给出最简单直接的写法.

public class ImageLoader {
/**
* 图片的缓存
*/
LruCache<String , Bitmap> mImageCache;
/**
* 线程池
*/
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public ImageLoader(){
// 初始化内存缓存策略
initImageCache();
}
private void initImageCache() {
// 获得可使用的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 设置 1/4 的最大内存为作为缓存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
// 返回缓存的bitmap大小
return value.getRowBytes() * value.getHeight() /1024 ;
}
};
}
public void displayImage(final ImageView iv, final String imgUrl){
// 获取缓存
Bitmap bitmap = mImageCache.get(imgUrl);
if(null != bitmap){
iv.setImageBitmap(bitmap);
return;
}
// 网络加载
iv.setTag(imgUrl);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(imgUrl);
if (null == bitmap)
return ;
if (iv.getTag().equals(imgUrl)) {
iv.setImageBitmap(bitmap);
}
mImageCache.put(imgUrl,bitmap);
}
});
}
/**
* 根据图片的url下载图片并转换成bitmap对象返回
*/
public Bitmap downloadImage(String url){
Bitmap bitmap = null;
try {
URL url1 = new URL(url);
HttpURLConnection conn = (HttpURLConnection) url1.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}
```
对于功能来说这是没有问题的. 但是接下来分析一下. 这样写会有什么弊端.
1. 首先这个类的职责包括了两个`图片缓存的逻辑`和`图片下载的逻辑`. 耦合性太强,
2. `类的职责过多`那么就会造成类中的代码更多,两个逻辑的代码会交叉分布. 不易被阅读.
3. `扩展性`随着后续的实现`磁盘缓存`修改`加载图片`逻辑等会让代码越来越复杂难懂.
那么重构一下, 让两个职责分离开来.
```java
/**
* 处理图片的加载
*/
public class ImageLoader {
/**
* 图片的缓存
*/
ImageCache mImageCache = new ImageCache();
/**
* 线程池
*/
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public void displayImage(final ImageView iv, final String imgUrl){
// 获取缓存
Bitmap bitmap = mImageCache.getCache(imgUrl);
if(null != bitmap){
iv.setImageBitmap(bitmap);
return;
}
// 网络加载
iv.setTag(imgUrl);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(imgUrl);
if (null == bitmap)
return ;
if (iv.getTag().equals(imgUrl)) {
iv.setImageBitmap(bitmap);
}
mImageCache.putCache(imgUrl,bitmap);
}
});
}
/**
* 根据图片的url下载图片并转换成bitmap对象返回
*/
public Bitmap downloadImage(String url){
Bitmap bitmap = null;
try {
URL url1 = new URL(url);
HttpURLConnection conn = (HttpURLConnection) url1.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}
/**
* 图片缓存逻辑处理类
*/
public class ImageCache {
/**
* 图片的缓存
*/
LruCache<String , Bitmap> mImageCache;
public ImageCache(){
// 初始化内存缓存策略
initImageCache();
}
private void initImageCache() {
// 获得可使用的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 设置 1/4 的最大内存为作为缓存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
// 返回缓存的bitmap大小
return value.getRowBytes() * value.getHeight() /1024 ;
}
};
}
/**
* 提供一个对 bitmap 进行缓存的方法
*/
public void putCache(String imgUrl, Bitmap bitmap){
mImageCache.put(imgUrl, bitmap);
}
/**
* 对外提供一个 获取缓存的方法
*/
public Bitmap getCache(String imgUrl){
return mImageCache.get(imgUrl);
}
}

将原类拆分为两个类之后, 每个类的职责变得清晰. 如果需要更改缓存策略那么只需要修改ImageCache类总逻辑. 如果需要替换HttpURLConnectionOKHttp那么只需要在ImageLoader类中修改. 这样充分了体现了一个类单一职责SRP的好处, 清晰, 排除了修改时的多余干扰代码.

开闭原则 OCP

全称Open Close Principle

定义: 对于对象应该对于扩展是开放的, 对于修改是封闭的. 就是在后续的功能添加的时候要尽可能做到不修改已存在的代码! 就是通过继承 接口的特性来实现的. 就是我们口中常说的面向接口编程

还是上面的代码, 这个时候我们如果想增添一个二级缓存,添加一个磁盘的缓存那么代码就如下了

/**
* 磁盘缓存
*/
public class DiskCache {
public static final String cacheDir = "sdcard/cache/";
// 从磁盘中获取缓存
public Bitmap getCache(String imgUrl){
return BitmapFactory.decodeFile(cacheDir+imgUrl);
}
// 向磁盘中缓存
public void putCache(String imgUrl, Bitmap bitmap){
try(FileOutputStream fileOutputStream = new FileOutputStream(cacheDir+imgUrl)){
// 对bitmap压缩到本地文件
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 还需要对ImageLoader类进行部分修改 只贴出添加的代码
/**
* 处理图片的加载
*/
public class ImageLoader {
/**
* 磁盘缓存
*/
DiskCache mDiskCache = new DiskCache();
/**
* 设置一个标记表示是否开启磁盘缓存
*/
public boolean isUseDiskCache = false;
public void displayImage(final ImageView iv, final String imgUrl){
// 获取缓存
Bitmap bitmap = mImageCache.getCache(imgUrl);
if(null != bitmap){
iv.setImageBitmap(bitmap);
return;
}
// 判断磁盘缓存
if (isUseDiskCache){
bitmap = mDiskCache.getCache(imgUrl);
if (null != bitmap)
return;
}
// 省略相同的网络下载代码...
}
public void setUseDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache;
}
}

至此初步的添加功能需求已经完成, 增加了一个DiskCache类实现磁盘缓存, 并在ImageLoader类中进行判断使用的代码.

分析: 现在的加载可以有两种.一种是只使用LruCache缓存, 另一种是实现两种缓存同时实现. 那么这样的代码会有怎样的问题?

  • 问题1: 如果想实现单磁盘缓存? 那么必须再对ImageLoader进行修改. 添加判断条件. 并且三种缓存策略if的判断也就比较多.
  • 问题2: 如果用户想实现自定义缓存? 呵呵哒. 目前的代码没这么厉害的扩展性…

那么看一下UML类图

可以看出只要添加一个缓存策略就要建立一个依赖关系.

那么如果通过接口的方式来修改代码呢. 看一下…

/**
* 缓存接口
*/
public interface BaseCache {
// 添加缓存的抽象方法
void putCache(String imgUrl, Bitmap bitmap);
// 获取缓存的抽象方法
Bitmap getCache(String imgUrl);
}
/**
* 双缓存
*/
public class DoubleCache implements BaseCache {
BaseCache mMemoryCache = new ImageCache();
BaseCache mDiskCache = new DiskCache();
@Override
public void putCache(String imgUrl, Bitmap bitmap) {
// 缓存
mMemoryCache.putCache(imgUrl, bitmap);
mDiskCache.putCache(imgUrl, bitmap);
}
@Override
public Bitmap getCache(String imgUrl) {
// 先从缓存取没有再从sd取
Bitmap cache = mMemoryCache.getCache(imgUrl);
if (null == cache){
cache = mDiskCache.getCache(imgUrl);
}
return cache;
}
}
/**
* 图片缓存逻辑处理类
*/
public class ImageCache implements BaseCache{
/**
* 图片的缓存
*/
LruCache<String , Bitmap> mImageCache;
public ImageCache(){
// 初始化内存缓存策略
initImageCache();
}
private void initImageCache() {
// 获得可使用的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 设置 1/4 的最大内存为作为缓存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
// 返回缓存的bitmap大小
return value.getRowBytes() * value.getHeight() /1024 ;
}
};
}
/**
* 提供一个对 bitmap 进行缓存的方法
*/
@Override
public void putCache(String imgUrl, Bitmap bitmap){
mImageCache.put(imgUrl, bitmap);
}
/**
* 对外提供一个 获取缓存的方法
*/
@Override
public Bitmap getCache(String imgUrl){
return mImageCache.get(imgUrl);
}
}
/**
* 处理图片的加载
*/
public class ImageLoader {
/**
* 图片的缓存 默认只是内存缓存
*/
BaseCache mImageCache = new ImageCache();
/**
* 注入缓存策略
*/
public void setmImageCache(BaseCache mImageCache) {
this.mImageCache = mImageCache;
}
/**
* 线程池
*/
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public void displayImage(final ImageView iv, final String imgUrl){
// 获取缓存 具体的缓存策略实现了依赖注入. 有调用者后续决定, 默认内存缓存
Bitmap bitmap = mImageCache.getCache(imgUrl);
if(null != bitmap){
iv.setImageBitmap(bitmap);
return;
}
// 网络加载
iv.setTag(imgUrl);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(imgUrl);
if (null == bitmap)
return ;
if (iv.getTag().equals(imgUrl)) {
iv.setImageBitmap(bitmap);
}
mImageCache.putCache(imgUrl,bitmap);
}
});
}
/**
* 根据图片的url下载图片并转换成bitmap对象返回
*/
public Bitmap downloadImage(String url){
Bitmap bitmap = null;
try {
URL url1 = new URL(url);
HttpURLConnection conn = (HttpURLConnection) url1.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}

上面代码没有单磁盘缓存的策略, 但是如果现在想要实现了这个功能, 怎么做? 只需要创建一个类实现BaseCache. 在创建ImageLoader的时候通过setmImageCache()来注入不同的实现. 不需要修改源代码, 并且ImageLoader中的if语句判断和布尔标记也完全不需要, 代码更加简洁.

看一下类图:

可以看到ImageLoader依赖了接口编程. 接口定义了缓存的共性方法. 在后续只要是其子类就可以使用.这不正满足了开闭原则的定义, 对修改封闭, 对于扩展开放了.

里氏替换原则 LSP

全称Liskov Substitution Principle

定义: 所有引用基类的地方必须能透明的使用其子类.

说白了就是Java中的继承多态的特性. 父类可以直接引用子类类型. 比如Object可以引用任何类型的概念.

其实在上面的开闭原则中就已经存在了里氏替换

/**
* 注入缓存策略
*/
public void setmImageCache(BaseCache mImageCache) {
this.mImageCache = mImageCache;
}

参数接收的BaseCache类型. 可以透明的引用任何的子类. 通过抽象实现了多种可能.

一般来说开闭原则里氏替换是不离不弃, 生死相依的. 通过里氏替换达到了对扩展的开发, 对修改封闭的效果. 这两个原则同时实现了一个OOP的一个重要特性抽象

依赖倒置原则 DIP

全称:Dependence Inversion Principle

定义: 指代了一种特定的解耦方式, 使得高层次的模块不依赖于低层次的模块实现细节.

  • 高层次模块不应该依赖低层次, 两者都应该依赖其抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象

而在Java中的表现就是: 模块间的依赖通过抽象发生, 实现类之间不发生直接的依赖关系, 其依赖关系是通过接口或抽象产生的

直接看代码, 还是上面的ImageLoader类中

// 依赖于接口抽象,
BaseCache mImageCache = new ImageCache();
// 依赖于细节, 内存缓存
ImageCache mImageCache = new ImageCache();
// 依赖于细节, 双缓存
DoubleCache mImageCache = new DoubleCache();

如果依赖了内存缓存细节, 那么注入的缓存策略必须是ImageCache的子类. 但是这个子类已经具备一个细节的实现, 我们再去做其他细节的实现. 岂不是很怪异. 这个类中的出现的方法也是匪夷所思. 并且可能用户实现的具体策略也不一定是内存方法的缓存. 在命名上的限制也是很不友好.

总结一句话: 依赖抽象, 而不依赖具体实现

接口隔离原则 ISP

全称: Interface Segregation Principle

定义: 类间的依赖关系应该建立在最小的接口上.

接口隔离的原则就是让系统接口耦合, 从而更容易重构, 更改和重新部署.

对于操作IO或者网络我们总是需要在finally中确保资源的释放. 如下

FileOutputStream fileOutputStream = null;
try{
fileOutputStream = new FileOutputStream(cacheDir+imgUrl);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
}catch (IOException e) {
e.printStackTrace();
}finally {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}

这是多么蛋疼的代码… 一堆堆的花括号.

对于可关闭的对象, 都具备一个接口Closeable. 这个接口一个空接口, 也就是标识接口. 作用? 就是标识这个对象可以调用close()被关闭. 体现了一类对象的某一个特性. 而且这个特性建立在了最小接口的原则上.

那么编写这么一段代码

public class CloseUtils {
// 整个方法通过最小接口的特性, 实现了隔离其他无用的属性. 只关心
// Closeable接口即可. 接口隔离
// 参数 对应了里氏替换的原则
public static void fastClose(Closeable closeObj){
if (null != closeObj){
try {
// close()方法的调用, 对应了 依赖倒置原则
closeObj.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

之前的代码调用就变成了

FileOutputStream fileOutputStream = null;
try{
fileOutputStream = new FileOutputStream(cacheDir+imgUrl);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
}catch (IOException e) {
e.printStackTrace();
}finally {
CloseUtils.fastClose(fileOutputStream);
}

不仅简单, 而且方便到处调用, 看起来也舒服多了.

迪米特原则 LOD

Law of Demeter

就是一个对象应该尽可能少的关联其他对象.

通俗讲, 一个类应该对自己需要耦合或调用的类知道的最少, 类的内部如何实现与调用者或者依赖者没有关系, 调用者或者依赖者只需要知道他需要的方法即可, 其他的一概不关心. 因为类与类之间的关系越密切, 耦合度也就越大, 当一个类发生改变时, 对另一个类的影响也会变大.

太懒了, 无具体实现…