面向?qū)ο罅笤瓌t詳解

1、優(yōu)化代碼的第一步——單一職責(zé)原則

單一職責(zé)原則的英文名稱是Single Responsibility Principle,簡稱SRP。它的定義是:就一個(gè)類而言,應(yīng)該僅有一個(gè)引起它變化的原因。簡單來說,一個(gè)類中應(yīng)該是一組相關(guān)性很高的函數(shù)、數(shù)據(jù)的封裝。就像秦小波老師在《設(shè)計(jì)模式之禪》中說的:“這是一個(gè)備受爭議卻又及其重要的原則。只要你想和別人爭執(zhí)、慪氣或者是吵架,這個(gè)原則是屢試不爽的”。因?yàn)閱我宦氊?zé)的劃分界限并不是總是那么清晰,很多時(shí)候都是需要靠個(gè)人經(jīng)驗(yàn)來界定。當(dāng)然,最大的問題就是對職責(zé)的定義,什么是類的職責(zé),以及怎么劃分類的職責(zé)。
對于計(jì)算機(jī)技術(shù),通常只單純地學(xué)習(xí)理論知識并不能很好地領(lǐng)會其深意,只有自己動手實(shí)踐,并在實(shí)際運(yùn)用中發(fā)現(xiàn)問題、解決問題、思考問題,才能夠?qū)⒅R吸收到自己的腦海中。下面以我的朋友小民的事跡說起。

自從Android系統(tǒng)發(fā)布以來,小民就是Android的鐵桿粉絲,于是在大學(xué)期間一直保持著對Android的關(guān)注,并且利用課余時(shí)間做些小項(xiàng)目,鍛煉自己的實(shí)戰(zhàn)能力。畢業(yè)后,小民如愿地加入了心儀的公司,并且投入到了他熱愛的Android應(yīng)用開發(fā)行業(yè)中。將愛好、生活、事業(yè)融為一體,小民的第一份工作也算是順風(fēng)順?biāo)磺斜M在掌握中。
在經(jīng)歷過一周的適應(yīng)期以及熟悉公司的產(chǎn)品、開發(fā)規(guī)范之后,小民的開發(fā)工作就正式開始了。小民的主管是個(gè)工作經(jīng)驗(yàn)豐富的技術(shù)專家,對于小民的工作并不是很滿意,尤其小民最薄弱的面向?qū)ο笤O(shè)計(jì),而Android開發(fā)又是使用Java語言,什么抽象、接口、六大原則、23種設(shè)計(jì)模式等名詞把小民弄得暈頭轉(zhuǎn)向。小民自己也察覺到了自己的問題所在,于是,小民的主管決定先讓小民做一個(gè)小項(xiàng)目來鍛煉鍛煉這方面的能力。正所謂養(yǎng)兵千日用兵一時(shí),磨刀不誤砍柴工,小民的開發(fā)之路才剛剛開始。

在經(jīng)過一番思考之后,主管挑選了使用范圍廣、難度也適中的ImageLoader(圖片加載)作為小民的訓(xùn)練項(xiàng)目。既然要訓(xùn)練小民的面向?qū)ο笤O(shè)計(jì),那么就必須考慮到可擴(kuò)展性、靈活性,而檢測這一切是否符合需求的最好途徑就是開源。用戶不斷地提出需求、反饋問題,小民的項(xiàng)目需要不斷升級以滿足用戶需求,并且要保證系統(tǒng)的穩(wěn)定性、靈活性。在主管跟小民說了這一特殊任務(wù)之后,小民第一次感到了壓力,“生活不容易吶!”年僅22歲至今未婚的小民發(fā)出了如此深刻的感嘆!

挑戰(zhàn)總是要面對的,何況是從來不服輸?shù)男∶瘛V鞴艿囊蠛芎唵危∶駥?shí)現(xiàn)圖片加載,并且要將圖片緩存起來。在分析了需求之后,小民一下就放心下來了,“這么簡單,原來我還以為很難呢……”小民胸有成足的喃喃自語。在經(jīng)歷了十分鐘的編碼之后,小民寫下了如下代碼:

/**
 * 圖片加載類
 */
public class ImageLoader {
    // 圖片緩存
    LruCache<String, Bitmap> mImageCache;
    // 線程池,線程數(shù)量為CPU的數(shù)量
    ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
            // 計(jì)算可使用的最大內(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 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軟件進(jìn)行版本控制,將工程托管到github上,伴隨著git push命令的完成,小民的ImageLoader 0.1版本就正式發(fā)布了!如此短的時(shí)間內(nèi)就完成了這個(gè)任務(wù),而且還是一個(gè)開源項(xiàng)目,小民暗暗自喜,幻想著待會兒主管的稱贊。

在小民給主管報(bào)告了ImageLoader的發(fā)布消息的幾分鐘之后,主管就把小民叫到了會議室。這下小民納悶了,怎么夸人還需要到會議室。“小民,你的ImageLoader耦合太嚴(yán)重啦!簡直就沒有設(shè)計(jì)可言,更不要說擴(kuò)展性、靈活性了。所有的功能都寫在一個(gè)類里怎么行呢,這樣隨著功能的增多,ImageLoader類會越來越大,代碼也越來越復(fù)雜,圖片加載系統(tǒng)就越來越脆弱……”Duang,這簡直就是當(dāng)頭棒喝,小民的腦海里已經(jīng)聽不清主管下面說的內(nèi)容了,只是覺得自己之前沒有考慮清楚就匆匆忙忙完成任務(wù),而且把任務(wù)想得太簡單了。

“你還是把ImageLoader拆分一下,把各個(gè)功能獨(dú)立出來,讓它們滿足單一職責(zé)原則。”主管最后說道。小民是個(gè)聰明人,敏銳地捕捉到了單一職責(zé)原則這個(gè)關(guān)鍵詞。用Google搜索了一些優(yōu)秀資料之后總算是對單一職責(zé)原則有了一些認(rèn)識。于是打算對ImageLoader進(jìn)行一次重構(gòu)。這次小民不敢過于草率,也是先畫了一幅UML圖,如圖1-1所示。



圖1-1

ImageLoader代碼修改如下所示:

/**
 * 圖片加載類
 */
public  class ImageLoader {
    // 圖片緩存
    ImageCache mImageCache = new ImageCache() ;
    // 線程池,線程數(shù)量為CPU的數(shù)量
    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;
    }
}   

