面向?qū)ο罅笤O(shè)計(jì)原則

目錄

最新在閱讀《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》一書,我覺得寫的很清晰,每一個(gè)知識點(diǎn)都有示例,通過示例更加容易理解。書中的知識點(diǎn)有些都接觸過,有的沒有接觸過,總之,通過閱讀這本書來梳理一下知識點(diǎn),可能有些東西在項(xiàng)目中一直在使用,然并不能籠統(tǒng),清理的說明理解它。本文主要是記錄閱讀這本書的知識點(diǎn)和自己的一些理解。一來整理知識點(diǎn),二來方便以后查看,快速定位。

單一職責(zé)原則 :優(yōu)化代碼第一步

單一職責(zé)原則(英文簡稱:SRP):對于一個(gè)類而言,應(yīng)該僅有一個(gè)引起它變化的原因。這個(gè)有點(diǎn)抽象,因?yàn)樵撛瓌t的劃分界面并不是那么清晰,很多時(shí)候靠個(gè)人經(jīng)驗(yàn)來區(qū)分。簡單來說就是一個(gè)類只負(fù)責(zé)一個(gè)功能,比如加減乘除應(yīng)分別對應(yīng)一個(gè)類,而不是把四個(gè)功能放在一個(gè)類中,這樣在只要有一個(gè)功能變化都需要更改這個(gè)類。

下面以實(shí)現(xiàn)一個(gè)圖片加載器(ImageLoader)來說明:

public class ImageLoader {

    //圖片內(nèi)存緩存
    private LruCache<String,Bitmap> mImageCache;
    //線程池,線程數(shù)量為CPU的數(shù)量
    private ExecutorService mExecutorService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
    );

    public ImageLoader(){
        initImageLoader();
    }

    //初始化
    private void initImageLoader() {
        //計(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){
        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);
            }
        });

    }
}

我們一般都會這樣這樣簡單的實(shí)現(xiàn)一個(gè)圖片加載工具類,這樣寫功能雖然實(shí)現(xiàn)了,但是代碼是有問題的,代碼耦合嚴(yán)重,隨著ImageLoader功能越來越多,這個(gè)類會越來越大,代碼越來越復(fù)雜。按照單一職責(zé)原則,我們應(yīng)該把ImageLoader拆分一下,把各個(gè)功能獨(dú)立出來。

ImageLoader修改代碼如下:

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

    public void displayImage(final String url, final ImageView imageView){
         .............
    }
}
public class ImageCache {

    //圖片內(nèi)存緩存
    private 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 Bitmap get(String url){
        return mImageCache.get(url);
    }

    public void put(String url,Bitmap bitmap){
        mImageCache.put(url,bitmap);
    }
}

上述代碼將ImageLoader一分為二,ImageLoader只負(fù)責(zé)圖片加載的邏輯,ImageCache負(fù)責(zé)緩存策略,這樣,ImageLoader的代碼變少了,職責(zé)也清晰了,并且如果緩存策略改變了的話,只需要修改ImageCache而不需要在修改ImageLoader了。從這個(gè)例子可以更加清晰的理解什么是單一職責(zé)原則,如何去劃分一個(gè)類的職責(zé),每個(gè)人的看法不同,這需要根據(jù)個(gè)人的經(jīng)驗(yàn),業(yè)務(wù)邏輯而定。

開閉原則:讓程序更穩(wěn)定,更靈活

開閉原則(英文縮寫為OCP): 軟件中的對象(類,函數(shù),模塊等)對于擴(kuò)展是開放的,但是對于修改是封閉的。在軟件的生命周期內(nèi),因?yàn)樽兓?,升級和維護(hù)的原因需要對原代碼修改時(shí),可能會將錯(cuò)誤引入已經(jīng)經(jīng)過測試的舊代碼,破壞原有的系統(tǒng)。因?yàn)楫?dāng)需求變化時(shí),我們應(yīng)盡可能的通過擴(kuò)展來實(shí)現(xiàn),而不是修改原來的
代碼。

