面向對象的六大原則——讓代碼更優美

導語

讓你的代碼更加優美。

主要內容

  • 單一職責原則——優化代碼的第一步
  • 開閉原則——讓程序更穩定靈活
  • 里氏替換原則——構建擴展性更好的系統
  • 依賴倒置原則——讓項目擁有變化的能力
  • 接口隔離原則——讓系統有更高的靈活性
  • 迪米特原則——更好的可擴展性

具體內容

單一職責原則

單一職責原則的英文名名稱是Single Responsibility Principle,縮寫是SRP。SRP的定義是:就一個類而言,應該僅有一個引起它變化的原因。簡單來說,一個類中應該是一組相關性很高的函數、數據的封裝。

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

接口的定義

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

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

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

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

實現接口的類依舊單一職責

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!");
    }
}

產品的生產
我們如果基于我們現有的技術生產一部手機,那么我們需要它能打電話,觸屏控制和消息提醒:

//在聲明這臺手機時我們就明確知道了它的功能
class MyPhone implements Callable, MessagePromptable, Touchable{

    //無需重復研發已有的技術,直接裝載即可
    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();
    }
}

假如我們需要出一款新的手機,但是我們只是擁有了新的呼叫技術,那么只需要在實現這項技術時繼承Callable接口,然后在之前手機new的Callable的具體實例換成新的技術即可,只需要修改一行代碼,是不是感覺棒棒的。職責的單一,對于我們對于現有類的修改造成的影響有了約束。

那么如果我想生產一個Pad呢,同理啊,只需要在已有技術上裝載即可啊,Pad類依舊只是單一的整合技術形成產品的職責,整合成產品和研發出技術的職責分離,為我們的類的拓展帶來了方便。

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();
    }
}

下面一個例子,我們的接口依舊單一職責,但是接聽和撥打電話的功能往往是不可分的,他們會同時發生變化,所以我們可以提供一個同時繼承兩個接口的實現類。

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!");
    }
}

//在聲明這臺手機時我們就明確知道了它的功能
class MyPhone implements Callable,MessagePromptable,Touchable{

    //無需重復研發已有的技術,直接裝載即可
    private Callable caller = new CallAndPrompt();
    //不同的接口調用同一個實現類的不同功能
    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();
    }
}

開閉原則

開閉原則的英文全稱是Open Close Principle,縮寫是OCP,它是Java世界里最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟件中的對象(類、模塊、函數等)應該對于擴展是開放的,但是,對于修改是封閉的。

優點:按照OCP原則設計出來的系統,降低了程序各部分之間的耦合性,其適應性、靈活性、穩定性都比較好。當已有軟件系統需要增加新的功能時,不需要對作為系統基礎的抽象層進行修改,只需要在原有基礎上附加新的模塊就能實現所需要添加的功能。增加的新模塊對原有的模塊完全沒有影響或影響很小,這樣就無須為原有模塊進行重新測試。

如何實現“開-閉”原則
在面向對象設計中,不允許更改的是系統的抽象層,而允許擴展的是系統的實現層。換言之,定義一個一勞永逸的抽象設計層,允許盡可能多的行為在實現層被實現。
解決問題關鍵在于抽象化,抽象化是面向對象設計的第一個核心本質。
對一個事物抽象化,實質上是在概括歸納總結它的本質。抽象讓我們抓住最最重要的東西,從更高一層去思考。這降低了思考的復雜度,我們不用同時考慮那么多的東西。換言之,我們封裝了事物的本質,看不到任何細節。
在面向對象編程中,通過抽象類及接口,規定了具體類的特征作為抽象層,相對穩定,不需更改,從而滿足“對修改關閉”;而從抽象類導出的具體類可以改變系統的行為,從而滿足“對擴展開放”。
對實體進行擴展時,不必改動軟件的源代碼或者二進制代碼。關鍵在于抽象。

接口的定義

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

實現接口

// 內存緩存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 mDiskCache();

    // 先從內存中獲取圖片,如果沒有再從SD卡中獲取
    public Bitmap get(String url) {
        public 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);
    }
}

實現圖片加載器類

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

    // 注入緩存實現  利用了向上轉型
    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;
        }
        // 圖片沒有緩存,提交到線程池中下載圖片
        submitLoadRequest(imageUrl, imageView)
    }

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

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

調用方法

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

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

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

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

所以當需求發生變化時,應該盡量通過擴展的方式來實現變化,而不是通過修改已有代碼來實現,但要做到開閉原則,首先我們應該先寫出更易擴展的代碼。

里氏替換原則

里氏替換原則英文全稱是Liskov Substitution Principle,縮寫是LSP。LSP的第一種定義是:如果對每一個類型為S的對象O1,都有類型為T的對象O2,使得以T定義的所有程序P在所有的對象O1都代換成O2時,程序P的行為沒有發生變化,那么類型S是類型T的子類型。
或者說是:所有引用基類的地方必須能透明地使用其子類的對象。