并且添加了一個(gè)ImageCache類用于處理圖片緩存,具體代碼如下:

public class ImageCache {
    // 圖片LRU緩存
    LruCache<String, Bitmap> mImageCache;

    public ImageCache() {
        initImageCache();
    }

    private void initImageCache() {
         // 計(jì)算可使用的最大內(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 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只負(fù)責(zé)圖片加載的邏輯,而ImageCache只負(fù)責(zé)處理圖片緩存的邏輯,這樣ImageLoader的代碼量變少了,職責(zé)也清晰了,當(dāng)與緩存相關(guān)的邏輯需要改變時(shí),不需要修改ImageLoader類,而圖片加載的邏輯需要修改時(shí)也不會影響到緩存處理邏輯。主管在審核了小民的第一次重構(gòu)之后,對小民的工作給予了表揚(yáng),大致意思是結(jié)構(gòu)變得清晰了許多,但是可擴(kuò)展性還是比較欠缺,雖然沒有得到主管的完全肯定,但也是頗有進(jìn)步,再考慮到自己確實(shí)有所收獲,小民原本沮喪的心里也略微地好轉(zhuǎn)起來。

從上述的例子中我們能夠體會到,單一職責(zé)所表達(dá)出的用意就是“單一”二字。正如上文所說,如何劃分一個(gè)類、一個(gè)函數(shù)的職責(zé),每個(gè)人都有自己的看法,這需要根據(jù)個(gè)人經(jīng)驗(yàn)、具體的業(yè)務(wù)邏輯而定。但是,它也有一些基本的指導(dǎo)原則,例如,兩個(gè)完全不一樣的功能就不應(yīng)該放在一個(gè)類中。一個(gè)類中應(yīng)該是一組相關(guān)性很高的函數(shù)、數(shù)據(jù)的封裝。工程師可以不斷地審視自己的代碼,根據(jù)具體的業(yè)務(wù)、功能對類進(jìn)行相應(yīng)的拆分,我想這會是你優(yōu)化代碼邁出的第一步。

2、讓程序更穩(wěn)定、更靈活——開閉原則

開閉原則的英文全稱是Open Close Principle,簡稱OCP,它是Java世界里最基礎(chǔ)的設(shè)計(jì)原則,它指導(dǎo)我們?nèi)绾谓⒁粋€(gè)穩(wěn)定的、靈活的系統(tǒng)。開閉原則的定義是:軟件中的對象(類、模塊、函數(shù)等)應(yīng)該對于擴(kuò)展是開放的,但是,對于修改是封閉的。在軟件的生命周期內(nèi),因?yàn)樽兓⑸壓途S護(hù)等原因需要對軟件原有代碼進(jìn)行修改時(shí),可能會將錯(cuò)誤引入原本已經(jīng)經(jīng)過測試的舊代碼中,破壞原有系統(tǒng)。因此,當(dāng)軟件需要變化時(shí),我們應(yīng)該盡量通過擴(kuò)展的方式來實(shí)現(xiàn)變化,而不是通過修改已有的代碼來實(shí)現(xiàn)。當(dāng)然,在現(xiàn)實(shí)開發(fā)中,只通過繼承的方式來升級、維護(hù)原有系統(tǒng)只是一個(gè)理想化的愿景,因此,在實(shí)際的開發(fā)過程中,修改原有代碼、擴(kuò)展代碼往往是同時(shí)存在的。

軟件開發(fā)過程中,最不會變化的就是變化本身。產(chǎn)品需要不斷地升級、維護(hù),沒有一個(gè)產(chǎn)品從第一版本開發(fā)完就再沒有變化了,除非在下個(gè)版本誕生之前它已經(jīng)被終止。而產(chǎn)品需要升級,修改原來的代碼就可能會引發(fā)其他的問題。那么如何確保原有軟件模塊的正確性,以及盡量少地影響原有模塊,答案就是盡量遵守本章要講述的開閉原則。

勃蘭特·梅耶在1988年出版的《面向?qū)ο筌浖?gòu)造》一書中提出這一原則。這一想法認(rèn)為,一旦完成,一個(gè)類的實(shí)現(xiàn)只應(yīng)該因錯(cuò)誤而被修改,新的或者改變的特性應(yīng)該通過新建不同的類實(shí)現(xiàn)。新建的類可以通過繼承的方式來重用原類的代碼。顯然,梅耶的定義提倡實(shí)現(xiàn)繼承,已存在的實(shí)現(xiàn)對于修改是封閉的,但是新的實(shí)現(xiàn)類可以通過覆寫父類的接口應(yīng)對變化。
說了這么多,想必大家還是半懂不懂,還是讓我們以一個(gè)簡單示例說明一下吧。

在對ImageLoader進(jìn)行了一次重構(gòu)之后,小民的這個(gè)開源庫獲得了一些用戶。小民第一次感受到自己發(fā)明“輪子”的快感,對開源的熱情也越發(fā)高漲起來!通過動手實(shí)現(xiàn)一些開源庫來深入學(xué)習(xí)相關(guān)技術(shù),不僅能夠提升自我,也能更好地將這些技術(shù)運(yùn)用到工作中,從而開發(fā)出更穩(wěn)定、優(yōu)秀的應(yīng)用,這就是小民的真實(shí)想法。

