设计模式之六大原则解析
列举出设计模式中的六大原则应该如何实现
单一职责原则 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;
}
}
对于功能来说这是没有问题的. 但是接下来分析一下. 这样写会有什么弊端.
- 首先这个类的职责包括了两个
图片缓存的逻辑
和图片下载的逻辑
. 耦合性太强, 类的职责过多
那么就会造成类中的代码更多,两个逻辑的代码会交叉分布. 不易被阅读.扩展性
随着后续的实现磁盘缓存
修改加载图片
逻辑等会让代码越来越复杂难懂.
那么重构一下, 让两个职责分离开来.
/**
* 处理图片的加载
*/
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
类总逻辑. 如果需要替换HttpURLConnection
为OKHttp
那么只需要在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
就是一个对象应该尽可能少的关联其他对象.
通俗讲, 一个类应该对自己需要耦合或调用的类知道的最少
, 类的内部如何实现与调用者或者依赖者没有关系, 调用者或者依赖者只需要知道他需要的方法即可, 其他的一概不关心. 因为类与类之间的关系越密切, 耦合度也就越大, 当一个类发生改变时, 对另一个类的影响也会变大.
太懒了, 无具体实现…