面向?qū)ο蟮牧笤瓌t——讓代碼更優(yōu)美

導(dǎo)語(yǔ)

讓你的代碼更加優(yōu)美。

主要內(nèi)容

  • 單一職責(zé)原則——優(yōu)化代碼的第一步
  • 開(kāi)閉原則——讓程序更穩(wěn)定靈活
  • 里氏替換原則——構(gòu)建擴(kuò)展性更好的系統(tǒng)
  • 依賴(lài)倒置原則——讓項(xiàng)目擁有變化的能力
  • 接口隔離原則——讓系統(tǒng)有更高的靈活性
  • 迪米特原則——更好的可擴(kuò)展性

具體內(nèi)容

單一職責(zé)原則

單一職責(zé)原則的英文名名稱(chēng)是Single Responsibility Principle,縮寫(xiě)是SRP。SRP的定義是:就一個(gè)類(lèi)而言,應(yīng)該僅有一個(gè)引起它變化的原因。簡(jiǎn)單來(lái)說(shuō),一個(gè)類(lèi)中應(yīng)該是一組相關(guān)性很高的函數(shù)、數(shù)據(jù)的封裝。

比如Pear是一家電子產(chǎn)品商,它要生產(chǎn)pad,phone,watch等設(shè)備,但是有一些重復(fù)的功能,如果分別設(shè)計(jì)一套,很顯然并不劃算,那么接口定義上我們就可以根據(jù)功能劃分設(shè)定單一職責(zé)的接口:

接口的定義

//可以撥打電話(huà)
interface Callable{
    void call ();
}

//可以觸摸控制
interface Touchable{
    void touch();
}

//可以消息提醒
interface MessagePromptable{
    void prompt();
}

//可以接入鍵盤(pán)
interface KeyBoardMatchable{
    void match();
}

實(shí)現(xiàn)接口的類(lèi)依舊單一職責(zé)

class StandardCall implements Callable{

    @Override
    public void call() {
        System.out.println("Call to somebody!");
    }
}

class StandardTouch implements Touchable{

    @Override
    public void touch() {
        System.out.println("touch to press the button!");
    }
}

class StandardPromt implements MessagePromptable{

    @Override
    public void prompt() {
        System.out.println(" someone contact to you,sir!");
    }
}

class StandardMatch implements KeyBoardMatchable{

    @Override
    public void match() {
        System.out.println("The keyBoard is ready to work!");
    }
}

產(chǎn)品的生產(chǎn)
我們?nèi)绻谖覀儸F(xiàn)有的技術(shù)生產(chǎn)一部手機(jī),那么我們需要它能打電話(huà),觸屏控制和消息提醒:

//在聲明這臺(tái)手機(jī)時(shí)我們就明確知道了它的功能
class MyPhone implements Callable, MessagePromptable, Touchable{

    //無(wú)需重復(fù)研發(fā)已有的技術(shù),直接裝載即可
    private Callable caller = new StandardCall();
    private MessagePromptable prompter = new StandardPromt();
    private Touchable toucher = new StandardTouch();

    @Override
    public void call() {
        caller.call();
    }

    @Override
    public void prompt() {
        prompter.prompt();
    }

    @Override
    public void touch() {
        toucher.touch();
    }
}

public class SRPTest {
    public static void main ( String [] args ){
        MyPhone phone = new MyPhone();
        phone.call();
        phone.prompt();
        phone.touch();
    }
}

假如我們需要出一款新的手機(jī),但是我們只是擁有了新的呼叫技術(shù),那么只需要在實(shí)現(xiàn)這項(xiàng)技術(shù)時(shí)繼承Callable接口,然后在之前手機(jī)new的Callable的具體實(shí)例換成新的技術(shù)即可,只需要修改一行代碼,是不是感覺(jué)棒棒的。職責(zé)的單一,對(duì)于我們對(duì)于現(xiàn)有類(lèi)的修改造成的影響有了約束。

那么如果我想生產(chǎn)一個(gè)Pad呢,同理啊,只需要在已有技術(shù)上裝載即可啊,Pad類(lèi)依舊只是單一的整合技術(shù)形成產(chǎn)品的職責(zé),整合成產(chǎn)品和研發(fā)出技術(shù)的職責(zé)分離,為我們的類(lèi)的拓展帶來(lái)了方便。

class MyPad implements Touchable,KeyBoardMatchable{

    Touchable toucher = new StandardTouch();
    KeyBoardMatchable matcher = new StandardMatch();

    @Override
    public void match() {
        toucher.touch();
    }

    @Override
    public void touch() {
        matcher.match();
    }
}