小民第一輪重構(gòu)之后的ImageLoader職責(zé)單一、結(jié)構(gòu)清晰,不僅獲得了主管的一點(diǎn)肯定,還得到了用戶的夸獎(jiǎng),算是個(gè)不錯(cuò)的開始。隨著用戶的增多,有些問題也暴露出來了,小民的緩存系統(tǒng)就是大家“吐槽”最多的地方。通過內(nèi)存緩存解決了每次從網(wǎng)絡(luò)加載圖片的問題,但是,Android應(yīng)用的內(nèi)存很有限,且具有易失性,即當(dāng)應(yīng)用重新啟動之后,原來已經(jīng)加載過的圖片將會丟失,這樣重啟之后就需要重新下載!這又會導(dǎo)致加載緩慢、耗費(fèi)用戶流量的問題。小民考慮引入SD卡緩存,這樣下載過的圖片就會緩存到本地,即使重啟應(yīng)用也不需要重新下載了!小民在和主管討論了該問題之后就投入了編程中,下面就是小民的代碼。
DiskCache.java類,將圖片緩存到SD卡中:

public class DiskCache {
    // 為了簡單起見臨時(shí)寫個(gè)路徑,在開發(fā)中請避免這種寫法 !
    static String cacheDir = "sdcard/cache/";
     // 從緩存中獲取圖片
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }

    // 將圖片緩存到內(nèi)存中
    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();
             }
          }
      }
    }
}

因?yàn)樾枰獙D片緩存到SD卡中,所以,ImageLoader代碼有所更新,具體代碼如下:

public class ImageLoader {
    // 內(nèi)存緩存
    ImageCache mImageCache = new ImageCache();
    // SD卡緩存
    DiskCache mDiskCache = new DiskCache();
    // 是否使用SD卡緩存
    boolean isUseDiskCache = false;
    // 線程池,線程數(shù)量為CPU的數(shù)量
    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;
       }
        // 沒有緩存,則提交給線程池進(jìn)行下載
    }

    public void useDiskCache(boolean useDiskCache) {
        isUseDiskCache = useDiskCache ;
    }
}

從上述的代碼中可以看到,僅僅新增了一個(gè)DiskCache類和往ImageLoader類中加入了少量代碼就添加了SD卡緩存的功能,用戶可以通過useDiskCache方法來對使用哪種緩存進(jìn)行設(shè)置,例如:

ImageLoader imageLoader = new ImageLoader() ;
 // 使用SD卡緩存
imageLoader.useDiskCache(true);
// 使用內(nèi)存緩存
imageLoader.useDiskCache(false);

通過useDiskCache方法可以讓用戶設(shè)置不同的緩存,非常方便啊!小民對此很滿意,于是提交給主管做代碼審核。“小民,你思路是對的,但是有些明顯的問題,就是使用內(nèi)存緩存時(shí)用戶就不能使用SD卡緩存,類似的,使用SD卡緩存時(shí)用戶就不能使用內(nèi)存緩存。用戶需要這兩種策略的綜合,首先緩存優(yōu)先使用內(nèi)存緩存,如果內(nèi)存緩存沒有圖片再使用SD卡緩存,如果SD卡中也沒有圖片最后才從網(wǎng)絡(luò)上獲取,這才是最好的緩存策略。”主管真是一針見血,小民這時(shí)才如夢初醒,剛才還得意洋洋的臉上突然有些泛紅……
于是小民按照主管的指點(diǎn)新建了一個(gè)雙緩存類DoudleCache,具體代碼如下:

/**
 * 雙緩存。獲取圖片時(shí)先從內(nèi)存緩存中獲取,如果內(nèi)存中沒有緩存該圖片,再從SD卡中獲取。
 *  緩存圖片也是在內(nèi)存和SD卡中都緩存一份
 */
public class DoubleCache {
    ImageCache mMemoryCache = new ImageCache();
    DiskCache mDiskCache = new DiskCache();

    // 先從內(nèi)存緩存中獲取圖片,如果沒有,再從SD卡中獲取
    public   Bitmap get(String url) {
       Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap == null) {
            bitmap = mDiskCache.get(url);
        }
        return  bitmap;
    }

    // 將圖片緩存到內(nèi)存和SD卡中
    public void put(String url, Bitmap bmp) {
        mMemoryCache.put(url, bmp);
        mDiskCache.put(url, bmp);
   }
}

我們再看看最新的ImageLoader類吧,代碼更新也不多:

public class ImageLoader {
    // 內(nèi)存緩存
    ImageCache mImageCache = new ImageCache();
    // SD卡緩存
    DiskCache mDiskCache = new DiskCache();
    // 雙緩存
    DoubleCache mDoubleCache = new DoubleCache() ;
    // 使用SD卡緩存
    boolean isUseDiskCache = false;
    // 使用雙緩存
    boolean isUseDoubleCache = false;
    // 線程池,線程數(shù)量為CPU的數(shù)量
    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);
        }
        // 沒有緩存,則提交給線程池進(jìn)行異步下載圖片
    }

    public void useDiskCache(boolean useDiskCache) {
        isUseDiskCache = useDiskCache ;
    }

    public void useDoubleCache(boolean useDoubleCache) {
        isUseDoubleCache = useDoubleCache ;
    }
}

通過增加短短幾句代碼和幾處修改就完成了如此重要的功能。小民已越發(fā)覺得自己Android開發(fā)已經(jīng)到了的得心應(yīng)手的境地,不僅感覺一陣春風(fēng)襲來,他那飄逸的頭發(fā)一下從他的眼前拂過,小民感覺今天天空比往常敞亮許多。

“小民,你每次加新的緩存方法時(shí)都要修改原來的代碼,這樣很可能會引入Bug,而且會使原來的代碼邏輯變得越來越復(fù)雜,按照你這樣的方法實(shí)現(xiàn),用戶也不能自定義緩存實(shí)現(xiàn)呀!”到底是主管水平高,一語道出了小民這緩存設(shè)計(jì)上的問題。

