開閉原則的英文全稱是Open Close Principle,縮寫是OCP,它是Java世界里最基礎(chǔ)的設計原則,它指導我們?nèi)绾谓⒁粋€穩(wěn)定的、靈活的系統(tǒng)。開閉原則的定義是:軟件中的對象(類、模塊、函數(shù)等)應該對于擴展是開放的,但是對于修改是封閉的。
勃蘭特-梅耶在1988年出版的《面向?qū)ο筌浖?gòu)造》一書中提出這一原則---開閉原則。這一想法認為,程序一旦開發(fā)完成,程序中一個類的實現(xiàn)只應該因錯誤而被修改,新的或者改變的特性應該通過新建不同的類實現(xiàn),新建的類可以通過繼承的方式來重用原類的代碼。顯然梅耶的定義提倡實現(xiàn)繼承,已存在的實現(xiàn)類對于修改時封閉的,但是新的類可以通過覆寫父類的接口應對變化。
引入其他緩存方式
說了這么多,下面還是以一個簡單示例來說明開閉原則:
在對ImageLoader進行了一次重構(gòu)之后的ImageLoader職責單一、結(jié)構(gòu)清晰,算是個不錯的開始。隨著用戶的增多,有些問題開始暴露出來:緩存系統(tǒng)是最讓大家吐槽的地方,通過內(nèi)存緩存解決了每次從網(wǎng)絡加載圖片的問題,但是,Android應用的內(nèi)存很有限,且具有易失性,即當應用重新啟動之后,原來已經(jīng)加載過的圖片將會丟失,這樣重啟之后及需要重新下載圖片!而這又會導致加載緩慢、耗費用戶流量的問題。針對以上問題考慮引入SD卡緩存,這樣下載過的圖片就會緩存到本地,即使重啟應用也不需要重新下載了,接下來開始實現(xiàn)SD卡緩存。
DiskCache.java類,將圖片緩存到SD卡中:
package com.deason.library.ocp;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* SD卡圖片緩存
* Created by liuguoquan on 2016/3/17.
*/
public class DiskCache {
public static final String CACHE_DIR = "/sdcard/cache/";
/**
* 從緩存中獲取圖片
* @param url
* @return
*/
public Bitmap get(String url) {
return BitmapFactory.decodeFile(CACHE_DIR + url);
}
/**
* 將圖片存入SD卡中
* @param url
* @param bitmap
*/
public void put(String url,Bitmap bitmap) {
FileOutputStream out = null;
try {
out = new FileOutputStream(CACHE_DIR + url);
bitmap.compress(Bitmap.CompressFormat.PNG,100,out);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
MemoryCache類,把圖片緩存到內(nèi)存中
package com.deason.library.ocp;
import android.graphics.Bitmap;
import android.util.LruCache;
/**
* 處理圖片緩存
* Created by liuguoquan on 2016/3/14.
*/
public class MemoryCache {
/**
* 圖片緩存
*/
LruCache<String, Bitmap> mImageCache;
public MemoryCache() {
initImageCache();
}
private void initImageCache() {
//計算可使用的最大內(nèi)存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//取內(nèi)存的四分之一作為緩存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
}
/**
* 將圖片存入緩存
* @param key
* @param bitmap
*/
public void put(String key,Bitmap bitmap) {
mImageCache.put(key,bitmap);
}
/**
* 取出緩存圖片
* @param key
* @return
*/
public Bitmap get(String key) {
return mImageCache.get(key);
}
}
因為需要將圖片緩存到SD卡中,所有ImageLoader代碼有所更新,具體代碼如下:
/**
* 圖片加載類
* Created by liuguoquan on 2016/3/14.
*/
public class ImageLoader {
/**
* 圖片緩存
*/
MemoryCache mMemoryCache = new MemoryCache();
/**
* SD卡緩存
*/
DiskCache mDiskCache = new DiskCache();
/**
* 是否使用SD卡緩存
*/
private boolean isUseDiskCache = false;
/**
* 線程池,線程數(shù)量未CPU的數(shù)量
*/
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors());
/**
* 顯示圖片
* @param url
* @param imageView
*/
public void displayImage(final String url, final ImageView imageView) {
//判斷使用哪種緩存
Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) : mMemoryCache.get(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return ;
}
//沒有緩存則提交線程池下載
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);
}
//將下載的圖片存入內(nèi)存
mMemoryCache.put(url,bitmap);
mDiskCache.put(url,bitmap);
}
});
}
/**
* 設置是否用SD卡緩存
* @param isUseDiskCache
*/
public void useDiskCache(boolean isUseDiskCache) {
this.isUseDiskCache = isUseDiskCache;
}
/**
* 下載圖片
* @param url
* @return
*/
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
int code = mConnection.getResponseCode();
if (200 == code) {
bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
}
mConnection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
從上述的代碼中可以看到,僅僅新增了一個DiskCache類和往ImageLoader類中加入了少量代碼就添加了SD卡緩存的功能,用戶可以通過useDiskCache方法來對使用哪種緩存進行設置,例如
ImageLoader mImageLoader = new ImageLoader();
//使用SD卡緩存
mImageLoader.useDiskCache(true);
//使用內(nèi)存緩存
mImageLoader.useDisCache(false);
通過useDiskCache方法來讓用戶設置不同的緩存這個思路是對的,但是會有些明顯的問題,就是使用內(nèi)存緩存時用戶不能使用SD卡緩存。類似地,使用SD卡緩存時用戶就不能使用內(nèi)存緩存。
實際上,用戶需要這兩種緩存的綜合,首先緩存優(yōu)先使用內(nèi)存緩存,如果內(nèi)存緩存沒有圖片再使用SD卡緩存,如果SD卡中也沒有圖片最后才從網(wǎng)絡上獲取,這才是最好的緩存策略。
接下來,我們繼續(xù)重構(gòu),新建一個雙緩沖類DoubleCache,具體代碼如下:
package com.deason.library.ocp;
import android.graphics.Bitmap;
/**
* 雙緩沖。獲取圖片時先從內(nèi)存中獲取,如果內(nèi)存中沒有緩存該圖片,再從SD卡中獲取
* Created by liuguoquan on 2016/3/17.
*/
public class DoubleCache {
MemoryCache mMemoryCache = new MemoryCache();
DiskCache mDiskCache = new DiskCache();
/**
* 先從內(nèi)存獲取圖片,沒有再從SD卡獲取
* @param url
* @return
*/
public Bitmap get(String url) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap == null) {
bitmap = mDiskCache.get(url);
}
return bitmap;
}
/**
* 將圖片緩存到內(nèi)存和SD卡中
* @param url
* @param bitmap
*/
public void put(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
mDiskCache.put(url, bitmap);
}
}
我們再來看看最新的ImageLoader類,代碼更新也不多:
/**
* 圖片加載類
* Created by liuguoquan on 2016/3/14.
*/
public class ImageLoader {
/**
* 圖片緩存
*/
MemoryCache mMemoryCache = new MemoryCache();
/**
* SD卡緩存
*/
DiskCache mDiskCache = new DiskCache();
//雙緩存
DoubleCache mDoubleCache = new DoubleCache();
/**
* 是否使用SD卡緩存
*/
private boolean isUseDiskCache = false;
//使用雙緩存
private boolean isUseDoubleCache = false;
/**
* 線程池,線程數(shù)量未CPU的數(shù)量
*/
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors());
/**
* 顯示圖片
* @param url
* @param imageView
*/
public void displayImage(final String url, final ImageView imageView) {
//判斷使用哪種緩存
Bitmap bitmap = null;
if (isUseDoubleCache) {
bitmap = mDoubleCache.get(url);
} else if (isUseDiskCache) {
bitmap = mDiskCache.get(url);
} else {
bitmap = mMemoryCache.get(url);
}
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return ;
}
//沒有緩存則提交線程池下載
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);
}
//將下載的圖片存入內(nèi)存
mMemoryCache.put(url,bitmap);
mDiskCache.put(url,bitmap);
}
});
}
/**
* 設置是否用SD卡緩存
* @param isUseDiskCache
*/
public void useDiskCache(boolean isUseDiskCache) {
this.isUseDiskCache = isUseDiskCache;
}
/**
* 設置是否使用雙緩存
* @param isUseDoubleCache
*/
public void useDoubleCache(boolean isUseDoubleCache) {
this.isUseDoubleCache = isUseDoubleCache;
}
/**
* 下載圖片
* @param url
* @return
*/
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
int code = mConnection.getResponseCode();
if (200 == code) {
bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
}
mConnection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
此時程序越來越靈活了,通過增加短短幾行代碼就能夠完成如此重要的功能。
人們總會把美好的事情講在前面,最后給你來個“但是”,我們先來分析一下上面的程序:每次在程序中加入新的緩存實現(xiàn)都需要修改ImageLoader類,然后通過一個布爾類型的變量來讓用戶選擇使用哪種緩存,因此,就使得ImageLoader中存在各種if-else判斷語句,通過這些判斷語句來確定使用哪種緩存。隨著大量邏輯的引用,代碼變得越來越復雜脆弱,如果一不小心寫錯某個if條件,勢必會花費大量的時間去調(diào)試程序,整個ImageLoader類也會顯得臃腫。最重要的是,用戶不能自己實現(xiàn)緩存注入到ImageLoader類中,可擴展性較差,而可擴展性是框架的最重要的特性之一。
注入方式設置緩存方式
“軟件中的對象(類、模塊、函數(shù)等)應該對于擴展是開放的,但是對于修改則是封閉的,這就是開放-關(guān)閉原則。也就是說,我們應該盡量通過擴展的方式來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)”。現(xiàn)在,我們來實現(xiàn)終極解決方案,遵循開閉原則。
首先,我們先畫出UML圖,如下