下面一個(gè)例子,我們的接口依舊單一職責(zé),但是接聽(tīng)和撥打電話(huà)的功能往往是不可分的,他們會(huì)同時(shí)發(fā)生變化,所以我們可以提供一個(gè)同時(shí)繼承兩個(gè)接口的實(shí)現(xiàn)類(lèi)。

class CallAndPrompt implements Callable,MessagePromptable{

    @Override
    public void call() {
        System.out.println("Hello, I have some thing to tell you!");
    }

    @Override
    public void prompt() {
        System.out.println("Hello,what do you want to tell me!");
    }
}

//在聲明這臺(tái)手機(jī)時(shí)我們就明確知道了它的功能
class MyPhone implements Callable,MessagePromptable,Touchable{

    //無(wú)需重復(fù)研發(fā)已有的技術(shù),直接裝載即可
    private Callable caller = new CallAndPrompt();
    //不同的接口調(diào)用同一個(gè)實(shí)現(xiàn)類(lèi)的不同功能
    private MessagePromptable prompter = (MessagePromptable)caller;
    private Touchable toucher = new StandardTouch();

    @Override
    public void call() {
        caller.call();
    }

    @Override
    public void prompt() {
        prompter.prompt();
    }

    @Override
    public void touch() {
        toucher.touch();
    }
}

開(kāi)閉原則

開(kāi)閉原則的英文全稱(chēng)是Open Close Principle,縮寫(xiě)是OCP,它是Java世界里最基礎(chǔ)的設(shè)計(jì)原則,它指導(dǎo)我們?nèi)绾谓⒁粋€(gè)穩(wěn)定的、靈活的系統(tǒng)。開(kāi)閉原則的定義是:軟件中的對(duì)象(類(lèi)、模塊、函數(shù)等)應(yīng)該對(duì)于擴(kuò)展是開(kāi)放的,但是,對(duì)于修改是封閉的。

優(yōu)點(diǎn):按照OCP原則設(shè)計(jì)出來(lái)的系統(tǒng),降低了程序各部分之間的耦合性,其適應(yīng)性、靈活性、穩(wěn)定性都比較好。當(dāng)已有軟件系統(tǒng)需要增加新的功能時(shí),不需要對(duì)作為系統(tǒng)基礎(chǔ)的抽象層進(jìn)行修改,只需要在原有基礎(chǔ)上附加新的模塊就能實(shí)現(xiàn)所需要添加的功能。增加的新模塊對(duì)原有的模塊完全沒(méi)有影響或影響很小,這樣就無(wú)須為原有模塊進(jìn)行重新測(cè)試。

如何實(shí)現(xiàn)“開(kāi)-閉”原則
在面向?qū)ο笤O(shè)計(jì)中,不允許更改的是系統(tǒng)的抽象層,而允許擴(kuò)展的是系統(tǒng)的實(shí)現(xiàn)層。換言之,定義一個(gè)一勞永逸的抽象設(shè)計(jì)層,允許盡可能多的行為在實(shí)現(xiàn)層被實(shí)現(xiàn)。
解決問(wèn)題關(guān)鍵在于抽象化,抽象化是面向?qū)ο笤O(shè)計(jì)的第一個(gè)核心本質(zhì)。
對(duì)一個(gè)事物抽象化,實(shí)質(zhì)上是在概括歸納總結(jié)它的本質(zhì)。抽象讓我們抓住最最重要的東西,從更高一層去思考。這降低了思考的復(fù)雜度,我們不用同時(shí)考慮那么多的東西。換言之,我們封裝了事物的本質(zhì),看不到任何細(xì)節(jié)。
在面向?qū)ο缶幊讨?,通過(guò)抽象類(lèi)及接口,規(guī)定了具體類(lèi)的特征作為抽象層,相對(duì)穩(wěn)定,不需更改,從而滿(mǎn)足“對(duì)修改關(guān)閉”;而從抽象類(lèi)導(dǎo)出的具體類(lèi)可以改變系統(tǒng)的行為,從而滿(mǎn)足“對(duì)擴(kuò)展開(kāi)放”。
對(duì)實(shí)體進(jìn)行擴(kuò)展時(shí),不必改動(dòng)軟件的源代碼或者二進(jìn)制代碼。關(guān)鍵在于抽象。

接口的定義

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

實(shí)現(xiàn)接口

// 內(nèi)存緩存MemoryCache類(lèi)
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類(lèi)
public class DiskCache implements ImageCache {
    @Override
    public Bitmap get(String url) {
        // 改為從本地文件獲取圖片
        return null;
    }