我們還是來分析一下小民的程序,小民每次在程序中加入新的緩存實(shí)現(xiàn)時(shí)都需要修改ImageLoader類,然后通過一個(gè)布爾變量來讓用戶使用哪種緩存,因此,就使得在ImageLoader中存在各種if-else判斷,通過這些判斷來確定使用哪種緩存。隨著這些邏輯的引入,代碼變得越來越復(fù)雜、脆弱,如果小民一不小心寫錯(cuò)了某個(gè)if條件(條件太多,這是很容易出現(xiàn)的),那就需要更多的時(shí)間來排除。整個(gè)ImageLoader類也會變得越來越臃腫。最重要的是用戶不能自己實(shí)現(xiàn)緩存注入到ImageLoader中,可擴(kuò)展性可是框架的最重要特性之一。

“軟件中的對象(類、模塊、函數(shù)等)應(yīng)該對于擴(kuò)展是開放的,但是對于修改是封閉的,這就是開放-關(guān)閉原則。也就是說,當(dāng)軟件需要變化時(shí),我們應(yīng)該盡量通過擴(kuò)展的方式來實(shí)現(xiàn)變化,而不是通過修改已有的代碼來實(shí)現(xiàn)。”小民的主管補(bǔ)充到,小民聽得云里霧里的。主管看小民這等反應(yīng),于是親自“操刀”,為他畫下了如圖1-2的UML圖。


圖1-2

小民看到圖1-2似乎明白些什么,但是又不是太明確如何修改程序。主管看到小民這般模樣只好親自上陣,帶著小民把ImageLoader程序按照圖1-2進(jìn)行了一次重構(gòu)。具體代碼如下:

public class ImageLoader {
    // 圖片緩存
    ImageCache mImageCache = new MemoryCache();
    // 線程池,線程數(shù)量為CPU的數(shù)量
    ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());

    // 注入緩存實(shí)現(xiàn)
    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;
    }
}

經(jīng)過這次重構(gòu),沒有了那么多的if-else語句,沒有了各種各樣的緩存實(shí)現(xiàn)對象、布爾變量,代碼確實(shí)清晰、簡單了很多,小民對主管的崇敬之情又“泛濫”了起來。需要注意的是,這里的ImageCache類并不是小民原來的那個(gè)ImageCache,這次程序重構(gòu)主管把它提取成一個(gè)圖片緩存的接口,用來抽象圖片緩存的功能。我們看看該接口的聲明:

public interface ImageCache {
    public Bitmap get(String url);
    public void put(String url, Bitmap bmp);
}

ImageCache接口簡單定義了獲取、緩存圖片兩個(gè)函數(shù),緩存的key是圖片的url,值是圖片本身。內(nèi)存緩存、SD卡緩存、雙緩存都實(shí)現(xiàn)了該接口,我們看看這幾個(gè)緩存實(shí)現(xiàn):

// 內(nèi)存緩存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();

    // 先從內(nèi)存緩存中獲取圖片,如果沒有,再從SD卡中獲取
    public Bitmap get(String url) {
       Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap == null) {
            bitmap = mDiskCache.get(url);
       }
        return bitmap;
     }

    // 將圖片緩存到內(nèi)存和SD卡中
    public void put(String url, Bitmap bmp) {
        mMemoryCache.put(url, bmp);
        mDiskCache.put(url, bmp);
    }
}

細(xì)心的朋友可能注意到了,ImageLoader類中增加了一個(gè)setImageCache(ImageCache cache)函數(shù),用戶可以通過該函數(shù)設(shè)置緩存實(shí)現(xiàn),也就是通常說的依賴注入。下面就看看用戶是如何設(shè)置緩存實(shí)現(xiàn)的:

ImageLoader imageLoader = new ImageLoader() ;
        // 使用內(nèi)存緩存
imageLoader.setImageCache(new MemoryCache());
        // 使用SD卡緩存
imageLoader.setImageCache(new DiskCache());
        // 使用雙緩存
imageLoader.setImageCache(new DoubleCache());
        // 使用自定義的圖片緩存實(shí)現(xiàn)
imageLoader.setImageCache(new ImageCache() {

            @Override
        public void put(String url, Bitmap bmp) {
            // 緩存圖片
       }

            @Override
        public Bitmap get(String url) {
            return null/*從緩存中獲取圖片*/;
       }
    });

在上述代碼中,通過setImageCache(ImageCache cache)方法注入不同的緩存實(shí)現(xiàn),這樣不僅能夠使ImageLoader更簡單、健壯,也使得ImageLoader的可擴(kuò)展性、靈活性更高。MemoryCache、DiskCache、DoubleCache緩存圖片的具體實(shí)現(xiàn)完全不一樣,但是,它們的一個(gè)特點(diǎn)是都實(shí)現(xiàn)了ImageCache接口。當(dāng)用戶需要自定義實(shí)現(xiàn)緩存策略時(shí),只需要新建一個(gè)實(shí)現(xiàn)ImageCache接口的類,然后構(gòu)造該類的對象,并且通過setImageCache(ImageCache cache)注入到ImageLoader中,這樣ImageLoader就實(shí)現(xiàn)了變化萬千的緩存策略,而擴(kuò)展這些緩存策略并不會導(dǎo)致ImageLoader類的修改。經(jīng)過這次重構(gòu),小民的ImageLoader已經(jīng)基本算合格了。咦!這不就是主管說的開閉原則么!“軟件中的對象(類、模塊、函數(shù)等)應(yīng)該對于擴(kuò)展是開放的,但是對于修改是封閉的。而遵循開閉原則的重要手段應(yīng)該是通過抽象……”小民細(xì)聲細(xì)語的念叨中,陷入了思索中……

開閉原則指導(dǎo)我們,當(dāng)軟件需要變化時(shí),應(yīng)該盡量通過擴(kuò)展的方式來實(shí)現(xiàn)變化,而不是通過修改已有的代碼來實(shí)現(xiàn)。這里的“應(yīng)該盡量”4個(gè)字說明OCP原則并不是說絕對不可以修改原始類的,當(dāng)我們嗅到原來的代碼“腐化氣味”時(shí),應(yīng)該盡早地重構(gòu),以使得代碼恢復(fù)到正常的“進(jìn)化”軌道,而不是通過繼承等方式添加新的實(shí)現(xiàn),這會導(dǎo)致類型的膨脹以及歷史遺留代碼的冗余。我們的開發(fā)過程中也沒有那么理想化的狀況,完全地不用修改原來的代碼,因此,在開發(fā)過程中需要自己結(jié)合具體情況進(jìn)行考量,是通過修改舊代碼還是通過繼承使得軟件系統(tǒng)更穩(wěn)定、更靈活,在保證去除“代碼腐化”的同時(shí),也保證原有模塊的正確性。