然后,實現(xiàn)UML圖上的類和接口,先重構(gòu)ImageLoader類:
/**
* Created by liuguoquan on 2016/3/17.
*/
public interface ImageCache {
/**
* 獲取圖片
* @param url
* @return
*/
public Bitmap get(String url);
/**
* 緩存圖片
* @param url
* @param bitmap
*/
public void put(String url, Bitmap bitmap);
}
經(jīng)過重構(gòu)后,少了if-else語句,沒有了各種各樣的緩存實現(xiàn)對象、布爾變量,代碼確實清晰、簡單了很多。需要注意的是,這次重構(gòu)的ImageCache把它提取成一個圖片緩存的接口,用來抽象圖片緩存的功能,我們來看看該接口的聲明:
/**
* 圖片加載類
* Created by liuguoquan on 2016/3/14.
*/
public class ImageLoader {
/**
* 圖片緩存
*/
ImageCache mImageCache = new MemoryCache();
/**
* 注入緩存實現(xiàn)
* @param cache
*/
public void setIamgeCache(ImageCache cache) {
mImageCache = cache;
}
/**
* 線程池,線程數(shù)量未CPU的數(shù)量
*/
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors());
/**
* 顯示圖片
* @param url
* @param imageView
*/
public void displayImage(final String url, final ImageView imageView) {
Bitmap bitmap = mImageCache.get(url);
if (bitmap != null) {
imageView.setImageBitmap(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)) {
imageView.setImageBitmap(bitmap);
}
//將下載的圖片存入內(nèi)存
mImageCache.put(url,bitmap);
}
});
}
/**
* 下載圖片
* @param url
* @return
*/
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
int code = mConnection.getResponseCode();
if (200 == code) {
bitmap = BitmapFactory.decodeStream(mConnection.getInputStream());
}
mConnection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
ImageCache接口簡單定義了獲取、緩存圖片兩個方法,緩存的可以是圖片的url,值是圖片的本身。內(nèi)存緩存、SD卡緩存、爽緩存都實現(xiàn)該接口,我們看看幾個緩存的實現(xiàn):
MemoryCache.java
public class MemoryCache implements ImageCache {
/**
* 圖片緩存
*/
LruCache<String, Bitmap> mMemoryCache;
public MemoryCache() {
initImageCache();
}
/**
* 初始化LRU緩存
*/
private void initImageCache() {
//計算可使用的最大內(nèi)存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//取內(nèi)存的四分之一作為緩存
final int cacheSize = maxMemory / 4;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
}
@Override
public Bitmap get(String url) {
return mMemoryCache.get(url);
}
@Override
public void put(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
}
}
DiskCache.java
/**
* Created by liuguoquan on 2016/3/17.
*/
public class DiskCache implements ImageCache {
public static final String CACHE_DIR = "/sdcard/cache/";
@Override
public Bitmap get(String url) {
return BitmapFactory.decodeFile(CACHE_DIR + url);
}
@Override
public void put(String url, Bitmap bitmap) {
FileOutputStream out = null;
try {
out = new FileOutputStream(CACHE_DIR + url);
bitmap.compress(Bitmap.CompressFormat.PNG,100,out);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Double.java
/**
* Created by liuguoquan on 2016/3/17.
*/
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);
}
}
上述重構(gòu)后的ImageLoader類中增加了一個setImageCache(ImageCache cache)方法,用戶可以通過該函數(shù)設置緩存實現(xiàn),也就是通常所說的依賴注入。
下面看看用戶如何通過使用ImageLoader來設置緩存實現(xiàn)的:
ImageLoader mImageLoader = new ImageLoader();
//使用內(nèi)存緩存
mImageLoader.setIamgeCache(new MemoryCache());
//使用SD卡緩存
mImageLoader.setIamgeCache(new DiskCache());
//使用雙緩存
mImageLoader.setIamgeCache(new DoubleCache());
//使用自定義的圖片緩存實現(xiàn)
mImageLoader.setIamgeCache(new ImageCache() {
@Override
public Bitmap get(String url) {
return null;
}
@Override
public void put(String url, Bitmap bitmap) {
}
});
在上述代碼中,通過setImageCache(ImageCache cache)方法注入不同的緩存實現(xiàn),這樣不僅能夠使ImageLoader更簡單、健壯,也使得ImageLoader可課擴展性、靈活性更高。
MemeryCache、DiskCache、DoubleCache緩存圖片的具體實現(xiàn)完全不一樣,但是,它們有一個特點是,都實現(xiàn)了ImageCache接口。當用戶需要自定義緩存實現(xiàn)時,只需要新建一個實現(xiàn)ImageCache接口的類,然后構(gòu)造該類的對象,并且通過setImageCache(ImageCache cache)注入到ImageLoader中就可以了,這樣ImageLoader就實現(xiàn)了千變?nèi)f化的緩存策略,且擴展這些緩存策略并不會導致ImageLoader類的修改。經(jīng)過這次重構(gòu),ImageLoader在設計模式上基本已經(jīng)合格了。
開閉原則指導我們,當軟件需要變化時,應該盡量通過擴展的方式來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)。這里的“應該盡量”4個字說明OCP原則并不是說絕對不可以修改原始類的。當確定原來的代碼已經(jīng)“腐化”時,應該盡早的重構(gòu),以便讓代碼恢復帶正常的狀態(tài),而不是通過繼承等方式添加新的代碼。因此,在開發(fā)過程中需要自己結(jié)合具體情況進行考量,是通過修改舊代碼還是通過繼承使得軟件系統(tǒng)更穩(wěn)當、更靈活,在保證去除“腐化代碼”的同時,也保證原有模塊的正確性。
優(yōu)點
- 增加穩(wěn)定性
- 可擴展性提高