寫(xiě)在前面#
我來(lái)晚了,遲到的祝福,祝各位同行們**中秋快樂(lè)! **
今天文章的主題是六大原則中的開(kāi)閉原則。
面向?qū)ο缶幊痰牧笤瓌t
讓程序像組裝機(jī)一樣穩(wěn)定靈活--開(kāi)閉原則
開(kāi)閉原則的全稱是Open Close Principle,簡(jiǎn)寫(xiě)是OCP,它是Java世界里組基礎(chǔ)的設(shè)計(jì)原則,它將會(huì)指導(dǎo)我們?nèi)绾谓⒁粋€(gè)穩(wěn)定并且靈活的系統(tǒng),讓我們的程序可以像組裝機(jī)一樣,可以更新,換配件,但不能修改配件。開(kāi)閉原則的定義是:軟件中的對(duì)象(類(lèi),模塊,函數(shù)等)對(duì)于拓展是開(kāi)放的,對(duì)于修改時(shí)封閉的。在我們開(kāi)發(fā)軟件的周期中,我們可能會(huì)因?yàn)橛脩粜枨蠡蛘呒軜?gòu)設(shè)計(jì)的變化而需要對(duì)原有代碼進(jìn)行大量修改,這時(shí)候很可能會(huì)將錯(cuò)誤引入原本已經(jīng)經(jīng)過(guò)測(cè)試的代碼之中,因而產(chǎn)生新的BUG影響開(kāi)發(fā)周期與開(kāi)發(fā)質(zhì)量,破壞原有的軟件設(shè)計(jì)和代碼完整性。此時(shí),我們應(yīng)該盡量通過(guò)拓展的方式去修改代碼實(shí)現(xiàn)我們要的功能,而不是在原有的基礎(chǔ)上去修改代碼。我們希望只通過(guò)繼承的方式去實(shí)現(xiàn)代碼的更新和修改,但這對(duì)于實(shí)際開(kāi)發(fā)而言其實(shí)只能是我們的一個(gè)愿景。因此,在開(kāi)發(fā)過(guò)程中,我們通常情況下是會(huì)通過(guò)修改原有代碼并且拓展代碼去實(shí)現(xiàn)更新和修改的功能。
那么,如何才能盡可能確保原有的軟件模塊完整性,正確性,并且盡量少地影響到原有代碼呢?答案就是:盡量遵守本章講述的開(kāi)閉原則。
開(kāi)始
上一章我們?cè)谥v述單一職責(zé)原則時(shí),通過(guò)ImageLoader的例子去體現(xiàn)了單一職責(zé)原則的好處,但是在代碼中我們只實(shí)現(xiàn)了內(nèi)存緩存,那么用戶在關(guān)閉APP后圖片依舊是需要重新加載的,出于對(duì)用戶的流量考慮,我們勢(shì)必要在代碼中進(jìn)行拓展,添加本地磁盤(pán)緩存功能,下面是新添加的DiskCache類(lèi)。
public class DiskCache {
private final static String cacheDir = "sdcard/cacheTest";
// 從磁盤(pán)獲取圖片
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
// 緩存圖片到內(nèi)存卡
public void put(String url,Bitmap bitmap){
// 創(chuàng)建輸出流
FileOutputStream outputStream = null;
try {
// 輸出流命名
outputStream = new FileOutputStream(url);
// 將傳進(jìn)來(lái)的bitmap轉(zhuǎn)為文件
// 參數(shù)一:轉(zhuǎn)換的圖片格式,默認(rèn)支持 JPEG,PNG,WEBP
// 參數(shù)二:轉(zhuǎn)換圖片質(zhì)量,100為最高
// 參數(shù)三:承接轉(zhuǎn)出文件的輸出流
bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
// 進(jìn)行判斷
if (outputStream!=null){
try {
// 關(guān)閉輸出流
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
為了將我們的DiskCache也就是我們的磁盤(pán)緩存添加進(jìn)我們的ImageLoader,我對(duì)ImageLoader進(jìn)行了一定更新,不熟悉之前代碼的同學(xué)請(qǐng)?zhí)D(zhuǎn)到第一章單一職責(zé)原則,修改后代碼如下:
public class ImageLoader {
// 圖片緩存
ImageCache mImageCache = new ImageCache();
//磁盤(pán)緩存
DiskCache mDiskCache = new DiskCache();
// 是否使用磁盤(pán)緩存
boolean isUsedDisk = false;
// 線程池,線程數(shù)為CPU所允許的數(shù)量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public void displayImg(final String url, final ImageView imageView) {
Bitmap bitmap = isUsedDisk ? mDiskCache.get(url) : mImageCache.get(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
// View中的setTag(object)表示給View添加一個(gè)格外的數(shù)據(jù),以后可以用getTag()將這個(gè)數(shù)據(jù)取出來(lái)。
imageView.setTag(url);
//在子線程中完成加載圖片和緩存圖片
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImg(url);
if (bitmap == null) return;
if (imageView.getTag().equals(url)) {
imageView.setImageBitmap(bitmap);
}
if (isUsedDisk) mDiskCache.put(url, bitmap);
else mImageCache.put(url,bitmap);
}
});
}
private Bitmap downloadImg(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
connection.disconnect();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
public void setDiskCacheEnable(boolean enable) {
this.isUsedDisk = enable;
}
}
從上述代碼中我們可以看到,添加了DiskCache后再修改了ImageLoader中的一丁點(diǎn)代碼即可達(dá)成磁盤(pán)緩存的功能,但是眼尖的同學(xué)會(huì)發(fā)現(xiàn),現(xiàn)在的ImageLoader有個(gè)非常大的弊端,就是使用內(nèi)存緩存時(shí)就無(wú)法使用磁盤(pán)緩存,使用磁盤(pán)緩存就無(wú)法使用內(nèi)存緩存。
我們所知的各種網(wǎng)絡(luò)圖片加載框架的正常工作流程應(yīng)該是:加載圖片時(shí)首先使用內(nèi)存緩存,如果內(nèi)存中沒(méi)有再去尋找SD卡中的緩存,如果SD卡中依然沒(méi)有,再?gòu)木W(wǎng)上獲取。這應(yīng)該是最好的緩存策略了,那么,我們的ImageLoader應(yīng)該怎么修改呢?我新建了一個(gè)類(lèi)DoubleCache,代碼如下:
public class DoubleCache {
ImageCache mMemoryCache = new ImageCache();
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 save(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
mDiskCache.put(url, bitmap);
}
}
同時(shí)修改了ImageLoader中的部分內(nèi)容
public class ImageLoader {
// 圖片緩存
ImageCache mImageCache = new ImageCache();
//磁盤(pán)緩存
DiskCache mDiskCache = new DiskCache();
//雙緩存
DoubleCache mDoubleCache = new DoubleCache();
// 是否使用磁盤(pán)緩存
boolean isUsedDisk = false;
// 是否使用雙緩存
boolean isUsedDoubleCache = false;
// 線程池,線程數(shù)為CPU所允許的數(shù)量
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public void displayImg(final String url, final ImageView imageView) {
Bitmap bitmap = null;
if (isUsedDoubleCache){
bitmap = mDoubleCache.get(url);
}else if (isUsedDisk){
bitmap = mDiskCache.get(url);
}else{
bitmap = mImageCache.get(url);
}
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
// 如若沒(méi)有則開(kāi)啟線程從網(wǎng)絡(luò)加載圖片
}
private Bitmap downloadImg(String imageUrl) {
// 從網(wǎng)絡(luò)加載圖片
}
public void setDiskCacheEnable(boolean enable) {
this.isUsedDisk = enable;
}
public void setmDoubleCache(DoubleCache enable) {
this.mDoubleCache = enable;
}
}
以上代碼實(shí)現(xiàn)了雙緩存功能了,但是從它修改了太多已有代碼,并且舊代碼與新代碼相互耦合,可讀性也很差,不管從維護(hù)角度或者說(shuō)安全性上來(lái)說(shuō),都是不適合的,而且這樣的圖片加載功能幾乎不提供給用戶任何自定義的空間,并且if判斷條件過(guò)多,一步錯(cuò)步步錯(cuò),那么,究竟要怎樣才能實(shí)現(xiàn)出我們需要的并且漂亮合格的代碼呢?這里的代碼進(jìn)行了大量修改。
- 圖片緩存接口
public interface ImageCache {
Bitmap get(String url);
void put(String url,Bitmap bitmap);
}
- MemoryCache 內(nèi)存緩存
public class MemoryCache implements ImageCache{
// 圖片緩存
private LruCache<String, Bitmap> mImageCache;
public MemoryCache() {
initImageCache();
}
private void initImageCache() {
// 獲取APP可使用的最大內(nèi)存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 獲取四分之一大小作為緩存空間
int cacheMemorySize = maxMemory / 4;
mImageCache = new LruCache<String, Bitmap>(cacheMemorySize) {
/**
* Sizeof方法的作用只要是定義緩存中每項(xiàng)的大小,當(dāng)我們緩存進(jìn)去一個(gè)數(shù)據(jù)后,
* 當(dāng)前已緩存的Size就會(huì)根據(jù)這個(gè)方法將當(dāng)前加進(jìn)來(lái)的數(shù)據(jù)也加上,便于統(tǒng)計(jì)當(dāng)
* 前使用了多少內(nèi)存,如果已使用的大小超過(guò)maxSize就會(huì)進(jìn)行清除動(dòng)作;
*/
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
@Override
public void put(String url, Bitmap bitmap) {
mImageCache.put(url, bitmap);
}
@Override
public Bitmap get(String url) {
return mImageCache.get(url);
}
}
- DiskCache 磁盤(pán)緩存
public class DiskCache implements ImageCache{
private final static String cacheDir = "sdcard/cacheTest";
// 從磁盤(pán)獲取圖片
@Override
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
// 緩存圖片到內(nèi)存卡
@Override
public void put(String url,Bitmap bitmap){
// 創(chuàng)建輸出流
FileOutputStream outputStream = null;
try {
// 輸出流命名
outputStream = new FileOutputStream(url);
// 將傳進(jìn)來(lái)的bitmap轉(zhuǎn)為文件
// 參數(shù)一:轉(zhuǎn)換的圖片格式,默認(rèn)支持 JPEG,PNG,WEBP
// 參數(shù)二:轉(zhuǎn)換圖片質(zhì)量,100為最高
// 參數(shù)三:承接轉(zhuǎn)出文件的輸出流
bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
// 進(jìn)行判斷
if (outputStream!=null){
try {
// 關(guān)閉輸出流
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- DoubleCache 雙緩存
public class DoubleCache implements ImageCache {
MemoryCache mMemoryCache = new MemoryCache();
DiskCache 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 圖片加載類(lèi)
public class ImageLoader {
// 圖片緩存
ImageCache mImageCache = new MemoryCache();
// 線程池,線程數(shù)為CPU可用數(shù)
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// 注入緩存實(shí)現(xiàn)
public void setmImageCache(ImageCache cache){
mImageCache = cache;
}
public void displayImage(String imageUrl,ImageView imageView){
Bitmap bitmap = mImageCache.get(imageUrl);
if (bitmap!=null) {
imageView.setImageBitmap(bitmap);
return;
}
//如果沒(méi)有緩存,開(kāi)始網(wǎng)絡(luò)獲取圖片的過(guò)程
submitLoadRequest(imageUrl,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)){
imageView.setImageBitmap(bitmap);
}
mImageCache.put(url,bitmap);
}
});
}
private Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
上述代碼中,我寫(xiě)了一個(gè)接口類(lèi)ImageCache去定義獲取和緩存兩個(gè)函數(shù),內(nèi)存緩存,磁盤(pán)緩存,雙緩存都實(shí)現(xiàn)了該接口。細(xì)心的朋友應(yīng)該可以看見(jiàn)在ImageLoader類(lèi)中多了一個(gè)方法
setmImageCache(ImageCache cache),通過(guò)此方法可以讓用戶選擇需要的緩存類(lèi)型甚至實(shí)現(xiàn)自定義的緩存,這樣就是通常說(shuō)的依賴注入。用戶可以通過(guò)如下代碼實(shí)現(xiàn)緩存設(shè)置:
//實(shí)例化
ImageLoader imageLoader = new ImageLoader();
//設(shè)置為磁盤(pán)緩存
imageLoader.setmImageCache(new DiskCache());
//設(shè)置為內(nèi)存緩存
imageLoader.setmImageCache(new MemoryCache());
//設(shè)置為雙緩存
imageLoader.setmImageCache(new DoubleCache());
//設(shè)置為自定義緩存
imageLoader.setmImageCache(new ImageCache() {
@Override
public Bitmap get(String url) {
return null;
}
@Override
public void put(String url, Bitmap bitmap) {
}
});
以上代碼就體現(xiàn)了本章的主題:開(kāi)閉原則。
開(kāi)閉原則指導(dǎo)我們,當(dāng)我們的軟件要發(fā)生變化時(shí),應(yīng)該盡量通過(guò)拓展的方式實(shí)現(xiàn)變化,而不是通過(guò)修改原有代碼的形式進(jìn)行。請(qǐng)注意此處加黑應(yīng)該盡量四字,在開(kāi)發(fā)過(guò)程中OCP原則并不能保證所有情況下都可以不修改原有代碼去實(shí)現(xiàn)修改。
而在開(kāi)發(fā)過(guò)程中,當(dāng)我們發(fā)現(xiàn)我們的代碼慢慢地耦合家中或者看起來(lái)雜亂無(wú)章,慢慢開(kāi)始變成臭不可聞的代碼時(shí),就應(yīng)該及時(shí)重構(gòu)代碼,以便我們的代碼能夠滿足軟件或者系統(tǒng)的迭代更新,而不是通過(guò)繼承的方式去無(wú)限制的添加新代碼,這樣會(huì)導(dǎo)致類(lèi)不斷變大并造成代碼冗余。
在實(shí)際開(kāi)發(fā)中需要自己結(jié)合實(shí)際情況進(jìn)行決策,保證代碼的穩(wěn)定性和靈活性,同時(shí)在保證我們的代碼是清新迷人的同時(shí),也要保證代碼的正確性。
寫(xiě)在結(jié)尾#
- 本章內(nèi)容示例代碼引用自《安卓源碼設(shè)計(jì)模式》(何紅輝,關(guān)愛(ài)名著)。
- 走向面向?qū)ο?六大原則將會(huì)一直使用ImageLoader進(jìn)行講解,不斷通過(guò)犯錯(cuò)和改錯(cuò)的過(guò)程中完善代碼。
- 作者很喜歡和大家一起討論代碼,討論各種新的框架和設(shè)計(jì)模式,希望大家踴躍留言相互交流,共同進(jìn)步。
- 謝謝。