3、構(gòu)建擴(kuò)展性更好的系統(tǒng)——里氏替換原則

里氏替換原則英文全稱是Liskov Substitution Principle,簡稱LSP。它的第一種定義是:如果對每一個(gè)類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程序P在所有的對象o1都代換成o2時(shí),程序P的行為沒有發(fā)生變化,那么類型S是類型T的子類型。上面這種描述確實(shí)不太好理解,理論家有時(shí)候容易把問題抽象化,本來挺容易理解的事讓他們一概括就弄得拗口了。我們再看看另一個(gè)直截了當(dāng)?shù)亩x。里氏替換原則第二種定義:所有引用基類的地方必須能透明地使用其子類的對象。

我們知道,面向?qū)ο蟮恼Z言的三大特點(diǎn)是繼承、封裝、多態(tài),里氏替換原則就是依賴于繼承、多態(tài)這兩大特性。里氏替換原則簡單來說就是,所有引用基類的地方必須能透明地使用其子類的對象。通俗點(diǎn)講,只要父類能出現(xiàn)的地方子類就可以出現(xiàn),而且替換為子類也不會產(chǎn)生任何錯(cuò)誤或異常,使用者可能根本就不需要知道是父類還是子類。但是,反過來就不行了,有子類出現(xiàn)的地方,父類未必就能適應(yīng)。說了那么多,其實(shí)最終總結(jié)就兩個(gè)字:抽象。
小民為了深入地了解Android中的Window與View的關(guān)系特意寫了一個(gè)簡單示例,為了便于理解,我們先看如圖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){
        // 測量視圖大小
    }
}

// 按鈕類的具體實(shí)現(xiàn)
public class Button extends View {
    public void draw(){
        // 繪制按鈕
    }
}
// TextView的具體實(shí)現(xiàn)
public class TextView extends View {
    public void draw(){
        // 繪制文本
    }
}

上述示例中,Window依賴于View,而View定義了一個(gè)視圖抽象,measure是各個(gè)子類共享的方法,子類通過覆寫View的draw方法實(shí)現(xiàn)具有各自特色的功能,在這里,這個(gè)功能就是繪制自身的內(nèi)容。任何繼承自View類的子類都可以設(shè)置給show方法,也就我們所說的里氏替換。通過里氏替換,就可以自定義各式各樣、千變?nèi)f化的View,然后傳遞給Window,Window負(fù)責(zé)組織View,并且將View顯示到屏幕上。
里氏替換原則的核心原理是抽象,抽象又依賴于繼承這個(gè)特性,在OOP當(dāng)中,繼承的優(yōu)缺點(diǎn)都相當(dāng)明顯。
優(yōu)點(diǎn)如下:

  • (1)代碼重用,減少創(chuàng)建類的成本,每個(gè)子類都擁有父類的方法和屬性;
  • (2)子類與父類基本相似,但又與父類有所區(qū)別;
  • (3)提高代碼的可擴(kuò)展性。

繼承的缺點(diǎn):

  • (1)繼承是侵入性的,只要繼承就必須擁有父類的所有屬性和方法;
  • (2)可能造成子類代碼冗余、靈活性降低,因?yàn)樽宇惐仨殦碛懈割惖膶傩院头椒ā?/li>

事物總是具有兩面性,如何權(quán)衡利與弊都是需要根據(jù)具體場景來做出選擇并加以處理。里氏替換原則指導(dǎo)我們構(gòu)建擴(kuò)展性更好的軟件系統(tǒng),我們還是接著上面的ImageLoader來做說明。
上文的圖1-2也很好地反應(yīng)了里氏替換原則,即MemoryCache、DiskCache、DoubleCache都可以替換ImageCache的工作,并且能夠保證行為的正確性。ImageCache建立了獲取緩存圖片、保存緩存圖片的接口規(guī)范,MemoryCache等根據(jù)接口規(guī)范實(shí)現(xiàn)了相應(yīng)的功能,用戶只需要在使用時(shí)指定具體的緩存對象就可以動態(tài)地替換ImageLoader中的緩存策略。這就使得ImageLoader的緩存系統(tǒng)具有了無線的可能性,也就是保證了可擴(kuò)展性。

想象一個(gè)場景,當(dāng)ImageLoader中的setImageCache(ImageCache cache)中的cache對象不能夠被子類所替換,那么用戶如何設(shè)置不同的緩存對象以及用戶如何自定義自己的緩存實(shí)現(xiàn),通過1.3節(jié)中的useDiskCache方法嗎?顯然不是的,里氏替換原則就為這類問題提供了指導(dǎo)原則,也就是建立抽象,通過抽象建立規(guī)范,具體的實(shí)現(xiàn)在運(yùn)行時(shí)替換掉抽象,保證系統(tǒng)的高擴(kuò)展性、靈活性。開閉原則和里氏替換原則往往是生死相依、不棄不離的,通過里氏替換來達(dá)到對擴(kuò)展開放,對修改關(guān)閉的效果。然而,這兩個(gè)原則都同時(shí)強(qiáng)調(diào)了一個(gè)OOP的重要特性——抽象,因此,在開發(fā)過程中運(yùn)用抽象是走向代碼優(yōu)化的重要一步。

4、 讓項(xiàng)目擁有變化的能力——依賴倒置原則