在實(shí)際的開發(fā)過程中,只通過繼承的方式來升級,維護(hù)原有的系統(tǒng)只是一個(gè)理想化的狀態(tài),修改原代碼,擴(kuò)展代碼往往是同時(shí)存在的。我們應(yīng)盡可能的影響原代碼。避免引入的錯(cuò)誤造成系統(tǒng)的破壞。

還是上面的那個(gè)例子,雖然通過內(nèi)存緩存解決了每次都從網(wǎng)絡(luò)下載圖片的問題,但是Android內(nèi)存有限,并且當(dāng)應(yīng)用重啟后內(nèi)存緩存會丟失。我們需要修改一下,增加SD卡緩存,代碼如下:

public class ImageLoader {

    //內(nèi)存緩存
    ImageCache mImageCache = new ImageCache();
    //SD卡緩存
    DiskCache mDiskCache = new DiskCache();
    //線程池,線程數(shù)量為CPU的數(shù)量
    private ExecutorService mExecutorService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
    );

    public void displayImage(final String url, final ImageView imageView){
        //先從內(nèi)存緩存中讀取,如果沒有再從SD卡中讀取
        Bitmap bitmap = mImageCache.get(url);
        if(bitmap == null){
            bitmap = mDiskCache.get(url);
        }
        if(bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }
      //從網(wǎng)絡(luò)下載圖片
       ..........
    }
}
public class DiskCache {
    private final static String cacheDir = "sdcard/cache/";

    /* 從緩存中獲取圖片 */
    public Bitmap get(String url){
        return BitmapFactory.decodeFile(cacheDir + url);
    }

    /* 將圖片添加到緩存中 */
    public void put(String url,Bitmap bitmap){
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            if(fileOutputStream != null){
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上述代碼我們增加了SD卡緩存,我們在顯示圖片的時(shí)候先判斷內(nèi)存緩存中是否存在如果不存在就在SD卡中找,否則再從網(wǎng)絡(luò)下載,這樣就會有一個(gè)問題,每增加一個(gè)新的緩存方法,我們都需要修改原來的代碼,這樣可能引入Bug,而且會使原來的代碼越來越復(fù)雜,還有用戶也不能自定義緩存方法。我們具體使用哪一種緩存方法是通過if條件判斷的,條件太多,是很容易寫錯(cuò)的。而且代碼會越來越臃腫,并且可擴(kuò)展性差??蓴U(kuò)展性是框架的重要特性之一。

根據(jù)開閉原則,當(dāng)軟件需求改變的時(shí)候,我們應(yīng)該通過擴(kuò)展的方式實(shí)現(xiàn),而不是修改自己的代碼。對上述代碼進(jìn)行優(yōu)化:

public class ImageLoader {

    //默認(rèn)緩存方式為內(nèi)存緩存
    ImageCache mImageCache = new MemoryCache();

    //線程池,線程數(shù)量為CPU的數(shù)量
    private ExecutorService mExecutorService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
    );
   
    //設(shè)置緩存方式
    public void setImageCache(ImageCache cache){
        mImageCache = cache;
    }

    public void displayImage(final String url, final ImageView imageView){
        //先從緩存中讀取
        Bitmap bitmap = mImageCache.get(url);
        if(bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }
     //網(wǎng)絡(luò)下載圖片
      ............
    }
}
public interface ImageCache {
    Bitmap get(String url);
    void put(String url, Bitmap bitmap);
}

通過上述代碼我們可以看出,ImageLoader增加了一個(gè)方法setImageCache,我們可以通過該方法設(shè)置緩存方式,這就是我們常說的依賴注入。當(dāng)然我們還可以自定義自己的緩存方式,只需要實(shí)現(xiàn)ImageCache這個(gè)接口即可。然后再調(diào)用setImageCache這個(gè)方法來設(shè)置。而不需要修改ImageLoader的代碼。這樣當(dāng)緩存需求改變的時(shí)候我們可以通過擴(kuò)展的方式來實(shí)現(xiàn)而不是修改的方法,這就是所說的開閉原則。同時(shí)是ImageLoader的代碼更簡潔,擴(kuò)展性和靈活性也更高。

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

里氏替換原則(英文縮寫為LSP):所有引用基類的地方都必須能夠透明的使用其子類的對象。我們知道面向?qū)ο笥腥筇匦裕悍庋b,繼承和多態(tài),里氏替換原則就是依賴?yán)^承和多態(tài)這兩大原則,里氏替換原則簡單來說就是:只要是父類引用出現(xiàn)的地方都可以替換成其子類的對象,并且不會產(chǎn)生任何的錯(cuò)誤和異常。