    @Override
    public void put(String url, Bitmap bmp) {
        // 將Bitmap寫(xiě)入文件中
    }
}

// 雙緩存DoubleCache類(lèi)
public class DoubleCache implements ImageCache {
    ImageCache mMemoryCache = new MemoryCache();
    ImageCache mDiskCache = new mDiskCache();

    // 先從內(nèi)存中獲取圖片,如果沒(méi)有再?gòu)腟D卡中獲取
    public Bitmap get(String url) {
        public 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);
    }
}

實(shí)現(xiàn)圖片加載器類(lèi)

public class ImageLoader {
    // 圖片緩存,并設(shè)置了內(nèi)存緩存為默認(rèn)方式
    private ImageCache mImageCache = new MemoryCache();

    // 注入緩存實(shí)現(xiàn)  利用了向上轉(zhuǎn)型
    public void setImageCache(ImageCache imageCache) {
        mImageCache = imageCache;
    }

    public void displayImage(String imageUrl, ImageView imageView) {
        Bitmap bitmap = mImageCache.get(imageUrl);
        if(bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        // 圖片沒(méi)有緩存,提交到線(xiàn)程池中下載圖片
        submitLoadRequest(imageUrl, imageView)
    }

    public void submitLoadRequest(final String imageUrl, final ImageView imageView) {
        // 下載圖片
        // 緩存
        mImageCache.put(imageUrl, bitmap);
     }

    // 省略其他成員變量和方法
}

調(diào)用方法

//  使用方法  只是通過(guò)傳入不同實(shí)現(xiàn)就可以切換緩存方式
ImageLoader loader = new ImageLoader();
loader.setImageCache(new MemoryCache());  // 使用內(nèi)存緩存
loader.setImageCache(new DiskCache());  // 使用SD卡緩存
loader.setImageCache(new DoubleCache());  // 使用雙緩存

// 使用自定義的圖片緩存實(shí)現(xiàn)
loader.setImageCache(new ImageCache() {
    @Override
    public Bitmap get(String url) {
        // 改為從本地文件獲取圖片
        return null;
    }

    @Override
    public void put(String url, Bitmap bmp) {
        // 將Bitmap寫(xiě)入文件中
    }
    });

我們看得到通過(guò)setImageCache(ImageCache imageCache) 方式注入不同的緩存實(shí)現(xiàn),使得ImageLoader代碼變得更簡(jiǎn)單,健壯,提升高了它的靈活性和可擴(kuò)展性,如果還有還有新的緩存方式,只需要去實(shí)現(xiàn)ImageCachej接口就可以使用了。

所以當(dāng)需求發(fā)生變化時(shí),應(yīng)該盡量通過(guò)擴(kuò)展的方式來(lái)實(shí)現(xiàn)變化,而不是通過(guò)修改已有代碼來(lái)實(shí)現(xiàn),但要做到開(kāi)閉原則,首先我們應(yīng)該先寫(xiě)出更易擴(kuò)展的代碼。

里氏替換原則

里氏替換原則英文全稱(chēng)是Liskov Substitution Principle,縮寫(xiě)是LSP。LSP的第一種定義是:如果對(duì)每一個(gè)類(lèi)型為S的對(duì)象O1,都有類(lèi)型為T(mén)的對(duì)象O2,使得以T定義的所有程序P在所有的對(duì)象O1都代換成O2時(shí),程序P的行為沒(méi)有發(fā)生變化,那么類(lèi)型S是類(lèi)型T的子類(lèi)型。
或者說(shuō)是:所有引用基類(lèi)的地方必須能透明地使用其子類(lèi)的對(duì)象。

就像開(kāi)閉原則中舉的例子,創(chuàng)建了一個(gè)ImageCache,而其他緩存類(lèi)都是他的實(shí)現(xiàn)類(lèi),而setImageCache(ImageCache imageCache) 需要的就是ImageCache類(lèi)型,這時(shí)候我們就可以使用MemoryCache,DiskCache,DoubleCache來(lái)替換ImageCache的工作。ImageCache確定了規(guī)范,而新的緩存需求都可以通過(guò)實(shí)現(xiàn)它然后替換ImageCache來(lái)工作,從而保證了可擴(kuò)展性。

也可以看一下系統(tǒng)代碼

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

// 建立視圖抽象,測(cè)量視圖寬高為公用代碼,繪制實(shí)現(xiàn)交給具體的子類(lèi)
public abstract class View {
    public abstract void draw();
    public void measure(int width, int height) {
        // 測(cè)量視圖大小
    }
}

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

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

故里氏替換原則就是通過(guò)建立抽象,建立規(guī)范,然后在運(yùn)行時(shí)通過(guò)具體實(shí)現(xiàn)來(lái)替換掉抽象,從而保證了系統(tǒng)的擴(kuò)展性和靈活性。可見(jiàn),在開(kāi)發(fā)過(guò)程中運(yùn)用抽象是走向代碼優(yōu)化的重要一步。

開(kāi)閉原則和里氏替換原則往往都是一同出現(xiàn)的,通過(guò)里氏替換原則達(dá)到對(duì)擴(kuò)展的開(kāi)發(fā),對(duì)修改關(guān)閉的效果。

依賴(lài)倒置原則

依賴(lài)倒置原則英文全稱(chēng)是Dependence Inversion Principle,縮寫(xiě)是DIP。依賴(lài)倒置原則指代了一種特定的解耦形式,使得高層次的模塊不依賴(lài)于低層次的模塊的實(shí)現(xiàn)細(xì)節(jié)的目的,依賴(lài)模塊被顛倒了。

依賴(lài)倒置原則的三個(gè)關(guān)鍵點(diǎn):