依賴倒置原則英文全稱是Dependence Inversion Principle,簡稱DIP。依賴反轉(zhuǎn)原則指代了一種特定的解耦形式,使得高層次的模塊不依賴于低層次的模塊的實(shí)現(xiàn)細(xì)節(jié)的目的,依賴模塊被顛倒了。這個(gè)概念有點(diǎn)不好理解,這到底是什么意思呢?
依賴倒置原則的幾個(gè)關(guān)鍵點(diǎn):

  • (1)高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;
  • (2)抽象不應(yīng)該依賴細(xì)節(jié);
  • (3)細(xì)節(jié)應(yīng)該依賴抽象。

在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被實(shí)例化的;細(xì)節(jié)就是實(shí)現(xiàn)類,實(shí)現(xiàn)接口或繼承抽象類而產(chǎn)生的類就是細(xì)節(jié),其特點(diǎn)就是,可以直接被實(shí)例化,也就是可以加上一個(gè)關(guān)鍵字 new 產(chǎn)生一個(gè)對象。高層模塊就是調(diào)用端,低層模塊就是具體實(shí)現(xiàn)類。依賴倒置原則在 Java 語言中的表現(xiàn)就是:模塊間的依賴通過抽象發(fā)生,實(shí)現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過接口或抽象類產(chǎn)生的。這又是一個(gè)將理論抽象化的實(shí)例,其實(shí)一句話就可以概括:面向接口編程,或者說是面向抽象編程,這里的抽象指的是接口或者抽象類。面向接口編程是面向?qū)ο缶柚唬簿褪巧厦鎯晒?jié)強(qiáng)調(diào)的抽象。

如果在類與類直接依賴于細(xì)節(jié),那么它們之間就有直接的耦合,當(dāng)具體實(shí)現(xiàn)需要變化時(shí),意味著在這要同時(shí)修改依賴者的代碼,并且限制了系統(tǒng)的可擴(kuò)展性。我們看1.3節(jié)的圖1-3中,ImageLoader直接依賴于MemoryCache,這個(gè)MemoryCache是一個(gè)具體實(shí)現(xiàn),而不是一個(gè)抽象類或者接口。這導(dǎo)致了ImageLoader直接依賴了具體細(xì)節(jié),當(dāng)MemoryCache不能滿足ImageLoader而需要被其他緩存實(shí)現(xiàn)替換時(shí),此時(shí)就必須修改ImageLoader的代碼,例如:

public class ImageLoader {
    // 內(nèi)存緩存 ( 直接依賴于細(xì)節(jié) )
    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 ;
    }
    // 代碼省略
}

隨著產(chǎn)品的升級,用戶發(fā)現(xiàn)MemoryCache已經(jīng)不能滿足需求,用戶需要小民的ImageLoader可以將圖片同時(shí)緩存到內(nèi)存和SD卡中,或者可以讓用戶自定義實(shí)現(xiàn)緩存。此時(shí),我們的MemoryCache這個(gè)類名不僅不能夠表達(dá)內(nèi)存緩存和SD卡緩存的意義,也不能夠滿足功能。另外,用戶需要自定義緩存實(shí)現(xiàn)時(shí)還必須繼承自MemoryCache,而用戶的緩存實(shí)現(xiàn)可不一定與內(nèi)存緩存有關(guān),這在命名上的限制也讓用戶體驗(yàn)不好。重構(gòu)的時(shí)候到了!小民的第一種方案是將MemoryCache修改為DoubleCache,然后在DoubleCache中實(shí)現(xiàn)具體的緩存功能。我們需要將ImageLoader修改如下:

public class ImageLoader {
    // 雙緩存 ( 直接依賴于細(xì)節(jié) )
    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中緩存類的具體實(shí)現(xiàn),輕輕松松就滿足了用戶需求。等等!這不還是依賴于具體的實(shí)現(xiàn)類(DoubleCache)嗎?當(dāng)用戶的需求再次變化時(shí),我們又要通過修改緩存實(shí)現(xiàn)類和ImageLoader代碼來實(shí)現(xiàn)?修改原有代碼不是違反了1.3節(jié)中的開閉原則嗎?小民突然醒悟了過來,低下頭思索著如何才能讓緩存系統(tǒng)更靈活、擁抱變化……

當(dāng)然,這些都是在主管給出圖1-2(1.3節(jié))以及相應(yīng)的代碼之前,小民體驗(yàn)的煎熬過程。既然是這樣,那顯然主管給出的解決方案就能夠讓緩存系統(tǒng)更加靈活。一句話概括起來就是:依賴抽象,而不依賴具體實(shí)現(xiàn)。針對于圖片緩存,主管建立的ImageCache抽象,該抽象中增加了get和put方法用以實(shí)現(xiàn)圖片的存取。每種緩存實(shí)現(xiàn)都必須實(shí)現(xiàn)這個(gè)接口,并且實(shí)現(xiàn)自己的存取方法。當(dāng)用戶需要使用不同的緩存實(shí)現(xiàn)時(shí),直接通過依賴注入即可,保證了系統(tǒng)的靈活性。我們再來簡單回顧一下相關(guān)代碼:

ImageCache緩存抽象:

public interface ImageCache {
    public Bitmap get(String url);
    public void put(String url, Bitmap bmp);
}

ImageLoader類:

public class ImageLoader {
    // 圖片緩存類,依賴于抽象,并且有一個(gè)默認(rèn)的實(shí)現(xiàn)
    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);
       }
    }
    
    /**
     * 設(shè)置緩存策略,依賴于抽象
     */
    public void setImageCache(ImageCache cache) {
        mCache = cache;
    }
    // 代碼省略
}

在這里,我們建立了ImageCache抽象,并且讓ImageLoader依賴于抽象而不是具體細(xì)節(jié)。當(dāng)需求發(fā)生變更時(shí),小民只需要實(shí)現(xiàn)ImageCahce類或者繼承其他已有的ImageCache子類完成相應(yīng)的緩存功能,然后將具體的實(shí)現(xiàn)注入到ImageLoader即可實(shí)現(xiàn)緩存功能的替換,這就保證了緩存系統(tǒng)的高可擴(kuò)展性,擁有了擁抱變化的能力,而這一切的基本指導(dǎo)原則就是我們的依賴倒置原則。從上述幾節(jié)中我們發(fā)現(xiàn),要想讓我們的系統(tǒng)更為靈活,抽象似乎成了我們唯一的手段。