就像開閉原則中舉的例子,創建了一個ImageCache,而其他緩存類都是他的實現類,而setImageCache(ImageCache imageCache) 需要的就是ImageCache類型,這時候我們就可以使用MemoryCache,DiskCache,DoubleCache來替換ImageCache的工作。ImageCache確定了規范,而新的緩存需求都可以通過實現它然后替換ImageCache來工作,從而保證了可擴展性。

也可以看一下系統代碼

// 窗口類
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() {
        // 繪制文本
    }
}

故里氏替換原則就是通過建立抽象,建立規范,然后在運行時通過具體實現來替換掉抽象,從而保證了系統的擴展性和靈活性。可見,在開發過程中運用抽象是走向代碼優化的重要一步。

開閉原則和里氏替換原則往往都是一同出現的,通過里氏替換原則達到對擴展的開發,對修改關閉的效果。

依賴倒置原則

依賴倒置原則英文全稱是Dependence Inversion Principle,縮寫是DIP。依賴倒置原則指代了一種特定的解耦形式,使得高層次的模塊不依賴于低層次的模塊的實現細節的目的,依賴模塊被顛倒了。

依賴倒置原則的三個關鍵點:

  1. 高層次模塊不應該依賴于底層模塊,兩者都應該依賴其抽象。
  2. 抽象不應依賴細節。
  3. 細節應該依賴抽象。

抽象就是指接口或者抽象類;細節就是實現類;高層模塊就是調用端,低層模塊就是具體實現類。

依賴倒置原則在Java中表現就是:模塊間依賴是通過抽象發生的,實現類之間并不產生直接依賴關系,其依賴關系是通過接口或抽象類產生的。
一句話概括:面向接口編程,或者說面向抽象編程。

我們依然可以通過上面的例子繼續說明,代碼如下:

//  如果在ImageLoader中直接這樣寫的話
//  就是直接依賴于細節(直接依賴實現類)
private DoubleCache mCache = new DoubleCache();
public void setImageCache(DoublieCache cache) {
    mCache = cache;
}

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

//  依賴于抽象,通過向上轉型,有一個默認的實現類
private ImageCache mImageCache = new MemoryCache();

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

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

接口隔離原則

接口隔離原則英文全稱是InterfaceSegregation Principles,縮寫是ISP。ISP的定義是:客戶端不應該依賴它不需要的接口。或者說類的依賴關系應該將在最小的接口上。

接口隔離的目的是系統接口耦合,從而容易重構、更改和重新部署。一句話:讓客戶端依賴的接口盡可能小。

舉一個例子,當我們在使用流的時候我們需要在finally中判斷是否為空,如果不為空需要close()它,但每次使用流,都這么寫,也會讓代碼變得不優美,這個時候我們考慮借助外力,就比如Java為我們提供了一個Closeable接口,而它有100多個實現類,所以那些類都可以使用它,代碼如下:

//  這就是修改之前的代碼 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();
               }
        }
}

//  寫了個CloseUtil類,然后西面提供這個靜態方法,所有實現了Closeable的類都可以調用這個方法
 public static void closeQuietly (Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
}

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

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

迪米特原則

迪米特原則英文全稱為Law of Demeter,縮寫是LOD,也稱為最少知識原則(Least Knowledge Principle)。LOD的定義是:一個對象應該對其他對象有最少的了解。

通俗的講,一個類應該對自己需要耦合或者調用的類知道的最少,類的內部如何實現與調用者或者依賴者沒有關系,只需要知道它需要的方法即可,其他的一概不管,類與類之間的關系越密切,耦合度也就越大。

迪米特原則還有一個英文解釋:Only talk to your immediate friends.翻譯過來也就是說之與直接朋友進行通信。

還是前面的ImageLoder,緩存這塊是已經搞定了。假如在某次加載圖片中,,緩存沒找到就需要聯網去服務器拿圖片,并且需要存到緩存中以備下次直接從緩存加載,ok,很快可以寫出這樣的代碼:

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下載類中從網絡中加載圖片方法:

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

分析下這樣設計的耦合情況,

  • ImageLoder調用ImageCache和HttpImage4Service。
  • HttpImage4Service調用ImageCache。

三個類之間是否知道的最少?試想一下,從網絡拉取圖片跟緩存這樣兩個類應該有關聯嗎?實際上是沒必要的,根據最少知識原則,改進之后應該是下面這樣的:

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);//只負責下載圖片
        imageView.setImageBitmap(bitmap);
        imageCache.put(url, bitmap);//存到緩存中
    }
}

改進后三個類中只有ImageLoder調用HttpImage4Service和ImageCache中的方法,其余沒有任何調用關系,耦合度降低。

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

總結

應用開發,最難的不是完成開發工作,而是維護和升級。為了后續能夠很好的維護和升級,我們的系統需要在滿足穩定性的前提下保持以下三個特性:

  • 高可擴展性
  • 高內聚
  • 低耦合

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容