前言:有色無閹割版請參見 Android面向?qū)ο罅蠡驹瓌t-團隊技術(shù)分享
這是團隊技術(shù)分享前,編寫的文檔,一篇文章,講滿了技術(shù)分享的3個小時。
圍繞ImageLoader通俗易懂的講解了各原則的使用場景與優(yōu)缺點。幫助非常的大!不管是新的,還是老的程序員,都推薦來看一下。復(fù)習(xí)一下。相比理解了,記住了,更推薦朋友們能夠面向大家講出來,進步更大。
Android 面向?qū)ο罅蠡驹瓌t
本文幾個方面來介紹
1、面向?qū)ο罅蠡驹瓌t的定義+舉例說明
2、代碼中的使用
3、優(yōu)缺點,
4、小結(jié)
會圍繞一個圖片加載框架,一步步講解代碼優(yōu)化代碼,從而表明六大基本原則的應(yīng)用與效果。
因為這是設(shè)計模式的基礎(chǔ)與核心參照原則
第一章、走向靈活軟件之路-面向?qū)ο罅蠡驹瓌t
前言:面向?qū)ο罅蠡驹瓌t,是代碼編寫和代碼優(yōu)化的基礎(chǔ),也是核心,雖然是基礎(chǔ),但是非常重要,而且沒有深入了解過的話,一知半解,也不好。本次圍繞 圖片加載庫 舉例說明。深入淺出透徹的理解什么是六大基本原則,如何運用。以及優(yōu)缺點。
基本原則就好比 建造房租一樣,首先要打地基,明白建造房租的流程,哪些節(jié)點是可以優(yōu)化的,比如水電如何優(yōu)化、開關(guān)如何設(shè)計、接口如何預(yù)留等等。明白了基本原則后,做任何事,都會在正常安全的范圍內(nèi),不會導(dǎo)致大錯
這種例子 生活中其實比比皆是,比如相機、手機的設(shè)計和制造 都有屬于各自領(lǐng)域的基本原則,遵循原則可以讓一切變得有規(guī)律、順暢、符合邏輯。
如果某家單反生產(chǎn)商,搞特殊,相機鏡頭搞了個18:9的接口,那基本上就廢了,市面上所有的鏡頭接口都不適合他。無法更換鏡頭,也注定了,他永遠(yuǎn)都做不大。不管多牛逼,遵循基本原則,是首要前提。
比如一個攝影師,要拍人像,卻用廣角鏡頭,人當(dāng)然可以拍出來,但是效果就差太多了。或者說P圖的基本原則,磨皮、瘦臉、縮鼻翼,大眼、瘦腰、大長腿。遵循這些基本原則,不管是拍出來的照片、還是p出來的圖片,都要比無章法的自由發(fā)揮好很多。
1.1 優(yōu)化代碼的第一步-【單一職責(zé)原則(SRP)】
1.1.1 定義,什么是單一職責(zé)原則?
?????英文名稱是 SIngle Responsibility Principle(SRP),定義為:就一個類而言,應(yīng)該僅有一個引起他變化的原因。簡單來說,一個類中,應(yīng)該是一組相關(guān)性很高的函數(shù)、數(shù)據(jù)的封裝。
?????但是 單一職責(zé)的劃分界限并不算很清晰,很多時候都需要靠個人經(jīng)驗來界定,最大的問題就算對職責(zé)的定義,什么是類的職責(zé),以及怎么劃分類的職責(zé)。
?????比如:我經(jīng)常看到一些Android開發(fā)在Activity中寫B(tài)ean文件,網(wǎng)絡(luò)數(shù)據(jù)處理,如果有列表的話Adapter 也寫在Activity中,問他們?yōu)槭裁闯撕谜乙矝]啥理由了,把他們拆分到其他類豈不是更好找,如果Activity過于臃腫行數(shù)過多,顯然不是好事,如果我們要修改Bean文件,網(wǎng)絡(luò)處理和Adapter都需要上這個Activity來修改,就會導(dǎo)致引起這個Activity變化的原因太多,我們在版本維護時也會比較頭疼。也就嚴(yán)重違背了定義“就一個類而言,應(yīng)該僅有一個引起它變化的原因”。
?????當(dāng)然如果想爭論的話,這個模式是可以引起很多爭論的,但請記住一點,你寫代碼不只是為了你也是為了其他人
?????舉個栗子:小到燈的開關(guān)、馬桶的沖水按鈕、大一點到房屋設(shè)計,廚房是廚房、衛(wèi)生間是衛(wèi)生間,電閘是單獨在一個固定區(qū)域的。從來沒見過衛(wèi)生間和廚房是在一起的吧?也沒見過,按一下馬桶沖水按鈕,既能沖水,又能充話費吧?
1.1.2 代碼舉例說明:
舉例就舉大家最熟悉的例子,以 圖片加載庫為例,可以分析下,這個類,在設(shè)計上有什么問題
public class ImageLoader{
// 圖片緩存
LruCache<String,Bitmap> mImageCache;
// 線程池,線程數(shù)量為CPU的數(shù)量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
public ImageLoader(){
initImageCache();
}
// 初始化
private void initImageCache(){
// 計算可使用的最大內(nèi)存
final int maxMenory = (int)(Runtime.getRuntime().maxMenory()/1024);
// 取1/4的可用內(nèi)存作為緩存
final int cacheSize = maxMenory / 4;
mImageCache = new LryCache<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){
image.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();
}cache(Exception e){
e.printStackTrace();
}
return bitmap;
}
}</pre>
問題解析:1、耦合嚴(yán)重 2、拓展性,靈活性差。
主要表現(xiàn)在:圖片緩存邏輯 與 展示邏輯,寫在同一個類,隨著業(yè)務(wù)增多,個性化需求增多,代碼會越來越復(fù)雜和無法維護
1.1.3 通過單一職責(zé)原則優(yōu)化
public class ImageLoader{
// 圖片緩存-新建一個圖片緩存類
ImageCache mImageCache = new ImageCache() ;
// 線程池,線程數(shù)量為CPU的數(shù)量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
public void displayImage(final String url,final ImageView ImageView){
image.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();
}cache(Exception e){
e.printStackTrace();
}
return bitmap;
}
}</pre>
將圖片緩存邏輯拆分出來。
public class ImageCache{
// 圖片LRU緩存
LruCache<String,Bitmap> mImageCache;
// 初始化
private void initImageCache(){
// 計算可使用的最大內(nèi)存
final int maxMenory = (int)(Runtime.getRuntime().maxMenory()/1024);
// 取1/4的可用內(nèi)存作為緩存
final int cacheSize = maxMenory / 4;
mImageCache = new LryCache<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);
}
}
優(yōu)點:將ImageLoader一拆為二,ImageLoader只負(fù)責(zé)圖片加載的邏輯,ImageCache只負(fù)責(zé)處理圖片緩存的邏輯,這樣相互代碼量少了。職責(zé)也清晰了。 修改任一方邏輯,都相互不干預(yù)
小結(jié):一個函數(shù)的職責(zé),每個人都有自己的看法,這要根據(jù)經(jīng)驗而定,具體邏輯而定,但是也有基本指導(dǎo)原則:兩個完全不一樣的功能,就不應(yīng)該放在一個類中,一個類中應(yīng)該是一組相關(guān)性很高的函數(shù)、數(shù)據(jù)的封裝。這需要不斷的審視代碼,根據(jù)具體業(yè)務(wù)、功能進行拆分優(yōu)化;
1.2 讓程序更穩(wěn)定、更靈活-**【開閉原則(OCP)】重要
1.2.1 定義,什么是開閉原則
開閉原則的英文全稱是 Open Close Principle ,縮寫是OCP,定義為:軟件中的對象(類、模塊、函數(shù)等)應(yīng)該對于拓展是開放的,但是,對于修改是封閉的
實際應(yīng)用舉例說明:在軟件生命周期內(nèi),因為變化、升級和維護等原因,需要對軟件原有代碼進行修改時,可能會錯誤引入原本已經(jīng)經(jīng)過測試的舊代碼中,破壞原有系統(tǒng),本來是正常的,一修改 全掛了。 。 。
因此,當(dāng)軟件需要變化時,我們應(yīng)該盡量通過拓展的方式來實現(xiàn)變化,而不是通過修改已有代碼來實現(xiàn)。但是,實際開發(fā)中,拓展代碼、在原有基礎(chǔ)上修改,是經(jīng)常同時存在的
舉個栗子:生活中常見的,電源插座,當(dāng)位置不夠的時候,怎么操作?沒有人 把插座拿去改造吧?都是再找個插頭,續(xù)上。這其實就是開閉原則,我可以支持任意拓展,但是你不能來隨意修改我。
再比如,房屋的設(shè)計,電表是有個電箱單獨存放的,是開放的。沒見過,把電表砌在衛(wèi)生間墻里面的吧?萬一晚上電表爆了,難道要抹黑去一錘子80去敲墻 找電表再替換?這顯然是不可能的。電表都單獨存在,并且易替換、易維護。修改、替換電表,對我房子沒有任何影響。
1.2.2 代碼舉例說明
以上面的ImageLoader展示與緩存為例,通過內(nèi)存緩存解決了每次從網(wǎng)絡(luò)加載圖片的問題,但是,Android應(yīng)用內(nèi)存有限,并且具有易失性,應(yīng)用重啟后,原有加載過的圖片將丟失
因此,考慮加入SD卡緩存功能來解決這一問題,說干就干,初始代碼如下:
新建一個SD卡緩存實現(xiàn)類:
public class DiskCache {
static String cacheDir = "sdcard/cache/"
// 從緩存中獲取圖片
public Bitmap get(String url){
return BitmapFactory.decodeFile(cacheDir +url);
}
// 將圖片緩存SD卡
public void put(String url,Bitmap bitmap){
FileOutPutStream fileOutPutStream = null ;
try{
fileOutPutStream = new FileOutPutStream(cacheDir + url);
bitmap.compress(CompressFormat.PNG,100,fileOutPutStream);
}cache(Exception e){
e.printStackTrace();
}finally{
if(fileOutPutStream!=null){
try{
fileOutPutStream.close();
}cache(Exception e){
e.printStackTrace();
}
}
}
}
}
因為,要將圖片緩存到SD卡中,對應(yīng)ImageLoader代碼修改如下
public class ImageLoader{
// 內(nèi)存緩存
ImageCache mImageCache = new ImageCache() ;
// SD 卡緩存
DiskCache mDiskCache = new DiskCache();
// 是否
boolean isUseDiskCache = false;
// 線程池,線程數(shù)量為CPU的數(shù)量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
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 useeDiskCache(boolean useDiskCache){
isUseDiskCache = useDiskCache;
}
}</pre>
用戶可以自由選擇,使用內(nèi)存緩存 或者 SD卡緩存
問題:使用內(nèi)存緩存時,不能同時SD卡緩存,使用SD卡緩存,不能同時使用內(nèi)存緩存
正常的策略:優(yōu)先從內(nèi)存緩存,如果內(nèi)存緩存沒有則使用SD卡緩存,如果SD卡也沒有,才從網(wǎng)絡(luò)下載圖片,這才是好的緩存策略
于是按照這個思路,又優(yōu)化代碼。。。新增一個DoubleCache類:
/*
* 雙緩存,獲取圖片時,從內(nèi)存中獲取,如果內(nèi)存中沒有緩存圖片,再從SD卡中獲取。
* 緩存圖片在內(nèi)存和SD卡中都緩存一份
*/
public class DoubleCache{
ImageCache mMemoryCache = new ImageCache();
DiskCache mDiskCache = new DiskCache();
// 優(yōu)先從內(nèi)存緩存中獲取圖片,如果沒有,再從SD卡中獲取圖片
public Bitmap get(String url){
Bitmap bitmap = mMemoryCache.get(url);
if(bitmap==null){
bitmap = mDiskCache.get(url);
}
return bitmap;
}
public void put(String url,Bitmap bitmap){
mMemoryCache.put(url,bitmap);
mDiskCache.put(url,bitmap);
}
}</pre>
再看看最新的ImageLoader類:
public class ImageLoader{
// 內(nèi)存緩存
ImageCache mImageCache = new ImageCache() ;
// SD 卡緩存
DiskCache mDiskCache = new DiskCache();
// 雙緩存
DoubleCache mDoubleCache = new DoubleCache();
// 是否使用SD卡緩存
boolean isUseDiskCache = false;
// 是否使用雙緩存
boolea isUseDoubleCache = false;
// 線程池,線程數(shù)量為CPU的數(shù)量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
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);
}
// 如果沒有緩存,交給線程池下載...
}
public void useeDiskCache(boolean useDiskCache){
isUseDiskCache = useDiskCache;
}
public void useDoubleCache(boolean useDoubleCache){
isUseDoubleCache = useDoubleCache;
}
}
問題:1、每次修改緩存邏輯,都需要修改ImageLoader類,然后通過一個布爾變量來選擇使用哪種緩存,這樣可能引入bug,并且使得代碼越來越臃腫,容易出錯
** 2、用戶不能自己實現(xiàn)緩存策略注入到ImageLoader中,可拓展性差,可拓展性是框架最重要的特性之一**
回到開閉原則的定義,(類、模塊、函數(shù)等)對于拓展應(yīng)該是開放的,但是對于修改是封閉的。上面的代碼明顯不復(fù)合開閉原則。軟件優(yōu)化時,盡量通過拓展的方式來實現(xiàn)變化,而不是通過直接修改已有代碼來實現(xiàn)
1.2.3 通過開閉原則重新設(shè)計與優(yōu)化
public class ImageLoader{
// 內(nèi)存緩存-修改為抽象接口
ImageCache mImageCache = new ImageCache() ;
// 線程池,線程數(shù)量為CPU的數(shù)量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
// 注入緩存實現(xiàn)
public void setImageCache(ImageCache cache){
mImageCache = cache;
}
public void displayImage(String url,ImageView imageView){
Bitmap bitmap = mImageCache.get(url);
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();
}cache(Exception e){
e.printStackTrace();
}
return bitmap;
}
}
這里的ImageCache并不是原有的ImageCache,而是提取成了一個圖片緩存接口,用來抽象圖片緩存功能,具體聲明如下:
// 圖片緩存接口
public interface ImageCache{
public Bitmap get(String url);
public void put(String url,Bitmap bitmap);
}</pre>
ImageCache定義了獲取、緩存兩個函數(shù),緩存的key是圖片的url,值是圖片本身,內(nèi)存緩存,SD卡緩存,雙緩存都實現(xiàn)了該接口,看看幾個緩存的實現(xiàn):
public class MemoryCache implements ImageCache{
private LruCache<Stirng,Bitmap> mMemeryCache;
public MemoryCache(){
// 初始化LRU緩存
}
@Override
public Bitmap get(String url){
return mMemeryCache.get(url);
}
@Override
public void put(String url,Bitmap bitmap){
mMemeryCache.put(url,bitmap);
}
}
public class DiskCache implements ImageCache{
@Override
public Bitmap get(String url){
// 從本地文件獲取圖片
}
@Override
public void put(String url,Bitmap bitmap){
// 將Bitmap寫入文件中
}
}
public class DoubleCache implements ImageCache{
ImageCache mMemeryCache = new MemoryCache();
ImageCache mDiskCache = new DiskCache();
// 先從內(nèi)存緩存中獲取圖片,如果沒有,再從SD卡中獲取圖片
public Bitmap get(String url){
Bitmap bitmap = mMemeryCache.get(url);
if(bitmap==null){
bitmap = mDiskCache.get(url);
}
return bitmap;
}
// 將圖片寫入到內(nèi)存和SD卡中
public void put(String url,Bitmap bitmap){
mMemeryCache.put(url,bitmap);
mDiskCache.put(url,bitmap);
}
}
細(xì)心的人可能發(fā)現(xiàn)了。ImageLoader類中增加了一個setImageCache(ImageCache cache)函數(shù),用戶可以童工該函數(shù)設(shè)置緩存實現(xiàn),也就是通常說的依賴注入,設(shè)置方法如下:
ImageLoader imageLoader = new ImageLoader();
// 使用內(nèi)存緩存
imageLoader.setImageCache(new MemoryCache());
// 使用SD卡緩存
imageLoader.setImageCache(new DiskCache());
// 使用雙緩存
imageLoader.setImageCache(new DoubleCache());
// 用戶自定義緩存
imageLoader.setImageCache(new Imagecache(){
@Override
public void put(Strign url,Bitmap bitmap){
// 自定義將圖片緩存策略
}
@Override
public Bitmap get(String url){
// 自定義從緩存中獲取圖片
}
})
小結(jié):在上述代碼中,通過setImageCache(ImageCache cache)方法注入不同的緩存實現(xiàn),不僅能夠使ImageLoader更簡單,健壯,也使得ImageLoader的可拓展性,靈活性更高。通過ImageCache接口,注入到ImageLoader中,可以實現(xiàn)千變?nèi)f化的緩存策略,并且拓展這些緩存策略不會導(dǎo)致ImageLoader修改。這就是標(biāo)準(zhǔn)的開閉原則。
PS:當(dāng)軟件需要優(yōu)化時,應(yīng)該盡量通過拓展的方式來實現(xiàn)變化,而不是通過修改已有代碼來實現(xiàn) “應(yīng)該盡量”說明并非絕對不可以修改原始類。如果代碼惡心到吐,還是應(yīng)該盡早的重構(gòu),具體要根據(jù)開發(fā)時間以及配套資源來判定。
1.3 構(gòu)建拓展新更好的系統(tǒng)-【里氏替換原則(LSP)】
1.3.1 定義,什么里氏替換原則?
里氏替換原則英文全稱是(LisKov SubStitution Principle),縮寫是LSP,定義為:所有引用基類的地方,都能透明的使用其子類對象;
面向?qū)ο笕筇攸c,繼承、封裝、多態(tài),里氏替換就是依賴了繼承、多態(tài)兩大特性;通俗點講,只要父類出現(xiàn)的地方,子類就可以出現(xiàn),而且替換為子類也不會產(chǎn)生任何錯誤或異常,使用者根本就不需要知道父類還是子類,但是反過來就不行了,有子類的地方,父類未必能適應(yīng)。。。
總結(jié)其實就兩個字:抽象
舉個栗子:耳機,老的有3.5mm接口的二級,新的有type-c接口的耳機或者藍(lán)牙耳機。
手機只有一個,但是耳機我有十個,是什么讓我可以任意的、方便的替換耳機,而手機不需要任何更改。核心就在于耳機接口的設(shè)計。 只要耳機是3.5mm的,我就可以支持。不管你是森海塞爾的、鐵三角的,還是華為、小米的。隨便你換。
還有就是單反相機,單反相機的鏡頭都是可替換的,廣角、微距、人像定焦、一鏡走天下等等。我既可以使用原廠的鏡頭,也可以使用第三方鏡頭替換,為什么可以做到這樣?因為單反設(shè)計了鏡頭的接口,只要是符合單反接口的鏡頭。都可以支持。
試想一下,如果不設(shè)計這種接口,那會怎么樣?那只能是小作坊。永遠(yuǎn)做不大了。
****1.3.2 代碼舉例說明**
[圖片上傳失敗...(image-b23d7a-1554990639319)]
**1.3.3 通過里氏替換原則優(yōu)化代碼****
可以看上面講過的 圖片加載示例,其實已經(jīng)很好的體現(xiàn)了里氏替換原則,MemoryCache、DiskCache、DoubleCache 都可以替換ImageCache的工作,并且保證正常運行。
小結(jié):里氏替換原則與開閉原則生死相依、不離不棄 ,通過里氏替換原則達到對拓展的開放,對修改的關(guān)閉效果,倆原則同時強調(diào)了一個OOP的重要特性-抽象,因此,在開發(fā)過程中,運用抽象是走向代碼優(yōu)化的重要一步。
里氏替換原則核心原理是抽象,抽象又依賴于繼承這個特性,在OOP中,繼承的優(yōu)缺點都特別明顯,
優(yōu)點有以下幾個方面:
(1)代碼重用,減少創(chuàng)建類的成本,每個子類都擁有父類的方法和屬性
(2)子類與父類基本相似,但又與父類有所區(qū)別
(3)提高代碼的可拓展性
繼承的缺點:
(1)集成是侵入性的,只要繼承就必須擁有父類的所有屬性和方法
(2)可能造成子類代碼冗余,靈活性降低,因為子類必須擁有父類的屬性和方法;
1.4 讓項目擁有變化的能力-【依賴倒置原則(DIP)】****
1.4.1 定義 什么是依賴倒置原則
依賴倒置英文全稱(Dependence Inversion Principle) 縮小是DIP
定義:高層模塊不應(yīng)該依賴低層模塊,兩個都應(yīng)該依賴于抽象。抽象不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。
依賴倒置原則有以下幾個關(guān)鍵點:
(1)高層模塊不應(yīng)該依賴底層模塊,兩者應(yīng)該都依賴其抽象
(2)抽象接口不應(yīng)該依賴具體實現(xiàn)
(3)實現(xiàn)類應(yīng)該依賴抽象接口
解釋說明:在Java中,抽象就是指接口或者抽象類,兩者都是不能直接被實例化的;細(xì)節(jié)就是實現(xiàn)類,實現(xiàn)接口或者繼承抽象類而產(chǎn)生的就是細(xì)節(jié),也就是可以加上一個關(guān)鍵字new產(chǎn)生的對象。高層模塊就是調(diào)用端,低層模塊就是具體實現(xiàn)類。
依賴倒置原則在Java中的表現(xiàn)就是:模塊間通過抽象發(fā)生,實現(xiàn)類之間不發(fā)生直接依賴關(guān)系,其依賴關(guān)系是通過接口或者抽象類產(chǎn)生的。如果類與類直接依賴細(xì)節(jié),那么就會直接耦合,那么當(dāng)修改時,就會同時修改依賴者代碼,這樣限制了可擴展性。
舉個栗子:單反相機不應(yīng)該直接依賴某個鏡頭型號,即使這個鏡頭跟單反可以匹配。而是依賴4:3鏡頭連接接口,相機支持4:3接口的所有鏡頭。而鏡頭本身也是遵循4:3接口。那么兩者就可以關(guān)聯(lián)到一起。****相機就是高層模塊,鏡頭就是具體實現(xiàn),而抽象接口 就是4:3的鏡頭連接接口。
類似的有 插座、手機耳機、usb、type-c、hdmi等
1.4.2 代碼舉例
public class ImageLoader{
//內(nèi)存緩存(直接依賴細(xì)節(jié))
MemoeryCache mMenoryCache = new MenoryCache();
// 加載圖片到ImageLoader中
public void displayImage(String url,ImageView imageView){
Bitmap bitmap = mMenoryCache.get(url);
if(bitmap==null){
downloadImage(url,imageView);
}else if(bitmap != null){
imageView.setImageBitmap(bitmap);
}
}
public void setImageCache(MenoryCache cache){
mMemeryCache = cache;
}
// 下載圖片
...
}
問題分析:可以看到,ImageLoader 直接依賴了MemoryCache,MemoeryCache是具體實現(xiàn),不是一個抽象類或者接口,當(dāng)MemoryCache不能滿足ImageLoader惡如需要被其他緩存實現(xiàn)替換時,此時就必須要修改ImageLoader的代碼。
而且,抽象接口的定義要具體化,不要定義不需要的接口。讓類的依賴關(guān)系也建立在最小的接口上。而不是提供一大堆接口,這樣很臃腫,也會存在耦合不易修改。
小結(jié):這與前文的開閉原則,其實也是對應(yīng)的。里氏替換原則能夠很好的保證代碼的可拓展性,能隨時替換具體實現(xiàn)并做到不影響調(diào)用端使用者,有擁抱變化的能力。讓代碼更靈活。其核心就是:抽象
1.4.3 通過依賴倒置原則優(yōu)化代碼
見1.2 開閉原則即可
1.5 系統(tǒng)擁有更高的靈活性-【接口隔離原則(ISP)】
1.5.1 定義 什么是接口隔離原則?
接口隔離英文全稱(InterfaceSegregation Principles),縮寫是ISP
定義:一個類對另一個類的依賴應(yīng)該建立在最小的接口上。臃腫的接口,拆分細(xì)化成更小和更具體的接口,這樣客戶端將會只需要知道他們感興趣的方法,接口隔離的原則與目的是系統(tǒng)解開耦合,從而容易被重構(gòu)、更改和重新部署
換種說法:建立單一接口,不要建立龐大臃腫的接口,盡量細(xì)化接口,接口中的方法盡量少。也就是說,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調(diào)用。
采用接口隔離原則對接口進行約束時,要注意以下幾點:
(1)接口盡量小,但是要有限度。對接口進行細(xì)化可以提高程序設(shè)計靈活性,但是如果過小,則會造成接口數(shù)量過多,使設(shè)計復(fù)雜化。所以一定要適度。
(2)為依賴接口的類定制服務(wù),只暴露給調(diào)用的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模塊提供定制服務(wù),才能建立最小的依賴關(guān)系。
(3)提高內(nèi)聚,減少對外交互。使接口用最少的方法去完成最多的事情。
舉個栗子:燈的****開關(guān)、馬桶的沖水按鈕等等。燈的開關(guān)就是專門來控制開關(guān)的,沖水按鈕就是專門沖水的。一看就知道干嘛的。而不是,我想關(guān)燈,要從無數(shù)按鈕中查找。對我來說使用成本太大。而且無法量產(chǎn),這是不科學(xué)的。
1.5.2 代碼舉例說明
// 將圖片韓村到內(nèi)存中
public void put(String url,Bitmap bmp){
FileOutPutStream fileOutPutStream = null ;
try{
fileOutPutStream = new FileOutPutStream(cacheDir+url);
bmp.compress(CompressFormat.PNG,100,fileOutPutStream);
}cache(Exception e){
e.printStackTrace();
}finally{
// 注意這一段代碼
if(fileOutPutStream!=null){
try{
fileOutPutStream.close();
}cache(Exception e){
}
}// end if
}//end if finally
}</pre>
代碼分析:代碼可讀性非常的差,各種try...cache嵌套的都是些簡單的代碼,但是會嚴(yán)重影響代碼的可讀性,寫代碼的時候,也容易發(fā)生錯誤。
追溯fileOutPutStream.close()方法,發(fā)現(xiàn),fileOutPutStream的clsoe方法,實現(xiàn)了Closeable接口,如果我的項目中,而系統(tǒng)中,有100多個類都實現(xiàn)了Closeable接口。這意味著,在使用這些類結(jié)束,最后關(guān)閉這100多個對象的時候,都需要寫上面finally里類似的代碼。
這是無法容忍的。
既然都實現(xiàn)了Closeable接口,干脆把這段代碼抽離出來,新建一個方法統(tǒng)一關(guān)閉這些對象。以后只要是實現(xiàn)了Closeable接口的類,都可以使用。
1.5.3 通過接口隔離原則優(yōu)化代碼
public final class CloseUtils{
private CloseUtils(){ }
/**
* 關(guān)閉Closeable對象
*/
public static void closeQuitely(Closeable closeable){
if(closeable != null){
try{
closeable.close();
}cache(Exception 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);
}cache(Exception e){
e.printStackTrace();
}finally{
CloseUtils.closeQuitely(fileOutPutStream);
}
}
代碼明顯簡潔了很多,并且,這個類可以應(yīng)用到所有實現(xiàn)了Closeable的對象中,保證了代碼的重用性;
小結(jié):CloseUtils的closeQuality方法的基本原理,就在于依賴了Closeable抽象而不是具體實現(xiàn)(其實就是1.4中的依賴倒置原則),建立在最小化依賴原則的基礎(chǔ)上,它只需要知道這個對象是可關(guān)閉的,其它的一概不關(guān)心。這就是接口隔離。
思考:設(shè)想,如果關(guān)閉一個對象時,它卻暴露了其它的接口函數(shù),比如OutPutStream的write方法,這樣使得更多的細(xì)節(jié)暴露在客戶端代碼面前,不僅沒有很好的隱藏實現(xiàn),還增加了接口的使用難度。還有上文說到的ImageLoader中的ImageCache,調(diào)用者ImageLoader,只需要知道該緩存對象有存、取圖片的接口即可,其它的根本不關(guān)心。將龐大的接口,拆分到更細(xì)顆粒度的接口當(dāng)中,這樣代碼就會有更低的耦合性,更高靈活性。
Bob大叔(Robert C Martin)在21世紀(jì)早期,將 單一職責(zé)、開閉原則、里氏替換、接口隔離 以及 依賴倒置5個原則定義了SOLID原則,作為面向?qū)ο笞兂傻?個基本原則。當(dāng)這些原則被啟用時,系統(tǒng)軟件變得更加清晰、簡單、最大程度的擁抱變化。
1.1-1.5的總結(jié)和概括,其實可以為:抽象、單一職責(zé)、最小化。
1.6 更好的拓展性-【迪米特原則(LOD)】
1.6.1 定義,什么是迪米特原則?
定義:迪米特原則英文全稱 (LAW of Demeter) ,縮寫是LOD,也被稱為最少了解原則(Least Knowledage Principle),一個對象應(yīng)該對其它對象有最少的了解**
通俗的講,一個類應(yīng)該對自己需要耦合或者調(diào)用類知道的最少,類的內(nèi)部,如何實現(xiàn)與調(diào)用者或者依賴者沒有關(guān)系、調(diào)用者或者依賴者,只需要知道它需要的方法即可,其具體實現(xiàn),內(nèi)部如何騷造作,一概不管。
類與類之間的關(guān)系越密切,耦合程度就越大,當(dāng)一個類需要改變時,對另一個類的影響也越大。
舉個栗子:圖片加載類緩存類,ImageCache,MemoryCache、DiskCache,對于調(diào)用者ImageLoader,我只需要知道,存圖、取圖 就可以了。你也只需要開放這兩個接口給我就好了。我根本不需要知道你是怎么設(shè)置參數(shù)的、怎么設(shè)置緩存的。我根本不關(guān)心,也不應(yīng)該將這些業(yè)務(wù)邏輯暴露給調(diào)用者,
這樣會增加我使用的成本也會導(dǎo)致不必要的問題發(fā)生。比如調(diào)用者調(diào)用約定方法后,又同時調(diào)用了你提供的其它方法。這就有可能導(dǎo)致不必要的錯誤。