Java 中面向對象編程六大原則:
單一職責原則 英文名稱是Single Responsibility Principle,簡稱SRP
開閉原則 英文全稱是Open Close Principle,簡稱OCP
里氏替換原則 英文全稱是Liskov Substitution Principle,簡稱LSP
依賴倒置原則 英文全稱是Dependence Inversion Principle,簡稱DIP
接口隔離原則 英文全稱是InterfaceSegregation Principles,簡稱ISP
迪米特原則 英文全稱爲Law of Demeter,簡稱LOD,也稱爲最少知識原則(Least Knowledge Principle)
讓程序更穩定、更靈活——開閉原則
開閉原則的英文全稱是Open Close Principle,簡稱OCP,它是Java世界裏最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟件中的對象(類、模塊、函數等)應該對於擴展是開放的,但是,對於修改是封閉的。軟件開發過程中,最不會變化的就是變化本身。產品需要不斷地升級、維護,沒有一個產品從第一版本開發完就再沒有變化了,如果因爲變化、升級和維護等原因需要對軟件原有代碼進行修改時,這就可能會將錯誤引入原本已經經過測試的舊代碼中,破壞原有系統。因此,當軟件需要變化時,我們應該儘量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。那麼如何確保原有軟件模塊的正確性,以及儘量少地影響原有模塊,答案就是儘量遵守本章要講述的開閉原則
繼續上一章對ImageLoader進行分析,我們在第一輪重構之後的ImageLoader使用了單一原則,讓代碼職責單一、結構清晰,但是我們在使用中發現我們只實現了內存緩存圖片,通過內存緩存解決了每次從網絡加載圖片的問題,但是,Android應用的內存很有限,且具有易失性,即當app重新啓動之後,原來已經加載過的圖片將會丟失,這樣重啓之後就又需要重新下載。這又會導致加載緩慢、耗費用戶流量的問題。所以我們應該引入SD卡緩存,這樣下載過的圖片就會緩存到本地,即使重啓應用也不需要重新下載了。
根據上一章的單一原則,我們寫一個DiskCache.java類,將圖片緩存到SD卡中:
public class DiskCache {
private String mCacheDirName = "imageCache";
private String mCacheDirPath;
private File mCachDir = null;
public DiskCache(Context context){
initDiskCacheDir(context);
}
//初始化SD卡緩存的目錄位置
public void initDiskCacheDir(Context context) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
//獲取外部存儲卡的位置,/sdcard/Android/data/xxxx(應用包名)/cache
cachePath = context.getExternalCacheDir().getPath();
} else {
//如果無法獲取外部存儲卡的位置,則使用應用自己的私用空間,/data/data/xxxx(應用包名)/cache
cachePath = context.getCacheDir().getPath();
}
mCacheDirPath = cachePath+File.separator+mCacheDirName;
mCachDir = new File(mCacheDirPath);
if(!mCachDir.exists()){
mCachDir.mkdirs();
}
}
//將下載的圖片流緩存到sd卡里
public void put(String key, InputStream inputStream){
if(!mCachDir.exists()){
return;
}
//對傳入的key值進行MD5處理
String savedFilePath = mCacheDirPath+File.separator+hashKeyForDisk(key);
try {
FileOutputStream f = new FileOutputStream(new File(savedFilePath));
BufferedInputStream in = new BufferedInputStream(inputStream);
BufferedOutputStream out = new BufferedOutputStream(f);
int n;
byte[] buf = new byte[4096];
while ((n = in.read(buf)) != -1) {
out.write(buf,0,n);
}
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//根據傳入的key值返回緩存的文件流
public InputStream get(String key){
if(!mCachDir.exists()){
return null;
}
String savedFilePath = mCacheDirPath+File.separator+hashKeyForDisk(key);
File bitmapFile = new File(savedFilePath);
if(bitmapFile.exists()){
try {
InputStream in = new FileInputStream(bitmapFile);
return in;
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
}
return null;
}
/**
* 將Key用MD5碼編碼
* @param key
* @return 返回MD5碼後的編碼
*/
public String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
}
然後我們修改ImageLoader類加入我們的DiskCache:
public void setDiskCahe(Context context){
mDiskCache = new DiskCache(context);
}
再修改下載和顯示邏輯,加入判斷SD卡緩存裏有沒有緩存過要顯示的圖片,有緩存則直接取緩存顯示,避免再下載
public void displayImage(final String url, final ImageView imageView) {
imageView.setTag(url);
//先從cache中取圖片
if(mMemImageCache.get(url)!=null){
imageView.setImageBitmap(mMemImageCache.get(url));
return;
}
if(mDiskCache != null){
InputStream in = mDiskCache.get(url);
if(in != null){
Bitmap bitmap = BitmapFactory.decodeStream(in);
imageView.setImageBitmap(bitmap);
return;
}
}
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if (bitmap == null) {
return;
}
if (imageView.getTag().equals(url)) {
Message msg = mHandler.obtainMessage();
imageView.setTag(bitmap);
msg.obj = imageView;
mHandler.sendMessage(msg);
}
mMemImageCache.put(url, bitmap);
}
});
}
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setDoInput(true); //允許輸入流,即允許下載
conn.setUseCaches(false); //不使用緩衝
conn.setRequestMethod("GET"); //使用get請求
InputStream is = conn.getInputStream(); //獲取輸入流,此時才真正建立鏈接
if(mDiskCache!=null){ //緩存到硬盤上
mDiskCache.put(imageUrl,is);
}
bitmap = BitmapFactory.decodeStream(mDiskCache.get(imageUrl));
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
通過上面的修改,現在我們只需在使用ImageLoader時,調用一下setDiskCahe(Context context)方法就能使用SD卡緩存圖片,這是不是很方便,但是我們在添加一個DiskCache緩存時修改了許多ImageLoader代碼,如果我們需要更多緩存方式或更多緩存策略,比如只使用內存緩存,或內存緩存和SD卡緩存同時使用等等需求,每次加新的緩存方法時都要修改原來的代碼,這樣很可能會引入Bug,而且會使原來的代碼邏輯變得越來越複雜,按照上面這樣的方法實現,用戶也不能自定義緩存實現。
軟件中的對象(類、模塊、函數等)應該對於擴展是開放的,但是對於修改是封閉的,這就是開放-關閉原則。也就是說,當軟件需要變化時,我們應該儘量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。如何實現ImageLoader類需要的cache能擴展呢?我們可以定義一個ImageCache接口:
public interface ImageCache {
void put(String key, Bitmap bitmap);
Bitmap get(String key);
}
然後所有的具體Cache類實現這個接口,而我們的ImageLoader類只需操作這個ImageCache類就可以了.這就實現了用抽象類或接口來實現原功能的擴展。下面看一下UML圖:
如上圖,所有具體的Cache類實現了ImageCahe這個接口,而ImageLoader類只依賴於ImageCache類,這樣就達到了可以不修改原有代碼,而可以無限通過實現ImageCache這個接口來擴展ImageLoader類的緩存圖片功能了。
於是ImageLoader類裏增加設置ImageCache:
public void setImageCache(ImageCache cache){
mImageCache = cache;
}
通過setImageCache(ImageCache cache)方法注入不同的緩存實現,這樣不僅能夠使ImageLoader更簡單、健壯,也使得ImageLoader的可擴展性、靈活性更高。MemoryCache、DiskCache、DoubleCache緩存圖片的具體實現完全不一樣,但是,它們的一個特點是都實現了ImageCache接口。當用戶需要自定義實現緩存策略時,只需要新建一個實現ImageCache接口的類,然後構造該類的對象,並且通setImageCache(ImageCache cache)注入到ImageLoader中,這樣ImageLoader就實現了變化萬千的緩存策略,而擴展這些緩存策略並不會導致ImageLoader類的修改。這就是開閉原則!
開閉原則指導我們,當軟件需要變化時,應該儘量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。這裏的“應該儘量”4個字說明OCP原則並不是說絕對不可以修改原始類的,當我們嗅到原來的代碼“腐化氣味”時,應該儘早地重構,以使得代碼恢復到正常的“進化”軌道,而不是通過繼承等方式添加新的實現,這會導致類型的膨脹以及歷史遺留代碼的冗餘。當然我們在開發過程中也沒有那麼理想化的狀況,完全地不用修改原來的代碼,因此,在開發過程中需要自己結合具體情況進行考量,是通過修改舊代碼還是通過繼承使得軟件系統更穩定、更靈活,在保證去除“代碼腐化”的同時,也保證原有模塊的正確性。
代碼github地址:點擊打開鏈接