下面以Android中的Window和View的關(guān)系的例子來理解里氏替換原則:

//窗口類
public class Window {
    public void show(View child){
        child.draw();
    }
}

建立視圖抽象類,測量視圖的寬高為公共代碼,繪制交給具體的子類去實(shí)現(xiàn)

public abstract class View {
    public abstract void draw();
    public void measure(int width,int height){
        //測量視圖大小
        ...........
    }
}

//文本類具體實(shí)現(xiàn)
public class TextView extends View {
    @Override
    public void draw() {
        //繪制文本
        ...........
    }
}

//按鈕類具體實(shí)現(xiàn)
public class Button extends View {
    @Override
    public void draw() {
        //繪制按鈕
        .............
    }
}

上述示例中,Window依賴于View,View定義了一個(gè)視圖抽象,measure是各個(gè)子類共享的方法,子類通過重寫View的draw方法來實(shí)現(xiàn)具體各自特色的內(nèi)容。任何繼承View的子類都可以設(shè)置給show方法,這就是所說的里氏替換原則,通過里式替換,就可以自定義各種各樣的,千變?nèi)f化的View,然后傳遞給Window,Window負(fù)責(zé)組織View,并將View顯示到屏幕上。

上面ImageLoader的例子也體現(xiàn)了里氏替換原則,可以通過setImageCache方法來設(shè)置各種各樣的緩存方式,如果 setImageCache中的cache對象不能被子類替換,那么又怎么能設(shè)置各種各樣的緩存方式呢?

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

依賴倒置原則(英文縮寫為DIP)指代了一種特定的解耦方式,使得高層次的模塊不依賴于低層次模塊的實(shí)現(xiàn)細(xì)節(jié)的目的,依賴模塊被顛倒了。這個(gè)概念更加的抽象,該怎么理解呢?

依賴倒置原則有幾個(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é),可以直接實(shí)例化;高層模塊就是調(diào)用端;底層模塊就是實(shí)現(xiàn)端。

依賴倒置原則在Java語言中的表現(xiàn)就是:模塊間的依賴通過抽象發(fā)生,實(shí)現(xiàn)類直接不能直接發(fā)生依賴,其依賴關(guān)系是通過接口或者抽象類產(chǎn)生的。

如果類與類之間直接依賴于細(xì)節(jié),那么它們之間就有直接的耦合,當(dāng)需求變化的時(shí)候,意味著要同時(shí)修改依賴者的代碼。這就限制了系統(tǒng)的可擴(kuò)展性。

public class ImageLoader {
    //直接依賴于細(xì)節(jié)
    MemoryCache mImageCache = new MemoryCache();
    ...................
}

ImageLoader直接依賴于MemoryCache,MemoryCache是一個(gè)具體的實(shí)現(xiàn),這就導(dǎo)致ImageLoader直接依賴于細(xì)節(jié),當(dāng)MemoryCache不能滿足而被其他緩存實(shí)現(xiàn)替換時(shí),就必須需要修改ImageLoader的代碼。

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