5、系統(tǒng)有更高的靈活性——接口隔離原則

接口隔離原則英文全稱是InterfaceSegregation Principles,簡稱ISP。它的定義是:客戶端不應(yīng)該依賴它不需要的接口。另一種定義是:類間的依賴關(guān)系應(yīng)該建立在最小的接口上。接口隔離原則將非常龐大、臃腫的接口拆分成為更小的和更具體的接口,這樣客戶將會只需要知道他們感興趣的方法。接口隔離原則的目的是系統(tǒng)解開耦合,從而容易重構(gòu)、更改和重新部署。

接口隔離原則說白了就是,讓客戶端依賴的接口盡可能地小,這樣說可能還是有點(diǎn)抽象,我們還是以一個(gè)示例來說明一下。在此之前我們來說一個(gè)場景,在Java 6以及之前的JDK版本,有一個(gè)非常討厭的問題,那就是在使用了OutputStream或者其他可關(guān)閉的對象之后,我們必須保證它們最終被關(guān)閉了,我們的SD卡緩存類中就有這樣的代碼:

// 將圖片緩存到內(nèi)存中
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嵌套,都是些簡單的代碼,但是會嚴(yán)重影響代碼的可讀性,并且多層級的大括號很容易將代碼寫到錯(cuò)誤的層級中。大家應(yīng)該對這類代碼也非常反感,那我們看看如何解決這類問題。
我們可能知道Java中有一個(gè)Closeable接口,該接口標(biāo)識了一個(gè)可關(guān)閉的對象,它只有一個(gè)close方法,如圖1-4所示。
我們要講的FileOutputStream類就實(shí)現(xiàn)了這個(gè)接口,我們從圖1-4中可以看到,還有一百多個(gè)類實(shí)現(xiàn)了Closeable這個(gè)接口,這意味著,在關(guān)閉這一百多個(gè)類型的對象時(shí),都需要寫出像put方法中finally代碼段那樣的代碼。這還了得!你能忍,反正小民是忍不了的!于是小民打算要發(fā)揮他的聰明才智解決這個(gè)問題,既然都是實(shí)現(xiàn)了Closeable接口,那只要我建一個(gè)方法統(tǒng)一來關(guān)閉這些對象不就可以了么?說干就干,于是小民寫下來如下的工具類:

▲圖1-4

public final class CloseUtils {

    Private CloseUtils() { }
    
    /**
     * 關(guān)閉Closeable對象
     * @param closeable
     */
    public static void closeQuietly(Closeable closeable) {
        if (null != closeable) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
       }
    }
}

我們再看看把這段代碼運(yùn)用到上述的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);
   }
}

代碼簡潔了很多!而且這個(gè)closeQuietly方法可以運(yùn)用到各類可關(guān)閉的對象中,保證了代碼的重用性。CloseUtils的closeQuietly方法的基本原理就是依賴于Closeable抽象而不是具體實(shí)現(xiàn)(這不是1.4節(jié)中的依賴倒置原則么),并且建立在最小化依賴原則的基礎(chǔ),它只需要知道這個(gè)對象是可關(guān)閉,其他的一概不關(guān)心,也就是這里的接口隔離原則。

試想一下,如果在只是需要關(guān)閉一個(gè)對象時(shí),它卻暴露出了其他的接口函數(shù),比如OutputStream的write方法,這就使得更多的細(xì)節(jié)暴露在客戶端代碼面前,不僅沒有很好地隱藏實(shí)現(xiàn),還增加了接口的使用難度。而通過Closeable接口將可關(guān)閉的對象抽象起來,這樣只需要客戶端依賴于Closeable就可以對客戶端隱藏其他的接口信息,客戶端代碼只需要知道這個(gè)對象可關(guān)閉(只可調(diào)用close方法)即可。小民ImageLoader中的ImageCache就是接口隔離原則的運(yùn)用,ImageLoader只需要知道該緩存對象有存、取緩存圖片的接口即可,其他的一概不管,這就使得緩存功能的具體實(shí)現(xiàn)對ImageLoader具體的隱藏。這就是用最小化接口隔離了實(shí)現(xiàn)類的細(xì)節(jié),也促使我們將龐大的接口拆分到更細(xì)粒度的接口當(dāng)中,這使得我們的系統(tǒng)具有更低的耦合性,更高的靈活性。

Bob大叔(Robert C Martin)在21世紀(jì)早期將單一職責(zé)、開閉原則、里氏替換、接口隔離以及依賴倒置(也稱為依賴反轉(zhuǎn))5個(gè)原則定義為SOLID原則,指代了面向?qū)ο缶幊痰?個(gè)基本原則。當(dāng)這些原則被一起應(yīng)用時(shí),它們使得一個(gè)軟件系統(tǒng)更清晰、簡單、最大程度地?fù)肀ё兓OLID被典型地應(yīng)用在測試驅(qū)動開發(fā)上,并且是敏捷開發(fā)以及自適應(yīng)軟件開發(fā)基本原則的重要組成部分。在經(jīng)過第1.1~1.5節(jié)的學(xué)習(xí)之后,我們發(fā)現(xiàn)這幾大原則最終就可以化為這幾個(gè)關(guān)鍵詞:抽象、單一職責(zé)、最小化。那么在實(shí)際開發(fā)過程中如何權(quán)衡、實(shí)踐這些原則,是大家需要在實(shí)踐中多思考與領(lǐng)悟,正所謂”學(xué)而不思則罔,思而不學(xué)則殆”,只有不斷地學(xué)習(xí)、實(shí)踐、思考,才能夠在積累的過程有一個(gè)質(zhì)的飛越。

6、更好的可擴(kuò)展性——迪米特原則

