1、優化代碼的第一步——單一職責原則
單一職責原則的英文名稱是Single Responsibility Principle,簡稱SRP。它的定義是:就一個類而言,應該僅有一個引起它變化的原因。簡單來說,一個類中應該是一組相關性很高的函數、數據的封裝。就像秦小波老師在《設計模式之禪》中說的:“這是一個備受爭議卻又及其重要的原則。只要你想和別人爭執、慪氣或者是吵架,這個原則是屢試不爽的”。因為單一職責的劃分界限并不是總是那么清晰,很多時候都是需要靠個人經驗來界定。當然,最大的問題就是對職責的定義,什么是類的職責,以及怎么劃分類的職責。 對于計算機技術,通常只單純地學習理論知識并不能很好地領會其深意,只有自己動手實踐,并在實際運用中發現問題、解決問題、思考問題,才能夠將知識吸收到自己的腦海中。下面以我的朋友小民的事跡說起。
自從Android系統發布以來,小民就是Android的鐵桿粉絲,于是在大學期間一直保持著對Android的關注,并且利用課余時間做些小項目,鍛煉自己的實戰能力。畢業后,小民如愿地加入了心儀的公司,并且投入到了他熱愛的Android應用開發行業中。將愛好、生活、事業融為一體,小民的第一份工作也算是順風順水,一切盡在掌握中。 在經歷過一周的適應期以及熟悉公司的產品、開發規范之后,小民的開發工作就正式開始了。小民的主管是個工作經驗豐富的技術專家,對于小民的工作并不是很滿意,尤其小民最薄弱的面向對象設計,而Android開發又是使用JAVA語言,什么抽象、接口、六大原則、23種設計模式等名詞把小民弄得暈頭轉向。小民自己也察覺到了自己的問題所在,于是,小民的主管決定先讓小民做一個小項目來鍛煉鍛煉這方面的能力。正所謂養兵千日用兵一時,磨刀不誤砍柴工,小民的開發之路才剛剛開始。
在經過一番思考之后,主管挑選了使用范圍廣、難度也適中的ImageLoader(圖片加載)作為小民的訓練項目。既然要訓練小民的面向對象設計,那么就必須考慮到可擴展性、靈活性,而檢測這一切是否符合需求的最好途徑就是開源。用戶不斷地提出需求、反饋問題,小民的項目需要不斷升級以滿足用戶需求,并且要保證系統的穩定性、靈活性。在主管跟小民說了這一特殊任務之后,小民第一次感到了壓力,“生活不容易吶!”年僅22歲至今未婚的小民發出了如此深刻的感嘆!
挑戰總是要面對的,何況是從來不服輸的小民。主管的要求很簡單,要小民實現圖片加載,并且要將圖片緩存起來。在分析了需求之后,小民一下就放心下來了,“這么簡單,原來我還以為很難呢……”小民胸有成足的喃喃自語。在經歷了十分鐘的編碼之后,小民寫下了如下代碼:
/**
* 圖片加載類 */
public class ImageLoader {
// 圖片緩存
LruCache<String, Bitmap> mImageCache;
// 線程池,線程數量為CPU的數量
ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
public ImageLoader() {
initImageCache();
}
private void initImageCache() {
// 計算可使用的最大內存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 取四分之一的可用內存作為緩存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
} }; } public void displayImage(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); } }); } public Bitmap downloadImage(String imageUrl) { Bitmap bitmap = null; try { URL url = newURL(imageUrl); final HttpURLConnection conn = (HttpURLConnection)url.openConnection(); bitmap = BitmapFactory.decodeStream( conn.getInputStream()); conn.disconnect(); } catch (Exception e) { e.printStackTrace(); } return bitmap;
}}
并且使用git軟件進行版本控制,將工程托管到github上,伴隨著git push命令的完成,小民的ImageLoader 0.1版本就正式發布了!如此短的時間內就完成了這個任務,而且還是一個開源項目,小民暗暗自喜,幻想著待會兒主管的稱贊。
在小民給主管報告了ImageLoader的發布消息的幾分鐘之后,主管就把小民叫到了會議室。這下小民納悶了,怎么夸人還需要到會議室。“小民,你的ImageLoader耦合太嚴重啦!簡直就沒有設計可言,更不要說擴展性、靈活性了。所有的功能都寫在一個類里怎么行呢,這樣隨著功能的增多,ImageLoader類會越來越大,代碼也越來越復雜,圖片加載系統就越來越脆弱……”Duang,這簡直就是當頭棒喝,小民的腦海里已經聽不清主管下面說的內容了,只是覺得自己之前沒有考慮清楚就匆匆忙忙完成任務,而且把任務想得太簡單了。
“你還是把ImageLoader拆分一下,把各個功能獨立出來,讓它們滿足單一職責原則。”主管最后說道。小民是個聰明人,敏銳地捕捉到了單一職責原則這個關鍵詞。用google搜索了一些優秀資料之后總算是對單一職責原則有了一些認識。于是打算對ImageLoader進行一次重構。這次小民不敢過于草率,也是先畫了一幅UML圖,如圖1-1所示。
圖1-1
ImageLoader代碼修改如下所示:
/**
* 圖片加載類
*/
public class ImageLoader {
// 圖片緩存
ImageCache mImageCache = new ImageCache() ;
// 線程池,線程數量為CPU的數量
ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
// 加載圖片
public void displayImage(final String url, final ImageView imageView) {
Bitmap bitmap = mImageCache.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);
}
mImageCache.put(url, bitmap);
}
});
}
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
final HttpURLConnection conn =
(HttpURLConnection)
url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}
并且添加了一個ImageCache類用于處理圖片緩存,具體代碼如下:
public class ImageCache {
// 圖片LRU緩存
LruCache<String, Bitmap> mImageCache;
public ImageCache() { initImageCache(); } private void initImageCache() { // 計算可使用的最大內存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 取四分之一的可用內存作為緩存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String, Bitmap>(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) ;
}}
如圖1-1和上述代碼所示,小民將ImageLoader一拆為二,ImageLoader只負責圖片加載的邏輯,而ImageCache只負責處理圖片緩存的邏輯,這樣ImageLoader的代碼量變少了,職責也清晰了,當與緩存相關的邏輯需要改變時,不需要修改ImageLoader類,而圖片加載的邏輯需要修改時也不會影響到緩存處理邏輯。主管在審核了小民的第一次重構之后,對小民的工作給予了表揚,大致意思是結構變得清晰了許多,但是可擴展性還是比較欠缺,雖然沒有得到主管的完全肯定,但也是頗有進步,再考慮到自己確實有所收獲,小民原本沮喪的心里也略微地好轉起來。
從上述的例子中我們能夠體會到,單一職責所表達出的用意就是“單一”二字。正如上文所說,如何劃分一個類、一個函數的職責,每個人都有自己的看法,這需要根據個人經驗、具體的業務邏輯而定。但是,它也有一些基本的指導原則,例如,兩個完全不一樣的功能就不應該放在一個類中。一個類中應該是一組相關性很高的函數、數據的封裝。工程師可以不斷地審視自己的代碼,根據具體的業務、功能對類進行相應的拆分,我想這會是你優化代碼邁出的第一步。
2、讓程序更穩定、更靈活——開閉原則
開閉原則的英文全稱是Open Close Principle,簡稱OCP,它是Java世界里最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟件中的對象(類、模塊、函數等)應該對于擴展是開放的,但是,對于修改是封閉的。在軟件的生命周期內,因為變化、升級和維護等原因需要對軟件原有代碼進行修改時,可能會將錯誤引入原本已經經過測試的舊代碼中,破壞原有系統。因此,當軟件需要變化時,我們應該盡量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。當然,在現實開發中,只通過繼承的方式來升級、維護原有系統只是一個理想化的愿景,因此,在實際的開發過程中,修改原有代碼、擴展代碼往往是同時存在的。
軟件開發過程中,最不會變化的就是變化本身。產品需要不斷地升級、維護,沒有一個產品從第一版本開發完就再沒有變化了,除非在下個版本誕生之前它已經被終止。而產品需要升級,修改原來的代碼就可能會引發其他的問題。那么如何確保原有軟件模塊的正確性,以及盡量少地影響原有模塊,答案就是盡量遵守本章要講述的開閉原則。
勃蘭特·梅耶在1988年出版的《面向對象軟件構造》一書中提出這一原則。這一想法認為,一旦完成,一個類的實現只應該因錯誤而被修改,新的或者改變的特性應該通過新建不同的類實現。新建的類可以通過繼承的方式來重用原類的代碼。顯然,梅耶的定義提倡實現繼承,已存在的實現對于修改是封閉的,但是新的實現類可以通過覆寫父類的接口應對變化。 說了這么多,想必大家還是半懂不懂,還是讓我們以一個簡單示例說明一下吧。
在對ImageLoader進行了一次重構之后,小民的這個開源庫獲得了一些用戶。小民第一次感受到自己發明“輪子”的快感,對開源的熱情也越發高漲起來!通過動手實現一些開源庫來深入學習相關技術,不僅能夠提升自我,也能更好地將這些技術運用到工作中,從而開發出更穩定、優秀的應用,這就是小民的真實想法。
小民第一輪重構之后的ImageLoader職責單一、結構清晰,不僅獲得了主管的一點肯定,還得到了用戶的夸獎,算是個不錯的開始。隨著用戶的增多,有些問題也暴露出來了,小民的緩存系統就是大家“吐槽”最多的地方。通過內存緩存解決了每次從網絡加載圖片的問題,但是,Android應用的內存很有限,且具有易失性,即當應用重新啟動之后,原來已經加載過的圖片將會丟失,這樣重啟之后就需要重新下載!這又會導致加載緩慢、耗費用戶流量的問題。小民考慮引入SD卡緩存,這樣下載過的圖片就會緩存到本地,即使重啟應用也不需要重新下載了!小民在和主管討論了該問題之后就投入了編程中,下面就是小民的代碼。 DiskCache.java類,將圖片緩存到SD卡中:
public class DiskCache {
// 為了簡單起見臨時寫個路徑,在開發中請避免這種寫法 !
static String cacheDir = "sdcard/cache/";
// 從緩存中獲取圖片
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
// 將圖片緩存到內存中
public void put(String url, Bitmap bmp) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new
FileOutputStream(cacheDir + url);
bmp.compress(CompressFormat.PNG,
100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} final ly {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
因為需要將圖片緩存到SD卡中,所以,ImageLoader代碼有所更新,具體代碼如下:
public class ImageLoader {
// 內存緩存
ImageCache mImageCache = new ImageCache();
// SD卡緩存
DiskCache mDiskCache = new DiskCache();
// 是否使用SD卡緩存
boolean isUseDiskCache = false;
// 線程池,線程數量為CPU的數量
ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
public void displayImage(final String url, final ImageView imageView) {
// 判斷使用哪種緩存
Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url)
: mImageCache.get (url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
// 沒有緩存,則提交給線程池進行下載
}
public void useDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache ;
}
}
從上述的代碼中可以看到,僅僅新增了一個DiskCache類和往ImageLoader類中加入了少量代碼就添加了SD卡緩存的功能,用戶可以通過useDiskCache方法來對使用哪種緩存進行設置,例如:
ImageLoader imageLoader = new ImageLoader() ;
// 使用SD卡緩存
imageLoader.useDiskCache(true);
// 使用內存緩存
imageLoader.useDiskCache(false);
通過useDiskCache方法可以讓用戶設置不同的緩存,非常方便啊!小民對此很滿意,于是提交給主管做代碼審核。“小民,你思路是對的,但是有些明顯的問題,就是使用內存緩存時用戶就不能使用SD卡緩存,類似的,使用SD卡緩存時用戶就不能使用內存緩存。用戶需要這兩種策略的綜合,首先緩存優先使用內存緩存,如果內存緩存沒有圖片再使用SD卡緩存,如果SD卡中也沒有圖片最后才從網絡上獲取,這才是最好的緩存策略。”主管真是一針見血,小民這時才如夢初醒,剛才還得意洋洋的臉上突然有些泛紅…… 于是小民按照主管的指點新建了一個雙緩存類DoudleCache,具體代碼如下:
/**
* 雙緩存。獲取圖片時先從內存緩存中獲取,如果內存中沒有緩存該圖片,再從SD卡中獲取。
* 緩存圖片也是在內存和SD卡中都緩存一份
*/
public class DoubleCache {
ImageCache mMemoryCache = new ImageCache();
DiskCache mDiskCache = new DiskCache();
// 先從內存緩存中獲取圖片,如果沒有,再從SD卡中獲取
public Bitmap get(String url) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap == null) {
bitmap = mDiskCache.get(url);
}
return bitmap;
}
// 將圖片緩存到內存和SD卡中
public void put(String url, Bitmap bmp) {
mMemoryCache.put(url, bmp);
mDiskCache.put(url, bmp);
}
}
我們再看看最新的ImageLoader類吧,代碼更新也不多:
public class ImageLoader {
// 內存緩存
ImageCache mImageCache = new ImageCache();
// SD卡緩存
DiskCache mDiskCache = new DiskCache();
// 雙緩存
DoubleCache mDoubleCache = new DoubleCache() ;
// 使用SD卡緩存
boolean isUseDiskCache = false;
// 使用雙緩存
boolean isUseDoubleCache = false;
// 線程池,線程數量為CPU的數量
ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
public void displayImage(final String url, final ImageView imageView) {
Bitmap bmp = null;
if (isUseDoubleCache) {
bmp = mDoubleCache.get(url);
} else if (isUseDiskCache) {
bmp = mDiskCache.get(url);
} else {
bmp = mImageCache.get(url);
}
if ( bmp != null ) {
imageView.setImageBitmap(bmp);
}
// 沒有緩存,則提交給線程池進行異步下載圖片
}
public void useDiskCache(boolean useDiskCache) {
isUseDiskCache = useDiskCache ;
}
public void useDoubleCache(boolean useDoubleCache) {
isUseDoubleCache = useDoubleCache ;
}
}
通過增加短短幾句代碼和幾處修改就完成了如此重要的功能。小民已越發覺得自己Android開發已經到了的得心應手的境地,不僅感覺一陣春風襲來,他那飄逸的頭發一下從他的眼前拂過,小民感覺今天天空比往常敞亮許多。
“小民,你每次加新的緩存方法時都要修改原來的代碼,這樣很可能會引入Bug,而且會使原來的代碼邏輯變得越來越復雜,按照你這樣的方法實現,用戶也不能自定義緩存實現呀!”到底是主管水平高,一語道出了小民這緩存設計上的問題。
我們還是來分析一下小民的程序,小民每次在程序中加入新的緩存實現時都需要修改ImageLoader類,然后通過一個布爾變量來讓用戶使用哪種緩存,因此,就使得在ImageLoader中存在各種if-else判斷,通過這些判斷來確定使用哪種緩存。隨著這些邏輯的引入,代碼變得越來越復雜、脆弱,如果小民一不小心寫錯了某個if條件(條件太多,這是很容易出現的),那就需要更多的時間來排除。整個ImageLoader類也會變得越來越臃腫。最重要的是用戶不能自己實現緩存注入到ImageLoader中,可擴展性可是框架的最重要特性之一。
“軟件中的對象(類、模塊、函數等)應該對于擴展是開放的,但是對于修改是封閉的,這就是開放-關閉原則。也就是說,當軟件需要變化時,我們應該盡量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。”小民的主管補充到,小民聽得云里霧里的。主管看小民這等反應,于是親自“操刀”,為他畫下了如圖1-2的UML圖。
圖1-2
小民看到圖1-2似乎明白些什么,但是又不是太明確如何修改程序。主管看到小民這般模樣只好親自上陣,帶著小民把ImageLoader程序按照圖1-2進行了一次重構。具體代碼如下:
public class ImageLoader {
// 圖片緩存
ImageCache mImageCache = new MemoryCache();
// 線程池,線程數量為CPU的數量
ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
// 注入緩存實現
public void setImageCache(ImageCache cache) {
mImageCache = cache;
}
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.submit(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);
}
});
}
public Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
final HttpURLConnection conn = (HttpURLConnection)
url.openConnection();
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}
經過這次重構,沒有了那么多的if-else語句,沒有了各種各樣的緩存實現對象、布爾變量,代碼確實清晰、簡單了很多,小民對主管的崇敬之情又“泛濫”了起來。需要注意的是,這里的ImageCache類并不是小民原來的那個ImageCache,這次程序重構主管把它提取成一個圖片緩存的接口,用來抽象圖片緩存的功能。我們看看該接口的聲明:
public interface ImageCache {
public Bitmap get(String url);
public void put(String url, Bitmap bmp);
}
ImageCache接口簡單定義了獲取、緩存圖片兩個函數,緩存的key是圖片的url,值是圖片本身。內存緩存、SD卡緩存、雙緩存都實現了該接口,我們看看這幾個緩存實現:
// 內存緩存MemoryCache類
public class MemoryCache implements ImageCache {
private LruCache<String, Bitmap> mMemeryCache;
public MemoryCache() {
// 初始化LRU緩存
}
@Override
public Bitmap get(String url) {
return mMemeryCache.get(url);
}
@Override
public void put(String url, Bitmap bmp) {
mMemeryCache.put(url, bmp);
}
}
// SD卡緩存DiskCache類
public class DiskCache implements ImageCache {
@Override
public Bitmap get(String url) {
return null/* 從本地文件中獲取該圖片 */;
}
@Override
public void put(String url, Bitmap bmp) {
// 將Bitmap寫入文件中
}
}
// 雙緩存DoubleCache類
public class DoubleCache implements ImageCache{
ImageCache mMemoryCache = new MemoryCache();
ImageCache mDiskCache = new DiskCache();
// 先從內存緩存中獲取圖片,如果沒有,再從SD卡中獲取
public Bitmap get(String url) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap == null) {
bitmap = mDiskCache.get(url);
}
return bitmap;
}
// 將圖片緩存到內存和SD卡中
public void put(String url, Bitmap bmp) {
mMemoryCache.put(url, bmp);
mDiskCache.put(url, bmp);
}
}
細心的朋友可能注意到了,ImageLoader類中增加了一個setImageCache(ImageCache cache)函數,用戶可以通過該函數設置緩存實現,也就是通常說的依賴注入。下面就看看用戶是如何設置緩存實現的:
ImageLoader imageLoader = new ImageLoader() ;
// 使用內存緩存
imageLoader.setImageCache(new MemoryCache()); // 使用SD卡緩存
imageLoader.setImageCache(new DiskCache()); // 使用雙緩存
imageLoader.setImageCache(new DoubleCache()); // 使用自定義的圖片緩存實現
imageLoader.setImageCache(new ImageCache() { @Override public void put(String url, Bitmap bmp) { // 緩存圖片
} @Override public Bitmap get(String url) { return null/*從緩存中獲取圖片*/;
}
});
在上述代碼中,通過setImageCache(ImageCache cache)方法注入不同的緩存實現,這樣不僅能夠使ImageLoader更簡單、健壯,也使得ImageLoader的可擴展性、靈活性更高。MemoryCache、DiskCache、DoubleCache緩存圖片的具體實現完全不一樣,但是,它們的一個特點是都實現了ImageCache接口。當用戶需要自定義實現緩存策略時,只需要新建一個實現ImageCache接口的類,然后構造該類的對象,并且通過setImageCache(ImageCache cache)注入到ImageLoader中,這樣ImageLoader就實現了變化萬千的緩存策略,而擴展這些緩存策略并不會導致ImageLoader類的修改。經過這次重構,小民的ImageLoader已經基本算合格了。咦!這不就是主管說的開閉原則么!“軟件中的對象(類、模塊、函數等)應該對于擴展是開放的,但是對于修改是封閉的。而遵循開閉原則的重要手段應該是通過抽象……”小民細聲細語的念叨中,陷入了思索中……
開閉原則指導我們,當軟件需要變化時,應該盡量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。這里的“應該盡量”4個字說明OCP原則并不是說絕對不可以修改原始類的,當我們嗅到原來的代碼“腐化氣味”時,應該盡早地重構,以使得代碼恢復到正常的“進化”軌道,而不是通過繼承等方式添加新的實現,這會導致類型的膨脹以及歷史遺留代碼的冗余。我們的開發過程中也沒有那么理想化的狀況,完全地不用修改原來的代碼,因此,在開發過程中需要自己結合具體情況進行考量,是通過修改舊代碼還是通過繼承使得軟件系統更穩定、更靈活,在保證去除“代碼腐化”的同時,也保證原有模塊的正確性。
3、構建擴展性更好的系統——里氏替換原則
里氏替換原則英文全稱是Liskov Substitution Principle,簡稱LSP。它的第一種定義是:如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程序P在所有的對象o1都代換成o2時,程序P的行為沒有發生變化,那么類型S是類型T的子類型。上面這種描述確實不太好理解,理論家有時候容易把問題抽象化,本來挺容易理解的事讓他們一概括就弄得拗口了。我們再看看另一個直截了當的定義。里氏替換原則第二種定義:所有引用基類的地方必須能透明地使用其子類的對象。
我們知道,面向對象的語言的三大特點是繼承、封裝、多態,里氏替換原則就是依賴于繼承、多態這兩大特性。里氏替換原則簡單來說就是,所有引用基類的地方必須能透明地使用其子類的對象。通俗點講,只要父類能出現的地方子類就可以出現,而且替換為子類也不會產生任何錯誤或異常,使用者可能根本就不需要知道是父類還是子類。但是,反過來就不行了,有子類出現的地方,父類未必就能適應。說了那么多,其實最終總結就兩個字:抽象。 小民為了深入地了解Android中的Window與View的關系特意寫了一個簡單示例,為了便于理解,我們先看如圖1-3所示。
圖1-3
我們看看具體的代碼:
// 窗口類
public class Window {
public void show(View child){
child.draw();
}
}
// 建立視圖抽象,測量視圖的寬高為公用代碼,繪制交給具體的子類
public abstract class View {
public abstract void draw() ;
public void measure(int width, int height){
// 測量視圖大小
}
}
// 按鈕類的具體實現
public class Button extends View {
public void draw(){
// 繪制按鈕
}
}
// TextView的具體實現
public class TextView extends View {
public void draw(){
// 繪制文本
}
}
上述示例中,Window依賴于View,而View定義了一個視圖抽象,measure是各個子類共享的方法,子類通過覆寫View的draw方法實現具有各自特色的功能,在這里,這個功能就是繪制自身的內容。任何繼承自View類的子類都可以設置給show方法,也就我們所說的里氏替換。通過里氏替換,就可以自定義各式各樣、千變萬化的View,然后傳遞給Window,Window負責組織View,并且將View顯示到屏幕上。 里氏替換原則的核心原理是抽象,抽象又依賴于繼承這個特性,在OOP當中,繼承的優缺點都相當明顯。 優點如下:
- (1)代碼重用,減少創建類的成本,每個子類都擁有父類的方法和屬性;
- (2)子類與父類基本相似,但又與父類有所區別;
- (3)提高代碼的可擴展性。
繼承的缺點:
- (1)繼承是侵入性的,只要繼承就必須擁有父類的所有屬性和方法;
- (2)可能造成子類代碼冗余、靈活性降低,因為子類必須擁有父類的屬性和方法。
事物總是具有兩面性,如何權衡利與弊都是需要根據具體場景來做出選擇并加以處理。里氏替換原則指導我們構建擴展性更好的軟件系統,我們還是接著上面的ImageLoader來做說明。 上文的圖1-2也很好地反應了里氏替換原則,即MemoryCache、DiskCache、DoubleCache都可以替換ImageCache的工作,并且能夠保證行為的正確性。ImageCache建立了獲取緩存圖片、保存緩存圖片的接口規范,MemoryCache等根據接口規范實現了相應的功能,用戶只需要在使用時指定具體的緩存對象就可以動態地替換ImageLoader中的緩存策略。這就使得ImageLoader的緩存系統具有了無線的可能性,也就是保證了可擴展性。
想象一個場景,當ImageLoader中的setImageCache(ImageCache cache)中的cache對象不能夠被子類所替換,那么用戶如何設置不同的緩存對象以及用戶如何自定義自己的緩存實現,通過1.3節中的useDiskCache方法嗎?顯然不是的,里氏替換原則就為這類問題提供了指導原則,也就是建立抽象,通過抽象建立規范,具體的實現在運行時替換掉抽象,保證系統的高擴展性、靈活性。開閉原則和里氏替換原則往往是生死相依、不棄不離的,通過里氏替換來達到對擴展開放,對修改關閉的效果。然而,這兩個原則都同時強調了一個OOP的重要特性——抽象,因此,在開發過程中運用抽象是走向代碼優化的重要一步。
4、 讓項目擁有變化的能力——依賴倒置原則
依賴倒置原則英文全稱是Dependence Inversion Principle,簡稱DIP。依賴反轉原則指代了一種特定的解耦形式,使得高層次的模塊不依賴于低層次的模塊的實現細節的目的,依賴模塊被顛倒了。這個概念有點不好理解,這到底是什么意思呢? 依賴倒置原則的幾個關鍵點:
- (1)高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;
- (2)抽象不應該依賴細節;
- (3)細節應該依賴抽象。
在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被實例化的;細節就是實現類,實現接口或繼承抽象類而產生的類就是細節,其特點就是,可以直接被實例化,也就是可以加上一個關鍵字 new 產生一個對象。高層模塊就是調用端,低層模塊就是具體實現類。依賴倒置原則在 Java 語言中的表現就是:模塊間的依賴通過抽象發生,實現類之間不發生直接的依賴關系,其依賴關系是通過接口或抽象類產生的。這又是一個將理論抽象化的實例,其實一句話就可以概括:面向接口編程,或者說是面向抽象編程,這里的抽象指的是接口或者抽象類。面向接口編程是面向對象精髓之一,也就是上面兩節強調的抽象。
如果在類與類直接依賴于細節,那么它們之間就有直接的耦合,當具體實現需要變化時,意味著在這要同時修改依賴者的代碼,并且限制了系統的可擴展性。我們看1.3節的圖1-3中,ImageLoader直接依賴于MemoryCache,這個MemoryCache是一個具體實現,而不是一個抽象類或者接口。這導致了ImageLoader直接依賴了具體細節,當MemoryCache不能滿足ImageLoader而需要被其他緩存實現替換時,此時就必須修改ImageLoader的代碼,例如:
public class ImageLoader {
// 內存緩存 ( 直接依賴于細節 )
MemoryCache mMemoryCache = new MemoryCache();
// 加載圖片到ImageView中
public void displayImage(String url, ImageView imageView) {
Bitmap bmp = mMemoryCache.get(url);
if (bmp == null) {
downloadImage(url, imageView);
} else {
imageView.setImageBitmap(bmp);
}
}
public void setImageCache(MemoryCache cache) {
mCache = cache ;
}
// 代碼省略
}
隨著產品的升級,用戶發現MemoryCache已經不能滿足需求,用戶需要小民的ImageLoader可以將圖片同時緩存到內存和SD卡中,或者可以讓用戶自定義實現緩存。此時,我們的MemoryCache這個類名不僅不能夠表達內存緩存和SD卡緩存的意義,也不能夠滿足功能。另外,用戶需要自定義緩存實現時還必須繼承自MemoryCache,而用戶的緩存實現可不一定與內存緩存有關,這在命名上的限制也讓用戶體驗不好。重構的時候到了!小民的第一種方案是將MemoryCache修改為DoubleCache,然后在DoubleCache中實現具體的緩存功能。我們需要將ImageLoader修改如下:
public class ImageLoader {
// 雙緩存 ( 直接依賴于細節 )
DoubleCache mCache = new DoubleCache();
// 加載圖片到ImageView中
public void displayImage(String url, ImageView imageView) {
Bitmap bmp = mCache.get(url);
if (bmp == null) {
// 異步下載圖片
downloadImageAsync(url, imageView);
} else {
imageView.setImageBitmap(bmp);
}
}
public void setImageCache(DoubleCache cache) {
mCache = cache ;
}
// 代碼省略
}
我們將MemoryCache修改成DoubleCache,然后修改了ImageLoader中緩存類的具體實現,輕輕松松就滿足了用戶需求。等等!這不還是依賴于具體的實現類(DoubleCache)嗎?當用戶的需求再次變化時,我們又要通過修改緩存實現類和ImageLoader代碼來實現?修改原有代碼不是違反了1.3節中的開閉原則嗎?小民突然醒悟了過來,低下頭思索著如何才能讓緩存系統更靈活、擁抱變化……
當然,這些都是在主管給出圖1-2(1.3節)以及相應的代碼之前,小民體驗的煎熬過程。既然是這樣,那顯然主管給出的解決方案就能夠讓緩存系統更加靈活。一句話概括起來就是:依賴抽象,而不依賴具體實現。針對于圖片緩存,主管建立的ImageCache抽象,該抽象中增加了get和put方法用以實現圖片的存取。每種緩存實現都必須實現這個接口,并且實現自己的存取方法。當用戶需要使用不同的緩存實現時,直接通過依賴注入即可,保證了系統的靈活性。我們再來簡單回顧一下相關代碼:
ImageCache緩存抽象:
public interface ImageCache {
public Bitmap get(String url);
public void put(String url, Bitmap bmp);
}
ImageLoader類:
public class ImageLoader {
// 圖片緩存類,依賴于抽象,并且有一個默認的實現
ImageCache mCache = new MemoryCache();
// 加載圖片
public void displayImage(String url, ImageView imageView) {
Bitmap bmp = mCache.get(url);
if (bmp == null) {
// 異步加載圖片
downloadImageAsync(url, imageView);
} else {
imageView.setImageBitmap(bmp);
}
}
/**
* 設置緩存策略,依賴于抽象
*/
public void setImageCache(ImageCache cache) {
mCache = cache;
}
// 代碼省略
}
在這里,我們建立了ImageCache抽象,并且讓ImageLoader依賴于抽象而不是具體細節。當需求發生變更時,小民只需要實現ImageCahce類或者繼承其他已有的ImageCache子類完成相應的緩存功能,然后將具體的實現注入到ImageLoader即可實現緩存功能的替換,這就保證了緩存系統的高可擴展性,擁有了擁抱變化的能力,而這一切的基本指導原則就是我們的依賴倒置原則。從上述幾節中我們發現,要想讓我們的系統更為靈活,抽象似乎成了我們唯一的手段。
5、系統有更高的靈活性——接口隔離原則
接口隔離原則英文全稱是InterfaceSegregation Principles,簡稱ISP。它的定義是:客戶端不應該依賴它不需要的接口。另一種定義是:類間的依賴關系應該建立在最小的接口上。接口隔離原則將非常龐大、臃腫的接口拆分成為更小的和更具體的接口,這樣客戶將會只需要知道他們感興趣的方法。接口隔離原則的目的是系統解開耦合,從而容易重構、更改和重新部署。
接口隔離原則說白了就是,讓客戶端依賴的接口盡可能地小,這樣說可能還是有點抽象,我們還是以一個示例來說明一下。在此之前我們來說一個場景,在Java 6以及之前的JDK版本,有一個非常討厭的問題,那就是在使用了OutputStream或者其他可關閉的對象之后,我們必須保證它們最終被關閉了,我們的SD卡緩存類中就有這樣的代碼:
// 將圖片緩存到內存中
public void put(String url, Bitmap bmp) { FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace(); } finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close(); } catch (IOException e) {
e.printStackTrace(); } } // end if
} // end if finally
}
我們看到的這段代碼可讀性非常差,各種try…catch嵌套,都是些簡單的代碼,但是會嚴重影響代碼的可讀性,并且多層級的大括號很容易將代碼寫到錯誤的層級中。大家應該對這類代碼也非常反感,那我們看看如何解決這類問題。 我們可能知道Java中有一個Closeable接口,該接口標識了一個可關閉的對象,它只有一個close方法,如圖1-4所示。 我們要講的FileOutputStream類就實現了這個接口,我們從圖1-4中可以看到,還有一百多個類實現了Closeable這個接口,這意味著,在關閉這一百多個類型的對象時,都需要寫出像put方法中finally代碼段那樣的代碼。這還了得!你能忍,反正小民是忍不了的!于是小民打算要發揮他的聰明才智解決這個問題,既然都是實現了Closeable接口,那只要我建一個方法統一來關閉這些對象不就可以了么?說干就干,于是小民寫下來如下的工具類:
圖1-4
public final class CloseUtils {
Private CloseUtils() { }
/**
* 關閉Closeable對象
* @param closeable
*/
public static void closeQuietly(Closeable closeable) {
if (null != closeable) {
try {
closeable.close(); } catch (IOException e) {
e.printStackTrace(); } } }}
我們再看看把這段代碼運用到上述的put方法中的效果如何:
public void put(String url, Bitmap bmp) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace(); } final ly {
CloseUtils.closeQuietly(fileOutputStream); }}
代碼簡潔了很多!而且這個closeQuietly方法可以運用到各類可關閉的對象中,保證了代碼的重用性。CloseUtils的closeQuietly方法的基本原理就是依賴于Closeable抽象而不是具體實現(這不是1.4節中的依賴倒置原則么),并且建立在最小化依賴原則的基礎,它只需要知道這個對象是可關閉,其他的一概不關心,也就是這里的接口隔離原則。
試想一下,如果在只是需要關閉一個對象時,它卻暴露出了其他的接口函數,比如OutputStream的write方法,這就使得更多的細節暴露在客戶端代碼面前,不僅沒有很好地隱藏實現,還增加了接口的使用難度。而通過Closeable接口將可關閉的對象抽象起來,這樣只需要客戶端依賴于Closeable就可以對客戶端隱藏其他的接口信息,客戶端代碼只需要知道這個對象可關閉(只可調用close方法)即可。小民ImageLoader中的ImageCache就是接口隔離原則的運用,ImageLoader只需要知道該緩存對象有存、取緩存圖片的接口即可,其他的一概不管,這就使得緩存功能的具體實現對ImageLoader具體的隱藏。這就是用最小化接口隔離了實現類的細節,也促使我們將龐大的接口拆分到更細粒度的接口當中,這使得我們的系統具有更低的耦合性,更高的靈活性。
Bob大叔(Robert C Martin)在21世紀早期將單一職責、開閉原則、里氏替換、接口隔離以及依賴倒置(也稱為依賴反轉)5個原則定義為SOLID原則,指代了面向對象編程的5個基本原則。當這些原則被一起應用時,它們使得一個軟件系統更清晰、簡單、最大程度地擁抱變化。SOLID被典型地應用在測試驅動開發上,并且是敏捷開發以及自適應軟件開發基本原則的重要組成部分。在經過第1.1~1.5節的學習之后,我們發現這幾大原則最終就可以化為這幾個關鍵詞:抽象、單一職責、最小化。那么在實際開發過程中如何權衡、實踐這些原則,是大家需要在實踐中多思考與領悟,正所謂”學而不思則罔,思而不學則殆”,只有不斷地學習、實踐、思考,才能夠在積累的過程有一個質的飛越。
6、更好的可擴展性——迪米特原則
迪米特原則英文全稱為Law of Demeter,簡稱LOD,也稱為最少知識原則(Least Knowledge Principle)。雖然名字不同,但描述的是同一個原則:一個對象應該對其他對象有最少的了解。通俗地講,一個類應該對自己需要耦合或調用的類知道得最少,類的內部如何實現、如何復雜都與調用者或者依賴者沒關系,調用者或者依賴者只需要知道他需要的方法即可,其他的我一概不關心。類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。
迪米特法則還有一個英文解釋是:Only talk to your immedate friends,翻譯過來就是:只與直接的朋友通信。什么叫做直接的朋友呢?每個對象都必然會與其他對象有耦合關系,兩個對象之間的耦合就成為朋友關系,這種關系的類型有很多,例如組合、聚合、依賴等。
光說不練很抽象吶,下面我們就以租房為例來講講迪米特原則。 “北漂”的同學比較了解,在北京租房絕大多數都是通過中介找房。我們設定的情境為:我只要求房間的面積和租金,其他的一概不管,中介將符合我要求的房子提供給我就可以。下面我們看看這個示例:
/**
* 房間
*/
public class Room {
public float area;
public float price;
public Room(float area, float price) {
this.area = area;
this.price = price;
} @Override
public String toString() {
return "Room [area=" + area + ", price=" + price + "]";
}}/**
* 中介
*/
public class Mediator {
List<Room> mRooms = new ArrayList<Room>();
public Mediator() {
for (inti = 0; i < 5; i++) {
mRooms.add(new Room(14 + i, (14 + i) * 150));
} } public List<Room>getAllRooms() {
return mRooms;
}}/**
* 租戶
*/
public class Tenant {
public float roomArea;
public float roomPrice;
public static final float diffPrice = 100.0001f;
public static final float diffArea = 0.00001f;
public void rentRoom(Mediator mediator) {
List<Room>rooms = mediator.getAllRooms(); for (Room room : rooms) {
if (isSuitable(room)) {
System.out.println("租到房間啦! " + room);
break;
} } } private boolean isSuitable(Room room) {
return Math.abs(room.price - roomPrice) < diffPrice
&&Math.abs(room.area - roomArea) < diffArea;
}}
從上面的代碼中可以看到,Tenant不僅依賴了Mediator類,還需要頻繁地與Room類打交道。租戶類的要求只是通過中介找到一間適合自己的房間罷了,如果把這些檢測條件都放在Tenant類中,那么中介類的功能就被弱化,而且導致Tenant與Room的耦合較高,因為Tenant必須知道許多關于Room的細節。當Room變化時Tenant也必須跟著變化。Tenant又與Mediator耦合,就導致了糾纏不清的關系。這個時候就需要我們分清誰才是我們真正的“朋友”,在我們所設定的情況下,顯然是Mediator(雖然現實生活中不是這樣的)。上述代碼的結構如圖1-5所示。
圖1-5
既然是耦合太嚴重,那我們就只能解耦了,首先要明確地是,我們只和我們的朋友通信,這里就是指Mediator對象。必須將Room相關的操作從Tenant中移除,而這些操作案例應該屬于Mediator,我們進行如下重構:
/**
* 中介
*/
public class Mediator {
List<Room> mRooms = new ArrayList<Room>();
public Mediator() {
for (inti = 0; i < 5; i++) {
mRooms.add(new Room(14 + i, (14 + i) * 150));
} } public Room rentOut(float area, float price) {
for (Room room : mRooms) {
if (isSuitable(area, price, room)) {
return room;
} } return null;
} private boolean isSuitable(float area, float price, Room room) {
return Math.abs(room.price - price) < Tenant.diffPrice
&& Math.abs(room.area - area) < Tenant.diffPrice;
}}/**
* 租戶
*/
public class Tenant {
public float roomArea;
public float roomPrice;
public static final float diffPrice = 100.0001f;
public static final float diffArea = 0.00001f;
public void rentRoom(Mediator mediator) {
System.out.println("租到房啦 " + mediator.rentOut(roomArea, roomPrice));
}}
重構后的結構圖如圖1-6所示。
圖1-6
只是將對于Room的判定操作移到了Mediator類中,這本應該是Mediator的職責,他們根據租戶設定的條件查找符合要求的房子,并且將結果交給租戶就可以了。租戶并不需要知道太多關于Room的細節,比如與房東簽合同、房東的房產證是不是真的、房內的設施壞了之后我要找誰維修等,當我們通過我們的“朋友”中介租了房之后,所有的事情我們都通過與中介溝通就好了,房東、維修師傅等這些角色并不是我們直接的“朋友”。“只與直接的朋友通信”這簡單的幾個字就能夠將我們從亂七八糟的關系網中抽離出來,使我們的耦合度更低、穩定性更好。 通過上述示例以及小民的后續思考,迪米特原則這把利劍在小民的手中已經舞得風生水起。就拿sd卡緩存來說吧,ImageCache就是用戶的直接朋友,而SD卡緩存內部卻是使用了jake wharton的DiskLruCache實現,這個DiskLruCache就不屬于用戶的直接朋友了,因此,用戶完全不需要知道它的存在,用戶只需要與ImageCache對象打交道即可。例如將圖片存到SD卡中的代碼如下。
public void put(String url, Bitmap value) {
DiskLruCache.Editor editor = null;
try {
// 如果沒有找到對應的緩存,則準備從網絡上請求數據,并寫入緩存
editor = mDiskLruCache.edit(url);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (writeBitmapToDisk(value, outputStream)) {
// 寫入disk緩存
editor.commit();
} else {
editor.abort();
}
CloseUtils.closeQuietly(outputStream);
}
} catch (IOException e) {
e.printStackTrace();
}
}
用戶在使用SD卡緩存時,根本不知曉DiskLruCache的實現,這就很好地對用戶隱藏了具體實現。當小民已經“牛”到可以自己完成SD卡的rul實現時,他就可以隨心所欲的替換掉jake wharton的DiskLruCache。小民的代碼大體如下:
@Override
public void put(String url, Bitmap bmp) { // 將Bitmap寫入文件中
FileOutputStream fos = null;
try {
// 構建圖片的存儲路徑 ( 省略了對url取md5)
fos = new FileOutputStream("sdcard/cache/" + imageUrl2MD5(url));
bmp.compress(CompressFormat.JPEG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace(); } finally {
if ( fos != null ) {
try {
fos.close(); } catch (IOException e) {
e.printStackTrace(); } } } // end if finally
}
SD卡緩存的具體實現雖然被替換了,但用戶根本不會感知到。因為用戶根本不知道DiskLruCache的存在,他們沒有與DiskLruCache進行通信,他們只認識直接“朋友”ImageCache,ImageCache將一切細節隱藏在了直接“朋友”的外衣之下,使得系統具有更低的耦合性和更好的可擴展性。
7、總結
在應用開發過程中,最難的不是完成應用的開發工作,而是在后續的升級、維護過程中讓應用系統能夠擁抱變化。擁抱變化也就意味著在滿足需求且不破壞系統穩定性的前提下保持高可擴展性、高內聚、低耦合,在經歷了各版本的變更之后依然保持清晰、靈活、穩定的系統架構。當然,這是一個比較理想的情況,但我們必須要朝著這個方向去努力,那么遵循面向對象六大原則就是我們走向靈活軟件之路所邁出的第一步。