public class ImageLoader {
    //依賴于抽象,并且有一個(gè)默認(rèn)的實(shí)現(xiàn)
    ImageCache mImageCache = new MemoryCache();
    ......................

在這里我們建立了ImageCache抽象,并且讓ImageLoader直接依賴于抽象而不是具體的細(xì)節(jié),當(dāng)需求變化時(shí),只需要實(shí)現(xiàn)ImageCache或者繼承已有的類來完成相應(yīng)的緩存功能。然后再將具體的實(shí)現(xiàn)注入到ImageLoader中,保證了系統(tǒng)的高擴(kuò)展性。這就是依賴倒置原則。

接口隔離原則:讓系統(tǒng)擁有更高的靈活性

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

接口隔離原則說白了就是讓依賴的接口盡可能的小,看一下上個(gè)例子實(shí)現(xiàn)SD卡緩存的代碼:

 /* 將圖片添加到緩存中 */
 public void put(String url,Bitmap bitmap){
     FileOutputStream fileOutputStream = null;
     try {
         fileOutputStream = new FileOutputStream(cacheDir + url);
         bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
     } catch (FileNotFoundException e) {
         e.printStackTrace();
     }finally {
         if(fileOutputStream != null){
             try {
                 fileOutputStream.close();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }
  }

我們看到這段代碼的可讀性非常的差,各種try...catch都是非常簡單的代碼,但是會嚴(yán)重影響代碼的可讀性,并且多層級的大括號很容易將代碼寫到錯(cuò)誤的層級中。那么如何解決這樣的問題呢?Java中有一個(gè)Closeable接口,該接口標(biāo)識了一個(gè)可關(guān)閉的對象,它只有一個(gè)close方法。實(shí)現(xiàn)該接口的類有很多,F(xiàn)ileOutputStream也實(shí)現(xiàn)了該接口,當(dāng)程序有有多個(gè)可關(guān)閉的對象時(shí),如果都像上述代碼那樣在finally中去關(guān)閉,就非常的麻煩了。

我們可以抽取一個(gè)工具類來專門去關(guān)閉需要關(guān)閉的對象。

public class CloseUtils {

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

使用工具類替換上述的代碼

public void put(String url,Bitmap bitmap){
     FileOutputStream fileOutputStream = null;
    try {
        fileOutputStream = new FileOutputStream(cacheDir + url);
        bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }finally {
        CloseUtils.closeQuietly(fileOutputStream);
    }
}

這樣代碼就簡潔多了,并且CloseUtils可以用到多個(gè)可以關(guān)閉對象的地方,保證了代碼的重用性,它依賴于Closeable抽象而不是具體的實(shí)現(xiàn),并且建立在最小的依賴原則之上,他只需要知道對象是否可關(guān)閉,其他的一概不關(guān)心,這就是接口隔離。如果現(xiàn)在只需要關(guān)閉一個(gè)對象時(shí),它卻暴露了其他的接口方法,比如OutputStream的write方法,這使得更多的細(xì)節(jié)暴露在客戶端面前,還增加了使用難度。而通過Closeable接口將可關(guān)閉的對象抽象起來,這樣客戶端只需要依賴Closeable就可將其他的細(xì)節(jié)隱藏起來,客戶端只需要知道這個(gè)對象可關(guān)閉即可。

在上述的ImageLoader中,只需要知道該緩存對象有讀取和緩存的接口即可,其他的一概不管,這樣緩存的具體實(shí)現(xiàn)是對ImageLoader隱藏的。這就是用最小化接口隔離了實(shí)現(xiàn)類的細(xì)節(jié)。

Robert C Martin在21世紀(jì)早期將單一職責(zé),開閉原則,里氏替換,接口隔離和依賴倒置5個(gè)原則定義為SOLID原則,作為面向?qū)ο缶幊痰?個(gè)基本原則。當(dāng)這些原則在一起使用時(shí),它使得一個(gè)軟件系統(tǒng)更清晰,更簡單,最大程度的擁抱變化。

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

迪米特原則(英文縮寫為LOD):也稱為最少知識原則,一個(gè)對象應(yīng)該對其他對象有最少的理解。通俗的講,一個(gè)類對自己需要耦合或者調(diào)用的類知道的最少,類的內(nèi)部如果實(shí)現(xiàn)與調(diào)用者或依賴者沒有關(guān)系。調(diào)用者或依賴者只需要知道它調(diào)用的方法即可,其他的一概不知。

下面以租房的例子來理解說明這個(gè)原則。

租房大多數(shù)通過中介來租,我們假設(shè)設(shè)定的情景為:我們只要求房子的面積和租金,其他的一概不管,中介提供給我們符合要求的房子。

public class Room {
    
    public float area;
    public float price;

    public Room(float area, float price) {
        this.area = area;
        this.price = price;
    }
}
public class Mediator {
    private List<Room> mRooms = new ArrayList<>();

    public Mediator(){

        for (int i = 0; i < 5; i++) {
            mRooms.add(new Room(14 + i,(14 + i) * 150));
        }
    }

    public List<Room> getRooms(){
        return mRooms;
    }
}
public class Tenant {
    private float roomArea;
    private float roomPrice;
    private static final float diffArea = 0.0001f;
    private static final float diffPrice = 100.0001f;

    public void rentRoom(Mediator mediator){
        List<Room> rooms = mediator.getRooms();
        for (Room room : rooms) {
            if(isSuitable(room)){
                System.out.print("租到房子了" + room.toString());
                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)致TenantRoom耦合度較高。當(dāng)Room變化的時(shí)候,Tenant也必須跟著變化,而且Tenant還和Mediator耦合,這樣關(guān)系就顯得有些混亂了。

我們需要根據(jù)迪米特原則進(jìn)行解耦。

public class Mediator {
    private List<Room> mRooms = new ArrayList<>();

    public Mediator(){

        for (int i = 0; i < 5; i++) {
            mRooms.add(new Room(14 + i,(14 + i) * 150));
        }
    }

    public Room rentOut(float price,float area){
        for (Room room : mRooms) {
            if (isSuitable(price,area,room)) {
                return room;
            }
        }
        return null;
    }

    private boolean isSuitable(float price,float area,Room room){
        return Math.abs(room.price - price) < Tenant.diffPrice
                && Math.abs(room.area - area) < Tenant.diffArea;
    }
}
public class Tenant {
    
    private float roomArea;
    private float roomPrice;
    public static final float diffArea = 0.0001f;
    public static final float diffPrice = 100.0001f;

    public void rentRoom(Mediator mediator) {
        Room room = mediator.rentOut(roomPrice, roomArea);
        if(null != room){
            System.out.print("租到房子了" + room.toString());
        }
    }

}

我們將對Room的操作移到了Mediator中,這本來就是Mediator的職責(zé),根據(jù)租戶的條件檢索符合的房子,并且將房子返回給用戶即可。這樣租戶就不需要知道有關(guān)Room的細(xì)節(jié),比如和房東簽合同,房產(chǎn)證的真?zhèn)蔚取V恍枰P(guān)注和我們相關(guān)的即可。

總結(jié)

在應(yīng)用開發(fā)過程中,我們不僅要完成應(yīng)用的開發(fā)工作,還需要在后續(xù)的升級,維護(hù)中讓應(yīng)用系統(tǒng)能夠擁抱變化。擁抱變化意味著在滿足需求且不破壞系統(tǒng)穩(wěn)定的前提下保持高擴(kuò)展性,高內(nèi)聚,低耦合,在經(jīng)歷了各個(gè)版本變更之后依然保持清晰,靈活,穩(wěn)定的系統(tǒng)架構(gòu)。那么遵守面向?qū)ο蟮牧笤瓌t是我們邁向的第一步。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容