迪米特原則英文全稱為Law of Demeter,簡稱LOD,也稱為最少知識原則(Least Knowledge Principle)。雖然名字不同,但描述的是同一個(gè)原則:一個(gè)對象應(yīng)該對其他對象有最少的了解。通俗地講,一個(gè)類應(yīng)該對自己需要耦合或調(diào)用的類知道得最少,類的內(nèi)部如何實(shí)現(xiàn)、如何復(fù)雜都與調(diào)用者或者依賴者沒關(guān)系,調(diào)用者或者依賴者只需要知道他需要的方法即可,其他的我一概不關(guān)心。類與類之間的關(guān)系越密切,耦合度越大,當(dāng)一個(gè)類發(fā)生改變時(shí),對另一個(gè)類的影響也越大。

迪米特法則還有一個(gè)英文解釋是:Only talk to your immedate friends,翻譯過來就是:只與直接的朋友通信。什么叫做直接的朋友呢?每個(gè)對象都必然會與其他對象有耦合關(guān)系,兩個(gè)對象之間的耦合就成為朋友關(guān)系,這種關(guān)系的類型有很多,例如組合、聚合、依賴等。

光說不練很抽象吶,下面我們就以租房為例來講講迪米特原則。
“北漂”的同學(xué)比較了解,在北京租房絕大多數(shù)都是通過中介找房。我們設(shè)定的情境為:我只要求房間的面積和租金,其他的一概不管,中介將符合我要求的房子提供給我就可以。下面我們看看這個(gè)示例:

/**
 * 房間
 */
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類中,那么中介類的功能就被弱化,而且導(dǎo)致Tenant與Room的耦合較高,因?yàn)門enant必須知道許多關(guān)于Room的細(xì)節(jié)。當(dāng)Room變化時(shí)Tenant也必須跟著變化。Tenant又與Mediator耦合,就導(dǎo)致了糾纏不清的關(guān)系。這個(gè)時(shí)候就需要我們分清誰才是我們真正的“朋友”,在我們所設(shè)定的情況下,顯然是Mediator(雖然現(xiàn)實(shí)生活中不是這樣的)。上述代碼的結(jié)構(gòu)如圖1-5所示。

▲圖1-5

既然是耦合太嚴(yán)重,那我們就只能解耦了,首先要明確地是,我們只和我們的朋友通信,這里就是指Mediator對象。必須將Room相關(guān)的操作從Tenant中移除,而這些操作案例應(yīng)該屬于Mediator,我們進(jìn)行如下重構(gòu):

/**
 * 中介
 */
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));
     }
}

重構(gòu)后的結(jié)構(gòu)圖如圖1-6所示。

▲圖1-6

只是將對于Room的判定操作移到了Mediator類中,這本應(yīng)該是Mediator的職責(zé),他們根據(jù)租戶設(shè)定的條件查找符合要求的房子,并且將結(jié)果交給租戶就可以了。租戶并不需要知道太多關(guān)于Room的細(xì)節(jié),比如與房東簽合同、房東的房產(chǎn)證是不是真的、房內(nèi)的設(shè)施壞了之后我要找誰維修等,當(dāng)我們通過我們的“朋友”中介租了房之后,所有的事情我們都通過與中介溝通就好了,房東、維修師傅等這些角色并不是我們直接的“朋友”。“只與直接的朋友通信”這簡單的幾個(gè)字就能夠?qū)⑽覀儚膩y七八糟的關(guān)系網(wǎng)中抽離出來,使我們的耦合度更低、穩(wěn)定性更好。
通過上述示例以及小民的后續(xù)思考,迪米特原則這把利劍在小民的手中已經(jīng)舞得風(fēng)生水起。就拿sd卡緩存來說吧,ImageCache就是用戶的直接朋友,而SD卡緩存內(nèi)部卻是使用了jake wharton的DiskLruCache實(shí)現(xiàn),這個(gè)DiskLruCache就不屬于用戶的直接朋友了,因此,用戶完全不需要知道它的存在,用戶只需要與ImageCache對象打交道即可。例如將圖片存到SD卡中的代碼如下。

public void put(String url, Bitmap value) {
    DiskLruCache.Editor editor = null;
    try {
       // 如果沒有找到對應(yīng)的緩存,則準(zhǔn)備從網(wǎng)絡(luò)上請求數(shù)據(jù),并寫入緩存
        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卡緩存時(shí),根本不知曉DiskLruCache的實(shí)現(xiàn),這就很好地對用戶隱藏了具體實(shí)現(xiàn)。當(dāng)小民已經(jīng)“牛”到可以自己完成SD卡的rul實(shí)現(xiàn)時(shí),他就可以隨心所欲的替換掉jake wharton的DiskLruCache。小民的代碼大體如下:

@Override
public  void put(String url, Bitmap bmp) {
    // 將Bitmap寫入文件中
    FileOutputStream fos = null;
    try {
       // 構(gòu)建圖片的存儲路徑 ( 省略了對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卡緩存的具體實(shí)現(xiàn)雖然被替換了,但用戶根本不會感知到。因?yàn)橛脩舾静恢繢iskLruCache的存在,他們沒有與DiskLruCache進(jìn)行通信,他們只認(rèn)識直接“朋友”ImageCache,ImageCache將一切細(xì)節(jié)隱藏在了直接“朋友”的外衣之下,使得系統(tǒng)具有更低的耦合性和更好的可擴(kuò)展性。

7、總結(jié)

在應(yīng)用開發(fā)過程中,最難的不是完成應(yīng)用的開發(fā)工作,而是在后續(xù)的升級、維護(hù)過程中讓應(yīng)用系統(tǒng)能夠擁抱變化。擁抱變化也就意味著在滿足需求且不破壞系統(tǒng)穩(wěn)定性的前提下保持高可擴(kuò)展性、高內(nèi)聚、低耦合,在經(jīng)歷了各版本的變更之后依然保持清晰、靈活、穩(wěn)定的系統(tǒng)架構(gòu)。當(dāng)然,這是一個(gè)比較理想的情況,但我們必須要朝著這個(gè)方向去努力,那么遵循面向?qū)ο罅笤瓌t就是我們走向靈活軟件之路所邁出的第一步。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容