  1. 高層次模塊不應(yīng)該依賴(lài)于底層模塊,兩者都應(yīng)該依賴(lài)其抽象。
  2. 抽象不應(yīng)依賴(lài)細(xì)節(jié)。
  3. 細(xì)節(jié)應(yīng)該依賴(lài)抽象。

抽象就是指接口或者抽象類(lèi);細(xì)節(jié)就是實(shí)現(xiàn)類(lèi);高層模塊就是調(diào)用端,低層模塊就是具體實(shí)現(xiàn)類(lèi)。

依賴(lài)倒置原則在Java中表現(xiàn)就是:模塊間依賴(lài)是通過(guò)抽象發(fā)生的,實(shí)現(xiàn)類(lèi)之間并不產(chǎn)生直接依賴(lài)關(guān)系,其依賴(lài)關(guān)系是通過(guò)接口或抽象類(lèi)產(chǎn)生的。
一句話(huà)概括:面向接口編程,或者說(shuō)面向抽象編程。

我們依然可以通過(guò)上面的例子繼續(xù)說(shuō)明,代碼如下:

//  如果在ImageLoader中直接這樣寫(xiě)的話(huà)
//  就是直接依賴(lài)于細(xì)節(jié)(直接依賴(lài)實(shí)現(xiàn)類(lèi))
private DoubleCache mCache = new DoubleCache();
public void setImageCache(DoublieCache cache) {
    mCache = cache;
}

而我們的代碼卻直接完成1.2.3.4這四個(gè)原則

//  依賴(lài)于抽象,通過(guò)向上轉(zhuǎn)型,有一個(gè)默認(rèn)的實(shí)現(xiàn)類(lèi)
private ImageCache mImageCache = new MemoryCache();

//  設(shè)置緩存策略,依賴(lài)于抽象
public void setImageCache(ImageCache imageCache) {
    mImageCache = imageCache;
}

依賴(lài)于抽象,依賴(lài)于基類(lèi),這樣當(dāng)需求發(fā)生變化,只需要實(shí)現(xiàn)ImageCache或者繼承已實(shí)現(xiàn)的之類(lèi)都可以完成緩存功能,然后將實(shí)現(xiàn)注入到setImageCache(ImageCache imageCache)就可以了。

接口隔離原則

接口隔離原則英文全稱(chēng)是InterfaceSegregation Principles,縮寫(xiě)是ISP。ISP的定義是:客戶(hù)端不應(yīng)該依賴(lài)它不需要的接口?;蛘哒f(shuō)類(lèi)的依賴(lài)關(guān)系應(yīng)該將在最小的接口上。

接口隔離的目的是系統(tǒng)接口耦合,從而容易重構(gòu)、更改和重新部署。一句話(huà):讓客戶(hù)端依賴(lài)的接口盡可能小。

舉一個(gè)例子,當(dāng)我們?cè)谑褂昧鞯臅r(shí)候我們需要在finally中判斷是否為空,如果不為空需要close()它,但每次使用流,都這么寫(xiě),也會(huì)讓代碼變得不優(yōu)美,這個(gè)時(shí)候我們考慮借助外力,就比如Java為我們提供了一個(gè)Closeable接口,而它有100多個(gè)實(shí)現(xiàn)類(lèi),所以那些類(lèi)都可以使用它,代碼如下:

//  這就是修改之前的代碼 try/catch中還有try/catch
FileOutputStream fileOutputStream = null;
try {
//  邏輯省略
} catch (Exception e) {
        e.printStackTrace();
} finally {
        if (fileOutputStream != null) {
                try {
                        fileOutputStream.close();
               } catch (IOException e) {
                        e.printStackTrace();
               }
        }
}

