1. 单一职责原则(Single Responsibility Principle)
对一个类而言,应该仅有一个引起它变化的原因,……,一个类应该是一组相关性很高的函数、数据的封装。……。这是一个备受争议却有极其重要的原则,……,需要靠个人的经验来界定。
1.1. 违反单一职责原则的例子
public class ImageLoader {
// 图片缓存
LruCache<String, Bitmap> mImageCache;
public ImageLoader() {
initImageCache();
}
/**
* 初始化图片缓存
*/
private void initImageCache() {
// ……
}
/**
* 显示图片
* @param url 图片地址
* @param imageView ImageView
*/
public void displayImage(final String url, final ImageView imageView) {
// ……
}
/**
* 下载图片
* @param imageUrl 图片地址
* @return 图片 Bitmap
*/
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
// ……
return bitmap;
}
}
这是一个简单的 ImageLoader
实现,其中包含了缓存相关的逻辑,根据单一职责原则的要求,这是不合理的,应该讲缓存初始化等逻辑独立至一个图片缓存类中,在 ImageLoader
中,只保留与图片加载相关的逻辑代码。
1.2. 修改之后的代码
/**
* 图片加载类
*/
public class ImageLoader {
// 图片缓存
ImageCache mImageCache = new ImageCache();
public ImageLoader() {
}
/**
* 显示图片
* @param url 图片地址
* @param imageView ImageView
*/
public void displayImage(final String url, final ImageView imageView) {
// ……
}
/**
* 下载图片
* @param imageUrl 图片地址
* @return 图片 Bitmap
*/
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
// ……
return bitmap;
}
}
/**
* 图片缓存类
*/
public class ImageCache {
// LRU 缓存
LruCache<String, Bitmap> mImageCache;
public ImageCache() {
initImageCache();
}
/**
* 初始化图片缓存
*/
private void initImageCache() {
// ……
}
public void put(String url, Bitmap bitmap) {
mImageCache.put(url, bitmap);
}
public Bitmap get(String url) {
return mImageCache.get(url);
}
}
修改后的代码将上面例子中的类拆分为了两个类 ImageLoader
和 ImageCache
,ImageLoader
只负责图片加载相关的逻辑,ImageCache
则负责图片缓存,使得类职责分明。
2. 开闭原则(Open Close Principle)
软件中的对象(类、模块、函数等)应该对扩展是开放的,但是,对于修改是封闭的。……。当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
2.1. 违反开闭原则的例子
继续拿上面的 ImageLoader
举例,根据上述的代码,我们目前只进行了内存缓存,如果我们需要添加一种缓存方式,支持缓存至 SD 卡中,那我们就需要添加一个 DiskCache
类,实现 SD 卡缓存逻辑,并且修改 ImageLoader
的代码,将其中的 ImageCache
替换为 DiskCache
,如果两个缓存类提供的方法不同的话,我们还需要去修改方法调用处,这就违反了开闭原则,对于这种后期的需求更改,我们不应该通过修改已有代码达到目的,而是应该在不修改原有代码的情况下进行扩展,而在现有的代码结构下,我们无法做到不修改原有代码来实现新需求。
2.2. 修改代码使其符合开闭原则
public interface ImageCache {
Bitmap get(String url);
void put(String url, Bitmap bmp);
}
public class MemoryCache implements ImageCache {
private LruCache<String, Bitmap> mMemoryCache;
public MemoryCache() {
}
@Override
public Bitmap get(String url) {
return mMemoryCache.get(url);
}
@Override
public void put(String url, Bitmap bmp) {
mMemoryCache.put(url, bmp);
}
}
public class ImageLoader {
/** 图片缓存 */
private ImageCache mImageCache = new MemoryCache();
/** 线程池,线程数量为 CPU 的数量 */
private ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public void setImageCache(ImageCache imageCache) {
mImageCache = imageCache;
}
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.execute(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(imageUrl);
if (bitmap == null) {
return;
}
if (imageView.getTag().equals(imageUrl)) {
imageView.setImageBitmap(bitmap);
}
mImageCache.put(imageUrl, bitmap);
}
});
}
private Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try{
URL url = new URL(imageUrl);
final URLConnection conn = url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
在修改代码之后,我们主要关注 ImageLoader
中的 mImageCache
属性,它是一个 ImageCache
引用,而 ImageCache
是一个 Interface,引入接口是实现开闭原则的关键之一,根据多态的特性,mImageCache
可以指向任何一个实现了 ImageCache
的类的对象,这里指向了一个 MemoryCache
对象。在 ImageLoader
中同时提供了一个 setter 方法 setImageCache
,可以通过该方法来注入 ImageCache
,当我们完成这两点,ImageLoader
类也就符合了开闭原则,当我们需要实现上述的需求,即更换缓存方式为缓存至 SD 卡或者同时使用两种缓存方式时,我们不需要对 ImageLoader
类做任何修改,仅仅需要创建一个新的实现了 ImageCache
接口的类,再通过 ImageLoader
的 setImageCache
方法注入即可。
/**
* SD 卡缓存类,实现了 ImageCache 接口
*/
public class DiskCache implements ImageCache {
@Override
public Bitmap get(String url) {
return null; // TODO: 2017/10/25 从本地文件中获取图片
}
@Override
public void put(String url, Bitmap bmp) {
// TODO: 2017/10/25 将 Bitmap 写入文件
}
}
/**
* 双缓存类,同样实现了 ImageCache 接口
*/
public class DoubleCache implements ImageCache {
private ImageCache mMemoryCache = new MemoryCache();
private ImageCache 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 bmp) {
mMemoryCache.put(url, bmp);
mDiskCache.put(url, bmp);
}
}
// 换用不同缓存方式的方法
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 bmp) {
}
});
3. 里氏替换原则(Liskov Substitution Principle)
所有引用基类的地方必须能透明地使用其子类的对象。……。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换达到对扩展开放,对修改关闭的效果。这两个原则都同时强调了一个 OOP 的重要特性——封装。
4. 依赖倒置原则(Dependence Inversion Principle)
依赖倒置原则指代了一种特定的解耦方式,使得高层次的模块不依赖于低层次的模块的实现细节。
有以下几个关键点:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
模块间到的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
面向接口编程或者说是面向抽象编程,也就是上两个原则中说到的抽象。
依赖倒置原则说的其实就是上面提到的 ImageLoader
不应该去依赖 ImageCache
的具体实现类,比如 MemoryCache
或者 DiskCache
,而应该是通过接口或抽象来发生依赖关系,也就是后来加的 ImageCache
接口。
5. 接口隔离原则(Interface Segregation Principles)
客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成更小和更具体的接口,这样客户将会只需要知道他们感兴趣的接口。
接口隔离原则的含义其实就是将接口拆分的尽可能小,这样以便于重构。书中以 JDK 中的 Closable
接口举例,这个接口仅定义了一个 close
方法,所有 Closable
接口的实现类,例如 FileOutputStream
在调用 close
方法时都需要 catch IOException
,于是书中写了一个工具类来解决这一问题。工具类代码如下:
public class CloseUtils {
private CloseUtils() {}
/**
* 关闭 Closable 对象
* @param closeable
*/
public static void closeQuietly(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
讲这个的目的其实是更好的帮助我们理解接口隔离原则,当把接口变得更小更具体之后,系统就有了更高的灵活性。
6. 迪米特原则(Law of Demeter,也称为最少知识原则 Least Knowledge Principle)
一个对象应该对其他对象有最少的了解。通俗的讲,一个类应该对自己需要耦合或调用的类知道的最少,类的内部如何实现与调用者或者依赖者没关系,只需要知道它需要的方法即可。
其实迪米特原则说的和上面的原则也有点类似,首先就是要依赖抽象,作为依赖者,不需要知道方法是如何实现的,只需要知道依赖的对象有这个方法就好了,实现上的改变对它来说其实是不可见的。另一点就是尽可能减少依赖,书中以通过中介租房举例,作为租户,只需要依赖于中介即可,至于房东的房产证是不是真的这些细节不需要租户知道太多,所有的事情通过与中介沟通。