面向对象六大原则:
- 单一职责原则
- 开闭原则
- 里氏替换原则
- 依赖倒置原则
- 接口隔离原则
- 最少知识原则
单一职责原则
单一职责原则 Single Responsibility Principle,就一个类而言,应该仅有一个引起它变化的原因,简单来说,一个类中应该是一组相关性很高的函数、数据的封装。
需求:实现图片加载,并将图片缓存起来。
书上代码有LruCache、ExecutorService两个我不太熟悉的内容。看了几篇博客,就差不多了解一些了。
- 彻底解析Android缓存机制——LruCache
- Java线程池 ExecutorService
- 多线程ExecutorService中submit和execute区别
下面的ImageLoader耦合严重,加载图片和图片缓存的逻辑写在一个类中,没有拓展性、灵活性。随着功能增多,ImageLoader类会越来越大,代码越来越复杂,图片加载系统就越来越脆弱。
public class ImageLoader {
//图片缓存
LruCache mImageCache;
//加载图片的线程池
//newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
//ui线程的handler 用来将图片加载进ImageView
Handler mUiHandler = new Handler(Looper.getMainLooper());
public ImageLoader() {
initImageCache();
}
/**
* 初始缓存
*/
private void initImageCache() {
//获取当前可用内存大小
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//四分之一用来缓存
final int cacheSize = maxMemory / 4;
//设置LruCache缓存大小,重写sizeOf方法 单位要一致
mImageCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
/**
* 全过程
* @param url 图片地址
* @param imageView view
*/
public void displayImage(final String url, final ImageView imageView) {
imageView.setTag(url);
//添加线程
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = mImageCache.get(url);
if (bitmap == null) {
bitmap = downloadImage(url);
if (bitmap == null) {
return;
}
//放入缓存
mImageCache.put(url, bitmap);
}
//判断防止过程中又对imageView做了下载其他url图片的请求
if (imageView.getTag().equals(url)) {
updateImageView(imageView, bitmap);
}
}
});
}
/**
* 图加载到view
* @param imageView view
* @param bitmap 图
*/
private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
/**
* 下载图片
* @param imageUrl 图片地址
* @return bitmap
*/
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
connection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
把上面的ImageLoader拆分一下,各个功能独立出来,分为两个类,ImageLoader只负责图片加载逻辑,ImageCache只负责处理图片缓存的逻辑。
public class ImageLoader {
ImageCache mImageCache = new ImageCache();
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Handler mUiHandler = new Handler(Looper.getMainLooper());
public void displayImage(final String url, final ImageView imageView) {
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = mImageCache.get(url);
if (bitmap == null) {
bitmap = downloadImage(url);
if (bitmap == null) {
return;
}
mImageCache.put(url, bitmap);
}
if (imageView.getTag().equals(url)) {
updateImageView(imageView, 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 connection = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
connection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
public class ImageCache {
LruCache mImageCache;
public ImageCache() {
initImageCache();
}
private void initImageCache() {
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
public void put(String url, Bitmap bitmap) {
mImageCache.put(url, bitmap);
}
public Bitmap get(String url) {
return mImageCache.get(url);
}
}
修改之后ImageLoader代码量变少了,指责也清晰了。当与缓存相关逻辑需要改变时,不需要修改ImageLoader类。图片加载逻辑需要改变时,不需要改变缓存类。
开闭原则
开闭原则Open Close Principle,软件中的对象(类、模块、函数等)应该对于拓展是开放的,但是,对于修改是封闭的。
如果因为需求的调整或其他原因需要修改原有代码,那就很有可能会向原先测试好的旧代码中引入错误,破坏原先系统。所以在需求改变时,应该先考虑使用拓展的方式实现变化,而不是修改已有代码来实现变化。
开闭原则就是,已存在的实现类对于修改是封闭的,新的实现类可以通过继承重写父类应对改变。
需求:引入SD卡缓存
之前实现只有内存缓存,现在需要引入SD卡缓存,将图片缓存到本地。
新增SD卡缓存类DiskCache
public class DiskCache {
static String cacheDir = "sdcard/cache/"
public Bitmap get (String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
修改ImageLoader类。增加了DiskCache对象,用一个isUseDiskCache来判断是否使用SD卡缓存。
public class ImageLoader {
ImageCache mImageCache = new ImageCache();
DiskCache mDiskCache = new DiskCache();
boolean isUseDiskCache = false;
...
public void displayImage(final String url, final ImageView imageView) {
...
Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) : mImageCache.get(url);
...
}
...
public void setUseDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache;
}
}
这只是增加了一个sd卡缓存的需求,如果在增加双缓存、用户可自定义缓存这样的需求,ImageLoader中就会出现各种缓存对象,并且displayImage()中还会判断到底使用哪一种方式。自定义缓存还不好实现。
可以发现每一种缓存策略都有共同的get()、put(),只是实现不同,那么就可以把它们抽象出来,抽象成一个ImageCache接口。
public interface ImageCache {
Bitmap get(String url);
void put(String url, Bitmap bitmap);
}
再去实现这个接口,创造不同的缓存策略实现类。
内存缓存实现类MemoryCache
public class MemoryCache implements ImageCache {
LruCache mImageCache;
public MemoryCache() {
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
@Override
public Bitmap get(String url) {
return mImageCache.get(url);
}
@Override
public void put(String url, Bitmap bitmap) {
mImageCache.put(url, bitmap);
}
}
本地缓存实现类DiskCache
public class DiskCache implements ImageCache{
static String cacheDir = "sdcard/cache/";
@Override
public Bitmap get (String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
@Override
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
双缓存实现类DoubleCache
public class DoubleCache implements ImageCache {
ImageCache mMemoryCache = new MemoryCache();
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 bitmap) {
mMemoryCache.put(url, bitmap);
mDiskCache.put(url, bitmap);
}
}
这样就可以去修改ImageLoader类了。只需要一个接口对象,使用不同的缓存策略时使用setImageCache()传入不同缓存的实现类。接口统一了缓存和获取缓存的方法,所以这里只用调用抽象方法就可以了。
public class ImageLoader {
ImageCache mImageCache = new MemoryCache();
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Handler mUiHandler = new Handler(Looper.getMainLooper());
//设置不同缓存对象
public void setImageCache(ImageCache imageCache) {
mImageCache = imageCache;
}
public void displayImage(final String url, final ImageView imageView) {
Bitmap bitmap = mImageCache.get(url);
if (bitmap != null) {//是否可以从缓存中拿到图片
updateImageView(imageView, 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 = downloadImage(url);
if (bitmap == null) {
return;
}
if (imageView.getTag().equals(url)) {
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);
}
});
}
//下载逻辑
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
connection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
再来看如何使用
final ImageLoader imageLoader = new ImageLoader();
imageLoader.setImageCache(new DoubleCache());//使用双缓存
imageLoader.setImageCache(new ImageCache() {//使用自定义缓存
@Override
public Bitmap get(String url) {
//具体实现
return null;
}
@Override
public void put(String url, Bitmap bitmap) {
//具体实现
}
});
imageLoader.displayImage(url, imageView);
这样一修改代码,如果之后还需要实现其他缓存策略,就不需要去修改ImageLoader了,现在的ImageLoder非常请。更不需要对原先的ImageCache或者其他缓存实现类做修改,只需要去实现ImageCache这个接口,加入新逻辑,就可以实现新需求了。
这就是开闭原则,哪怕有千变万化的缓存策略,也不需要去改变原有代码,而是实现接口,通过公用的setImageCache()注入ImageLoader。就遵循了尽量通过拓展的方式来实现变化,而不是通过修改已有代码来实现,保证了可拓展性。
里氏替换原则
里氏替换原则Liskov Substitution Principle,定义所有引用基类的地方必须能透明地使用其子类对象。
只要是父类能出现的地方子类都可以出现,而且替换为子类也不会产生任何错误和异常。里氏替换依赖于继承,继承是有两面性的:
优点:
- 代码重用,减少创建类的成本。
- 提高代码可拓展性。
缺点:
- 侵入性,必须拥有父类所有属性和方法。
- 可能造成子类代码冗余、灵活性降低。
在上述的图片加载代码修改中,其实就存在里氏替换原则,所有ImageCache出现的地方,都可以用MemoryCache、DiskCache、DoubleCache来替换,并且不会有错误。
里氏替换原则就建议通过建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的拓展性、灵活性。里氏替换原则保证了拓展性,从而达到开闭原则的对拓展开放、对修改关闭的效果,两个原则都强调面向对象的一个重要特性——抽象。
依赖倒置原则
依赖倒置原则Dependence Inversion Principle,指代了一种特定的解耦形式,使得高层模块不依赖于低层模块的实现细节。
关键点:
- 高层模块不用依赖底层模块,两者都应该依赖于抽象。
- 抽象不应该依赖于细节。
- 细节应该依赖抽象。
解释:
- 高层模块:调用端
- 低层模块:具体实现类
- 抽象:接口或抽象类
- 细节:实现类
如果类与类直接依赖于细节,那么当需求变化时,就意味着两个类都需要修改。就像最初的ImageLoader,ImageLoader直接依赖于ImageCache这个实现内存缓存的细节类,当需求增加需要实现既内存缓存又本地缓存时,就需要同时修改两个类了。而修改之后,ImageCache变为缓存的抽象接口,ImageLoader依赖于抽象的ImageCache,保证了依赖倒置原则,对应来看就是:
- ImageLoader和Cache实现类不直接依赖,而是两者都依赖于抽象的ImageCache接口。
- ImageCache不依赖于MemoryCache、DiskCache、DoubleCache。
- MemoryCache、DiskCache、DoubleCache依赖于ImageCache。
接口隔离原则
接口隔离原则Interface Segregation Princeple,类间的依赖关系应该建立在最小的接口上。
接口隔离原则将庞大的接口拆分成更小的更具体的接口,客户端只需要知道他们感兴趣的方法。接口隔离原则的目的是使系统解耦,从而更容易重构、更改和重新部署。
在DiskCache类中的put方法:
@Override
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
fileOuputStream在使用完后需要调用close()关闭资源,但是调用这个close()就必须嵌套一个try catch,降低了代码的可读性。
事实上close()方法属于Closeable接口的,很多类都实现了这个接口,FileOutputStream就属于其中之一。那么我们完全可以利用这个接口写一个工具类。
public class CloseUtil {
private CloseUtil() {}
public static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这时,我们的put()就可以变为如下。
@Override
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
CloseUtil.closeQuietly(fileOutputStream);
}
}
直接调用CloseUtil的closeQuietly()就可以了。
在使用过程中,fileOutputStream替换掉了closeQuietly()参数的Closeable接口,closeQuietly()依赖于抽象的Closeable接口而不是具体的FileOutputStream,也是遵循了依赖倒置原则。
CloseUtil的的closeQuietly()设计的最初目的是为了关闭fileOutputStream,它的关注点并不在FileOutputStream的整个设计上,而是只关注可关闭这一特点,也就是close()这一个方法。Closeable接口将流的关闭这一方法抽象出来,使得closeQuietly()只需要依赖Closeable这一个接口,而不需要去依赖整个FileOutputStream,从而降低了接口的使用难度,这就是接口隔离原则,使一个类依赖的接口尽可能的小。
回想前面ImageLoader的设计,它只需要知道缓存对象有可存可取的接口就可以了,其他的实现都是对ImageLoader隐藏的。
接口隔离原则用最小化接口隔离了实现类的细节,促使我们将庞大的接口拆分到更细粒度的接口当中去,使我们的系统具有更低的耦合性、更高的灵活性。
最少知识原则
最少知识原则Least Knowledge Principle,一个对象应该对其他对象有最少的了解。
一个类应该对自己需要耦合或调用的类知道得最少,类内部如何实现与调用者或者依赖者没关系,调用者或者依赖着只需要知道它需要的方法即可。类鱼类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
想要实现租客通过中介那里租房的逻辑,设计以下三个类。
public class Room {
private float area;
private float price;
public Room(float area, float price) {
this.area = area;
this.price = price;
}
public float getArea() {
return area;
}
public float getPrice() {
return price;
}
@Override
public String toString() {
return "Room{" +
"area=" + area +
", price=" + price +
'}';
}
}
public class Mediator {
private List mRooms = new ArrayList<>();
public Mediator() {
for (int i = 0; i < 5; i++) {
mRooms.add(new Room(20 + i, (20 + i) * 200));
}
}
public List getRooms() {
return mRooms;
}
}
public class Tenant {
private static final String TAG = "Tenant";
public void rentRoom(float area, float price, Mediator mediator) {
List rooms = mediator.getRooms();
for (Room room : rooms) {
if (isSuitable(area, price, room)) {
Log.d(TAG, "rentRoom: " + room.toString());
break;
}
}
}
private boolean isSuitable(float area, float price, Room room) {
return room.getArea() >= area && room.getPrice() <= price;
}
}
可以看到现在的设计中,三个类两两之间都存在依赖关系,Mediator依赖Room,Tenant依赖Mediator和Room,这样一来,如果需要改变Room类,Mediator和Tenant也都要跟着改变。
但是按照现实逻辑来说,租客是不需要依赖Room的,他只需要将自己的需求标准给中介就可以了,并不需要依赖Room并知道其中的成员细节。于是可以将代码改成这样。
public class Mediator {
private List mRooms = new ArrayList<>();
public Mediator() {
for (int i = 0; i < 5; i++) {
mRooms.add(new Room(20 + i, (20 + i) * 200));
}
}
private boolean isSuitable(float area, float price, Room room) {
return room.getArea() >= area && room.getPrice() <= price;
}
public Room rentOut(float area, float price) {
for (Room room : mRooms) {
if (isSuitable(area, price, room)) {
return room;
}
}
return null;
}
}
public class Tenant {
private static final String TAG = "Tenant";
public void rentRoom(float area, float price, Mediator mediator) {
Log.d(TAG, "rentRoom: " + mediator.rentOut(area, price).toString());
}
}
将筛选房的逻辑交给了中介,租客只需要向中介传达标准就可以了,并不依赖Room的内部成员,将Tenant和Room解耦了。
在ImageLoader示例中,用户并不会知道MemoryCache的内部实现,不知道内部使用了LruCache算法,只知道有ImageCache中的get()、put(),这样的话如果我们想要将LruCache替换为别的去实现内存缓存,用户也不会感觉到,和缓存相关的其他代码也不需要去改变。
最少知识原则让类暴露最少的内容给其他类,使得系统具有更低的耦合性和更好的扩展性。