//  寫(xiě)了個(gè)CloseUtil類(lèi),然后西面提供這個(gè)靜態(tài)方法,所有實(shí)現(xiàn)了Closeable的類(lèi)都可以調(diào)用這個(gè)方法
 public static void closeQuietly (Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
}

//  我們只需要在finally中調(diào)用這一句話(huà)就好了
CloseUtil.closeQuietly(xxx);

不僅讓代碼的可讀性增加了,還保證了它的重用性,這里也用到了依賴(lài)倒置原則,closeQuietly()方法的參數(shù)就一個(gè)抽象,做到了我只需要知道這個(gè)對(duì)象是可關(guān)閉的,其他一概不管辛,也就是作者所說(shuō)的接口隔離原則。

迪米特原則

迪米特原則英文全稱(chēng)為L(zhǎng)aw of Demeter,縮寫(xiě)是LOD,也稱(chēng)為最少知識(shí)原則(Least Knowledge Principle)。LOD的定義是:一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象有最少的了解。

通俗的講,一個(gè)類(lèi)應(yīng)該對(duì)自己需要耦合或者調(diào)用的類(lèi)知道的最少,類(lèi)的內(nèi)部如何實(shí)現(xiàn)與調(diào)用者或者依賴(lài)者沒(méi)有關(guān)系,只需要知道它需要的方法即可,其他的一概不管,類(lèi)與類(lèi)之間的關(guān)系越密切,耦合度也就越大。

迪米特原則還有一個(gè)英文解釋?zhuān)篛nly talk to your immediate friends.翻譯過(guò)來(lái)也就是說(shuō)之與直接朋友進(jìn)行通信。

還是前面的ImageLoder,緩存這塊是已經(jīng)搞定了。假如在某次加載圖片中,,緩存沒(méi)找到就需要聯(lián)網(wǎng)去服務(wù)器拿圖片,并且需要存到緩存中以備下次直接從緩存加載,ok,很快可以寫(xiě)出這樣的代碼:

public class ImageLoder {
    private ImageCache mImageCache = new DoubleCache();
    //...
    public void dispalyImage(String url, ImageView imageView) {
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        HttpImage4Service.down4Service(url, imageView, mImageCache);
    }
}

HttpImage4Service下載類(lèi)中從網(wǎng)絡(luò)中加載圖片方法:

public static void down4Service(String url, ImageView imageView, ImageCache imageCache) {
        //...從網(wǎng)絡(luò)拉取圖片
        //回調(diào)↓
        imageView.setImageBitmap(bitmap4Service);//顯示圖片
        imageCache.put(url, bitmap4Service);//存到緩存中
    }

分析下這樣設(shè)計(jì)的耦合情況,

  • ImageLoder調(diào)用ImageCache和HttpImage4Service。
  • HttpImage4Service調(diào)用ImageCache。

三個(gè)類(lèi)之間是否知道的最少?試想一下,從網(wǎng)絡(luò)拉取圖片跟緩存這樣兩個(gè)類(lèi)應(yīng)該有關(guān)聯(lián)嗎?實(shí)際上是沒(méi)必要的,根據(jù)最少知識(shí)原則,改進(jìn)之后應(yīng)該是下面這樣的:

public class ImageLoder {
    private ImageCache mImageCache = new DoubleCache();
    //...
    public void dispalyImage(String url, ImageView imageView) {
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        bitmap=HttpImage4Service.down4Service(url);//只負(fù)責(zé)下載圖片
        imageView.setImageBitmap(bitmap);
        imageCache.put(url, bitmap);//存到緩存中
    }
}

改進(jìn)后三個(gè)類(lèi)中只有ImageLoder調(diào)用HttpImage4Service和ImageCache中的方法,其余沒(méi)有任何調(diào)用關(guān)系,耦合度降低。

在MVP中,View層和Model層拒絕通信,也是符合最少知識(shí)原則的,達(dá)到降低耦合效果,同時(shí)可擴(kuò)展性會(huì)大大增加。

總結(jié)

應(yīng)用開(kāi)發(fā),最難的不是完成開(kāi)發(fā)工作,而是維護(hù)和升級(jí)。為了后續(xù)能夠很好的維護(hù)和升級(jí),我們的系統(tǒng)需要在滿(mǎn)足穩(wěn)定性的前提下保持以下三個(gè)特性:

  • 高可擴(kuò)展性
  • 高內(nèi)聚
  • 低耦合

更多內(nèi)容戳這里(整理好的各種文集)

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

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

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