設(shè)計(jì)模式概念和七大原則

什么是設(shè)計(jì)模式

在GoF(Gang of Four)的書籍《Design Patterns - Elements of Reusable Object-Oriented Software(設(shè)計(jì)模式-可復(fù)用面向?qū)ο筌浖幕A(chǔ))》中是這樣定義設(shè)計(jì)模式的:Christopher Alexander說(shuō)過:“每一個(gè)模式描述了一個(gè)在我們周圍不斷重復(fù)發(fā)生的問題以及該問題的解決方案的核心硫惕。這樣裁着,你就能一次又一次地使用該方案而不必做重復(fù)勞動(dòng)” [AIS+77沽翔,第10頁(yè)]。盡管Alexander所指的是城市和建筑模式气破,但他的思想也同樣適用于于面向?qū)ο笤O(shè)計(jì)模式焕数,只是在面向?qū)ο蟮慕鉀Q方案里泽疆, 我們喲偶那個(gè)對(duì)象和接口代替了墻壁和門窗要门。兩類模式的核心都在于提供了相關(guān)問題的解決方案。一般而言冠蒋,設(shè)計(jì)模式有四個(gè)基本要素:

  • 1羽圃、模式名稱(pattern name):一個(gè)助記名,它用一兩個(gè)詞來(lái)描述模式的問題抖剿、解決方案和效果朽寞。
  • 2、問題(problem):描述了應(yīng)該在何時(shí)使用模式斩郎。
  • 3脑融、解決方案(solution):描述了設(shè)計(jì)的組成成分,它們之間的相關(guān)關(guān)系以及各自的職責(zé)和協(xié)作方案缩宜。
  • 4肘迎、效果(consequences):描述了模式應(yīng)用的效果以及使用模式應(yīng)該權(quán)衡的問題。

設(shè)計(jì)模式的創(chuàng)始人很明確地指出了設(shè)計(jì)模式的基本要素锻煌,但是由于現(xiàn)實(shí)中浮躁妓布、偏向過度設(shè)計(jì)等因素的干擾,開發(fā)者很多時(shí)候會(huì)重點(diǎn)關(guān)注第1和第3點(diǎn)要素(過度關(guān)注設(shè)計(jì)模式和設(shè)計(jì)模式的實(shí)現(xiàn))宋梧,忽略第2和第4點(diǎn)要素(忽視使用設(shè)計(jì)模式的場(chǎng)景和目標(biāo))匣沼,導(dǎo)致設(shè)計(jì)出來(lái)的編碼邏輯可能過于復(fù)雜或者達(dá)不到預(yù)期的效果。

總的來(lái)說(shuō)捂龄,設(shè)計(jì)模式(Design Pattern)是一套被反復(fù)使用释涛、多數(shù)人知曉的、經(jīng)過分類編目的倦沧、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)唇撬。也就是本來(lái)并不存在所謂設(shè)計(jì)模式,用的人多了刀脏,也便成了設(shè)計(jì)模式局荚。

設(shè)計(jì)模式的七大原則

面向?qū)ο蟮脑O(shè)計(jì)模式有七大基本原則:

  • 開閉原則(Open Closed Principle,OCP)
  • 單一職責(zé)原則(Single Responsibility Principle, SRP)
  • 里氏代換原則(Liskov Substitution Principle愈污,LSP)
  • 依賴倒轉(zhuǎn)原則(Dependency Inversion Principle,DIP)
  • 接口隔離原則(Interface Segregation Principle轮傍,ISP)
  • 合成/聚合復(fù)用原則(Composite/Aggregate Reuse Principle暂雹,CARP)
  • 最少知識(shí)原則(Least Knowledge Principle,LKP)或者迪米特法則(Law of Demeter创夜,LOD)
設(shè)計(jì)模式原則名稱 簡(jiǎn)單定義
開閉原則 對(duì)擴(kuò)展開放杭跪,對(duì)修改關(guān)閉
單一職責(zé)原則 一個(gè)類只負(fù)責(zé)一個(gè)功能領(lǐng)域中的相應(yīng)職責(zé)
里氏代換原則 所有引用基類的地方必須能透明地使用其子類的對(duì)象
依賴倒轉(zhuǎn)原則 依賴于抽象,不能依賴于具體實(shí)現(xiàn)
接口隔離原則 類之間的依賴關(guān)系應(yīng)該建立在最小的接口上
合成/聚合復(fù)用原則 盡量使用合成/聚合,而不是通過繼承達(dá)到復(fù)用的目的
迪米特法則 一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少的與其他實(shí)體發(fā)生相互作用

這個(gè)表格看起來(lái)有點(diǎn)抽象涧尿,下面逐條分析系奉。

開閉原則

開閉原則(Open Closed Principle,OCP)的定義是:一個(gè)軟件實(shí)體如類姑廉、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放缺亮,對(duì)修改關(guān)閉。模塊應(yīng)盡量在不修改原(是"原"桥言,指原來(lái)的代碼)代碼的情況下進(jìn)行擴(kuò)展萌踱。

開閉原則的意義:

在軟件的生命周期內(nèi),因?yàn)樽兓虐ⅰ⑸?jí)和維護(hù)等原因需要對(duì)軟件原有代碼進(jìn)行修改時(shí)并鸵,可能會(huì)給舊代碼中引入錯(cuò)誤,也可能會(huì)使我們不得不對(duì)整個(gè)功能進(jìn)行重構(gòu)扔涧,并且需要原有代碼經(jīng)過重新測(cè)試园担。當(dāng)軟件需要變化時(shí),盡量通過擴(kuò)展軟件實(shí)體的行為來(lái)實(shí)現(xiàn)變化枯夜,而不是通過修改已有的代碼來(lái)實(shí)現(xiàn)變化弯汰。

如何實(shí)現(xiàn)對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉卤档?

要實(shí)現(xiàn)對(duì)擴(kuò)展開放蝙泼,對(duì)修改關(guān)閉,即遵循開閉原則劝枣,需要對(duì)系統(tǒng)進(jìn)行抽象化設(shè)計(jì)汤踏,抽象可以基于抽象類或者接口。一般來(lái)說(shuō)需要做到幾點(diǎn):

  • 1舔腾、通過接口或者抽象類約束擴(kuò)展溪胶,對(duì)擴(kuò)展進(jìn)行邊界限定,不允許出現(xiàn)在接口或抽象類中不存在的public方法稳诚,也就是擴(kuò)展必須添加具體實(shí)現(xiàn)而不是改變具體的方法哗脖。
  • 2、參數(shù)類型扳还、引用對(duì)象盡量使用接口或者抽象類才避,而不是實(shí)現(xiàn)類,這樣就能盡量保證抽象層是穩(wěn)定的氨距。
  • 3桑逝、一般抽象模塊設(shè)計(jì)完成(例如接口的方法已經(jīng)敲定),不允許修改接口或者抽象方法的定義俏让。

下面通過一個(gè)例子遵循開閉原則進(jìn)行設(shè)計(jì)楞遏,場(chǎng)景是這樣:某系統(tǒng)的后臺(tái)需要監(jiān)測(cè)業(yè)務(wù)數(shù)據(jù)展示圖表茬暇,如柱狀圖、折線圖等寡喝,在未來(lái)需要支持圖表的著色操作糙俗。在開始設(shè)計(jì)的時(shí)候,代碼可能是這樣的:

public class BarChart {

    public void draw(){
        System.out.println("Draw bar chart...");
    }
}

public class LineChart {

    public void draw(){
        System.out.println("Draw line chart...");
    }
}

public class App {

    public void drawChart(String type){
        if (type.equalsIgnoreCase("line")){
            new LineChart().draw();
        }else if (type.equalsIgnoreCase("bar")){
            new BarChart().draw();
        }
    }
}

這樣做在初期是能滿足業(yè)務(wù)需要的预鬓,開發(fā)效率也十分高巧骚,但是當(dāng)后面需要新增一個(gè)餅狀圖的時(shí)候,既要添加一個(gè)餅狀圖的類珊皿,原來(lái)的客戶端App類的drawChart方法也要新增一個(gè)if分支网缝,這樣做就是修改了原有客戶端類庫(kù)的方法,是十分不合理的蟋定。如果這個(gè)時(shí)候粉臊,在圖中加入一個(gè)顏色屬性,復(fù)雜性也大大提高驶兜《笾伲基于此,需要引入一個(gè)抽象Chart類AbstractChart抄淑,App類在畫圖的時(shí)候總是把相關(guān)的操作委托到具體的AbstractChart的派生類實(shí)例屠凶,這樣的話App類的代碼就不用修改:

public abstract class AbstractChart {

    public abstract void draw();
}

public class BarChart extends AbstractChart{

    @Override
    public void draw() {
        System.out.println("Draw bar chart...");
    }
}

public class LineChart extends AbstractChart {

    @Override
    public void draw() {
        System.out.println("Draw line chart...");
    }
}

public class App {

    public void drawChart(AbstractChart chart){
        chart.draw();
    }
}

如果新加一種圖,只需要新增一個(gè)AbstractChart的子類即可肆资〈@ⅲ客戶端類App不需要改變?cè)瓉?lái)的邏輯。修改后的設(shè)計(jì)符合開閉原則郑原,因?yàn)檎麄€(gè)系統(tǒng)在擴(kuò)展時(shí)原有的代碼沒有做任何修改。

單一職責(zé)原則

單一職責(zé)原則(Single Responsibility Principle, SRP)的定義是:指一個(gè)類或者模塊應(yīng)該有且只有一個(gè)改變的原因犯犁。如果一個(gè)類承擔(dān)的職責(zé)過多属愤,就等于把這些職責(zé)耦合在一起了。一個(gè)職責(zé)的變化可能會(huì)削弱或者抑制這個(gè)類完成其他職責(zé)的能力酸役。這種耦合會(huì)導(dǎo)致脆弱的設(shè)計(jì)住诸,當(dāng)發(fā)生變化時(shí),設(shè)計(jì)會(huì)遭受到意想不到的破壞涣澡。而如果想要避免這種現(xiàn)象的發(fā)生贱呐,就要盡可能的遵守單一職責(zé)原則。此原則的核心就是解耦和增強(qiáng)內(nèi)聚性入桂。

單一職責(zé)原則的意義:

單一職責(zé)原則告訴我們:一個(gè)類不能做太多的東西吼句。在軟件系統(tǒng)中,一個(gè)類(一個(gè)模塊事格、或者一個(gè)方法)承擔(dān)的職責(zé)越多惕艳,那么其被復(fù)用的可能性就會(huì)越低。一個(gè)很典型的例子就是萬(wàn)能類驹愚。其實(shí)可以說(shuō)一句大實(shí)話:任何一個(gè)常規(guī)的MVC項(xiàng)目远搪,在極端的情況下,可以用一個(gè)類(甚至一個(gè)方法)完成所有的功能逢捺。但是這樣做就會(huì)嚴(yán)重耦合谁鳍,甚至牽一發(fā)動(dòng)全身。一個(gè)類承(一個(gè)模塊劫瞳、或者一個(gè)方法)擔(dān)的職責(zé)過多倘潜,就相當(dāng)于將這些職責(zé)耦合在一起,當(dāng)其中一個(gè)職責(zé)變化時(shí)志于,可能會(huì)影響其他職責(zé)的運(yùn)作涮因,因此要將這些職責(zé)進(jìn)行分離,將不同的職責(zé)封裝在不同的類中伺绽,即將不同的變化原因封裝在不同的類中养泡,如果多個(gè)職責(zé)總是同時(shí)發(fā)生改變則可將它們封裝在同一類中。

不過說(shuō)實(shí)話奈应,其實(shí)有的時(shí)候很難去衡量一個(gè)類的職責(zé)澜掩,主要是很難確定職責(zé)的粒度。這一點(diǎn)不僅僅體現(xiàn)在一個(gè)類或者一個(gè)模塊中杖挣,也體現(xiàn)在采用微服務(wù)的分布式系統(tǒng)中肩榕。這也就是為什么我們?cè)趯?shí)施微服務(wù)拆分的時(shí)候經(jīng)常會(huì)撕逼:"這個(gè)功能不應(yīng)該發(fā)在A服務(wù)中,它不做這個(gè)領(lǐng)域的東西惩妇,應(yīng)該放在B服務(wù)中"諸如此類的爭(zhēng)論株汉。存在爭(zhēng)論是合理的,不過最好不要不了了之屿附,而應(yīng)該按照領(lǐng)域定義好每個(gè)服務(wù)的職責(zé)(職責(zé)的粒度最好找業(yè)務(wù)和架構(gòu)專家咨詢)郎逃,得出相對(duì)合理的職責(zé)分配。

下面通過一個(gè)很簡(jiǎn)單的實(shí)例說(shuō)明一下單一職責(zé)原則:

在一個(gè)項(xiàng)目系統(tǒng)代碼編寫的時(shí)候挺份,由于歷史原因和人為的不規(guī)范褒翰,導(dǎo)致項(xiàng)目沒有分層,一個(gè)Service類的偽代碼是這樣的:

public class Service {
    
    public UserDTO findUser(String name){
        Connection connection = getConnection();
        PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");
        preparedStatement.setObject(1, name);
        User user = //處理結(jié)果
        UserDTO dto = new UserDTO();
        //entity值拷貝到dto
        return dto;
    }
}

這里出現(xiàn)一個(gè)問題匀泊,Service做了太多東西优训,包括數(shù)據(jù)庫(kù)連接的管理,Sql的執(zhí)行這些業(yè)務(wù)層不應(yīng)該接觸到的邏輯各聘,更可怕的是揣非,例如到時(shí)候如果數(shù)據(jù)庫(kù)換成了Oracle,這個(gè)方法將會(huì)大改躲因。因此早敬,拆分出新的DataBaseUtils類用于專門管理數(shù)據(jù)庫(kù)資源忌傻,Dao類用于專門執(zhí)行查詢和查詢結(jié)果封裝,改造后Service類的偽代碼如下:

public class Service {

    private Dao dao;
    
    public UserDTO findUser(String name){
       User user =  dao.findUserByName(name);
       UserDTO dto = new UserDTO();
        //entity值拷貝到dto
       return dto;
    }
}


public class Dao{

    public User findUserByName(String name){
       Connection connection = DataBaseUtils.getConnnection();
       PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");
        preparedStatement.setObject(1, name);
        User user = //處理結(jié)果
        return user;
    }
}

現(xiàn)在搞监,如果有查詢封裝的變動(dòng)只需要修改Dao類水孩,數(shù)據(jù)庫(kù)相關(guān)變動(dòng)只需要修改DataBaseUtils類,每個(gè)類的職責(zé)分明琐驴。這個(gè)時(shí)候俘种,如果我們要把底層的存儲(chǔ)結(jié)構(gòu)緩成Redis或者M(jìn)ongoDB怎么辦,這樣顯然要重建整個(gè)Dao類绝淡,這種情況下宙刘,需要進(jìn)行接口隔離,下面分析接口隔離原則的時(shí)候再詳細(xì)分析牢酵。

里氏代換原則

里氏代換原則(Liskov Substitution Principle悬包,LSP)的定義是:所有引用基類的地方必須能透明地使用其子類的對(duì)象,也可以簡(jiǎn)單理解為任何基類可以出現(xiàn)的地方茁帽,子類一定可以出現(xiàn)玉罐。

里氏代換原則的意義:

只有當(dāng)衍生類可以替換掉基類,軟件單位的功能不受到影響時(shí)潘拨,基類才能真正被復(fù)用吊输,而衍生類也能夠在基類的基礎(chǔ)上增加新的行為。里氏代換原則是對(duì)"開-閉"原則的補(bǔ)充铁追。實(shí)現(xiàn)"開-閉"原則的關(guān)鍵步驟就是抽象化季蚂。而基類與子類的繼承關(guān)系就是抽象化的具體實(shí)現(xiàn),所以里氏代換原則是對(duì)實(shí)現(xiàn)抽象化的具體步驟的規(guī)范琅束。當(dāng)然扭屁,如果反過來(lái),軟件單位使用的是一個(gè)子類對(duì)象的話涩禀,那么它不一定能夠使用基類對(duì)象料滥。舉個(gè)很簡(jiǎn)單的例子說(shuō)明這個(gè)問題:如果一個(gè)方法接收Map類型參數(shù),那么它一定可以接收Map的子類參數(shù)例如HashMap艾船、LinkedHashMap葵腹、ConcurrentHashMap類型的參數(shù);但是返過來(lái)屿岂,如果另一個(gè)方法只接收HashMap類型的參數(shù)践宴,那么它一定不能接收所有Map類型的參數(shù),否則它可以接收LinkedHashMap爷怀、ConcurrentHashMap類型的參數(shù)阻肩。

子類為什么可以替換基類的位置?

其實(shí)原因很簡(jiǎn)單运授,只要存在繼承關(guān)系烤惊,基類的所有非私有屬性或者方法乔煞,子類都可以通過繼承獲得(白箱復(fù)用),反過來(lái)不成立撕氧,因?yàn)樽宇惡苡锌赡軘U(kuò)充自身的非私有屬性或者方法瘤缩,這個(gè)時(shí)候不能用基類獲取子類新增的這些屬性或者方法。

里氏代換原則是實(shí)現(xiàn)開閉原則的基礎(chǔ)伦泥,它告訴我們?cè)谠O(shè)計(jì)程序的時(shí)候進(jìn)可能使用基類進(jìn)行對(duì)象的定義和引用,在運(yùn)行時(shí)再?zèng)Q定基類的具體子類型锦溪。

舉個(gè)簡(jiǎn)單的例子不脯,假設(shè)一種會(huì)呼吸的動(dòng)物作為父類,子類豬和鳥也有自身的呼吸方式:

public abstract class Animal {

    protected abstract void breathe();
}

public class Bird extends Animal {

    @Override
    public void breathe() {
        System.out.println("Bird breathes...");
    }
}

public class Pig extends Animal {

    @Override
    public void breathe() {
        System.out.println("Pig breathes...");
    }
}

public class App {

    public static void main(String[] args) throws Exception {
        Animal bird = new Bird();
        bird.breathe();
        Animal pig = new Pig();
        pig.breathe();
    }
}    

依賴倒轉(zhuǎn)原則

依賴倒轉(zhuǎn)原則(Dependency Inversion Principle刻诊,DIP)的定義:程序要依賴于抽象接口防楷,不要依賴于具體實(shí)現(xiàn)。簡(jiǎn)單的說(shuō)就是要求對(duì)抽象進(jìn)行編程则涯,不要對(duì)實(shí)現(xiàn)進(jìn)行編程复局,這樣就降低了客戶與實(shí)現(xiàn)模塊間的耦合。

依賴倒轉(zhuǎn)原則的意義:

依賴倒轉(zhuǎn)原則要求我們?cè)诔绦虼a中傳遞參數(shù)時(shí)或在關(guān)聯(lián)關(guān)系中粟判,盡量引用層次高的抽象層類亿昏,即使用接口和抽象類進(jìn)行變量類型聲明、參數(shù)類型聲明档礁、方法返回類型聲明角钩,以及數(shù)據(jù)類型的轉(zhuǎn)換等,而不要用具體類來(lái)做這些事情呻澜。為了確保該原則的應(yīng)用递礼,一個(gè)具體類應(yīng)當(dāng)只實(shí)現(xiàn)接口或抽象類中聲明過的方法,而不要給出多余的方法羹幸,否則將無(wú)法調(diào)用到在子類中增加的新方法脊髓。在引入抽象層后,系統(tǒng)將具有很好的靈活性栅受,在程序中盡量使用抽象層進(jìn)行編程将硝,而將具體類寫在配置文件中,這樣一來(lái)窘疮,如果系統(tǒng)行為發(fā)生變化袋哼,只需要對(duì)抽象層進(jìn)行擴(kuò)展,并修改配置文件闸衫,而無(wú)須修改原有系統(tǒng)的源代碼涛贯,在不修改的情況下來(lái)擴(kuò)展系統(tǒng)的功能,滿足開閉原則的要求蔚出。

依賴倒轉(zhuǎn)原則的注意事項(xiàng):

  • 高層模塊不應(yīng)該依賴低層模塊弟翘,高層模塊和低層模塊都應(yīng)該依賴于抽象虫腋。
  • 抽象不應(yīng)該依賴于具體,具體應(yīng)該依賴于抽象稀余。

在實(shí)現(xiàn)依賴倒轉(zhuǎn)原則時(shí)悦冀,我們需要針對(duì)抽象層編程,而將具體類的對(duì)象通過依賴注入(DependencyInjection, DI)的方式注入到其他對(duì)象中睛琳,依賴注入是指當(dāng)一個(gè)對(duì)象要與其他對(duì)象發(fā)生依賴關(guān)系時(shí)盒蟆,通過抽象來(lái)注入所依賴的對(duì)象坏挠。常用的注入方式有三種澳淑,分別是:構(gòu)造注入,設(shè)值注入(Setter注入)和接口注入滥玷。Spring的IOC是此實(shí)現(xiàn)的典范辟癌。

從Java角度看待依賴倒轉(zhuǎn)原則的本質(zhì)就是:面向接口(抽象)編程寒屯。

  • 每個(gè)具體的類都應(yīng)該有其接口或者基類,或者兩者都具備黍少。
  • 類中的引用對(duì)象應(yīng)該是接口或者基類寡夹。
  • 任何具體類都不應(yīng)該派生出子類。
  • 盡量不要覆寫基類中的方法厂置。
  • 結(jié)合里氏代換原則使用菩掏。

遵循依賴倒轉(zhuǎn)原則的一個(gè)例子,場(chǎng)景是司機(jī)開車:

public interface Driver {

    void drive();

    void setCar(Car car);
}

public interface Car {

    void run();
}

public class DefaultDriver implements Driver {

    private Car car;

    @Override
    public void drive() {
        car.run();
    }

    @Override
    public void setCar(Car car) {
        this.car = car;
    }
}

public class Bmw implements Car {

    @Override
    public void run() {
        System.out.println("Bmw runs...");
    }
}

public class Benz implements Car {

    @Override
    public void run() {
        System.out.println("Benz runs...");
    }
}

public class App {

    public static void main(String[] args) throws Exception {
        Driver driver = new DefaultDriver();
        Car car = new Benz();
        driver.setCar(car);
        driver.drive();
        car = new Bmw();
        driver.setCar(car);
        driver.drive();
    }
}

這樣實(shí)現(xiàn)了一個(gè)司機(jī)可以開各種類型的車农渊,如果還有其他類型的車患蹂,只需要新加一個(gè)Car的實(shí)現(xiàn)即可。

接口隔離原則

接口隔離原則(Interface Segregation Principle砸紊,ISP)的定義是客戶端不應(yīng)該依賴它不需要的接口传于,類間的依賴關(guān)系應(yīng)該建立在最小的接口上。簡(jiǎn)單來(lái)說(shuō)就是建立單一的接口醉顽,不要建立臃腫龐大的接口沼溜。也就是接口盡量細(xì)化,同時(shí)接口中的方法盡量少游添。

如何看待接口隔離原則和單一職責(zé)原則系草?

單一職責(zé)原則注重的是類和接口的職責(zé)單一,這里職責(zé)是從業(yè)務(wù)邏輯上劃分的唆涝,但是在接口隔離原則要求當(dāng)一個(gè)接口太大時(shí)找都,我們需要將它分割成一些更細(xì)小的接口,使用該接口的客戶端僅需知道與之相關(guān)的方法即可廊酣。也就是說(shuō)能耻,我們?cè)谠O(shè)計(jì)接口的時(shí)候有可能滿足單一職責(zé)原則但是不滿足接口隔離原則。

接口隔離原則的規(guī)范:

  • 使用接口隔離原則前首先需要滿足單一職責(zé)原則。
  • 接口需要高內(nèi)聚晓猛,也就是提高接口饿幅、類、模塊的處理能力戒职,少對(duì)外發(fā)布public的方法栗恩。
  • 定制服務(wù),就是單獨(dú)為一個(gè)個(gè)體提供優(yōu)良的服務(wù)洪燥,簡(jiǎn)單來(lái)說(shuō)就是拆分接口磕秤,對(duì)特定接口進(jìn)行定制。
  • 接口設(shè)計(jì)是有限度的蚓曼,接口的設(shè)計(jì)粒度越小亲澡,系統(tǒng)越靈活,但是值得注意不能過小纫版,否則變成"字節(jié)碼編程"。

如果有用過spring-data-redis的人就知道客情,RedisTemplate中持有一些列的基類其弊,分別是ValueOperations(處理K-V)、ListOperations(處理Hash)膀斋、SetOperations(處理集合)等等梭伐。

public interface ValueOperations<K, V> {

    void set(K key, V value);
    void set(K key, V value, long timeout, TimeUnit unit);
    //....
}

合成/聚合復(fù)用原則

合成/聚合復(fù)用原則(Composite/Aggregate Reuse Principle,CARP)一般也叫合成復(fù)用原則(Composite Reuse Principle, CRP)仰担,定義是:盡量使用合成/聚合糊识,而不是通過繼承達(dá)到復(fù)用的目的

合成/聚合復(fù)用原則就是在一個(gè)新的對(duì)象里面使用一些已有的對(duì)象摔蓝,使之成為新對(duì)象的一部分赂苗;新的對(duì)象通過向內(nèi)部持有的這些對(duì)象的委派達(dá)到復(fù)用已有功能的目的,而不是通過繼承來(lái)獲得已有的功能贮尉。

聚合(Aggregate)的概念:

聚合表示一種弱的"擁有"關(guān)系拌滋,一般表現(xiàn)為松散的整體和部分的關(guān)系,其實(shí)猜谚,所謂整體和部分也可以是完全不相關(guān)的败砂。例如A對(duì)象持有B對(duì)象,B對(duì)象并不是A對(duì)象的一部分魏铅,也就是B對(duì)象的生命周期是B對(duì)象自身管理昌犹,和A對(duì)象不相關(guān)。

合成(Composite)的概念:

合成表示一種強(qiáng)的"擁有"關(guān)系览芳,一般表現(xiàn)為嚴(yán)格的整體和部分的關(guān)系斜姥,部分和整體的生命周期是一樣的。

聚合和合成的關(guān)系:

這里用山羊舉例說(shuō)明聚合和合成的關(guān)系:

dp-1.png

為什么要用合成/聚合來(lái)替代繼承達(dá)到復(fù)用的目的?

繼承復(fù)用破壞包裝,因?yàn)槔^承將基類的實(shí)現(xiàn)細(xì)節(jié)暴露給派生類疾渴,基類的內(nèi)部細(xì)節(jié)通常對(duì)子類來(lái)說(shuō)是可見的千贯,這種復(fù)用也稱為"白箱復(fù)用"。這里有一個(gè)明顯的問題是:派生類繼承自基類搞坝,如果基類的實(shí)現(xiàn)發(fā)生改變搔谴,將會(huì)影響到所有派生類的實(shí)現(xiàn);如果從基類繼承而來(lái)的實(shí)現(xiàn)是靜態(tài)的桩撮,不可能在運(yùn)行時(shí)發(fā)生改變敦第,不夠靈活。

由于合成或聚合關(guān)系可以將已有的對(duì)象店量,一般叫成員對(duì)象芜果,納入到新對(duì)象中,使之成為新對(duì)象的一部分融师,因此新對(duì)象可以調(diào)用已有對(duì)象的功能右钾,這樣做可以使得成員對(duì)象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)對(duì)于新對(duì)象不可見,所以這種復(fù)用又稱為"黑箱"復(fù)用旱爆,相對(duì)繼承關(guān)系而言舀射,其耦合度相對(duì)較低,成員對(duì)象的變化對(duì)新對(duì)象的影響不大怀伦,可以在新對(duì)象中根據(jù)實(shí)際需要有選擇性地調(diào)用成員對(duì)象的操作脆烟;合成/聚合復(fù)用可以在運(yùn)行時(shí)動(dòng)態(tài)進(jìn)行,新對(duì)象可以動(dòng)態(tài)地引用與成員對(duì)象類型相同的其他對(duì)象房待。

如果有閱讀過《Effective Java 2nd》的同學(xué)就知道邢羔,此書也建議慎用繼承。一般情況下桑孩,只有明確知道派生類和基類滿IS A的時(shí)候才選用繼承拜鹤,當(dāng)滿足HAS A或者不能判斷的情況下應(yīng)該選用合成/聚合。

下面舉個(gè)很極端的例子說(shuō)明一下如果在非IS A的情況下使用繼承會(huì)出現(xiàn)什么問題:

先定義一個(gè)抽象手洼怔,手有一個(gè)搖擺的方法署惯,然后定義左右手繼承抽象手,實(shí)現(xiàn)搖擺方法:

public abstract class AbstractHand {

    protected abstract void swing();
}

public class LeftHand extends AbstractHand {

    @Override
    public void swing() {
        System.out.println("Left hand swings...");
    }
}

public class RightHand extends AbstractHand {

    @Override
    public void swing() {
        System.out.println("Right hand swings...");
    }
}

現(xiàn)在看起來(lái)沒有任何問題镣隶,實(shí)現(xiàn)也十分正確极谊,現(xiàn)在出現(xiàn)了人(Person)這個(gè)類,具備搖左右手的功能安岂,如果不考慮IS A的關(guān)系轻猖,很有可能有人會(huì)這樣做:

public abstract class AbstractSwingHand extends AbstractHand{

    @Override
    protected void swing() {
        System.out.println(" hand swings...");
    }
}

public class Person extends AbstractSwingHand {

    public void swingLeftHand(){
        System.out.print("Left ");
        super.swing();
    }

    public void swingRightHand(){
        System.out.print("Right ");
        super.swing();
    }
}

上面Person的實(shí)現(xiàn)讓人覺得百思不得其解,但是往往這會(huì)出現(xiàn)在真實(shí)的環(huán)境中域那,因?yàn)镠and不是Person咙边,所以Person繼承Hand一定會(huì)出現(xiàn)曲線實(shí)現(xiàn)等奇葩邏輯猜煮。Hand和Person是嚴(yán)格的部分和整體的關(guān)系,或者說(shuō)Person和Hand是HAS A的關(guān)系败许,如果使用合成王带,邏輯將會(huì)十分清晰:

public class Person  {

    private AbstractHand leftHand;
    private AbstractHand rightHand;

    public Person() {
        leftHand = new LeftHand();
        rightHand = new RightHand();
    }

    public void swingLeftHand(){
        leftHand.swing();
    }

    public void swingRightHand(){
        rightHand.swing();
    }
}

這里使用了合成,說(shuō)明了Person和AbstractHand實(shí)例的生命周期是一致的市殷。

迪米特法則

迪米特法則(Law of Demeter愕撰,LOD),有時(shí)候也叫做最少知識(shí)原則(Least Knowledge Principle醋寝,LKP)搞挣,它的定義是:一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少地與其他實(shí)體發(fā)生相互作用。每一個(gè)軟件單位對(duì)其他的單位都只有最少的知識(shí)音羞,而且局限于那些與本單位密切相關(guān)的軟件單位囱桨。迪米特法則的初衷在于降低類之間的耦合。由于每個(gè)類盡量減少對(duì)其他類的依賴嗅绰,因此舍肠,很容易使得系統(tǒng)的功能模塊功能獨(dú)立,相互之間不存在(或很少有)依賴關(guān)系窘面。迪米特法則不希望類之間建立直接的聯(lián)系貌夕。如果真的有需要建立聯(lián)系,也希望能通過它的友元類(中間類或者跳轉(zhuǎn)類)來(lái)轉(zhuǎn)達(dá)民镜。

迪米特法則的規(guī)則:

  • Only talk to your immediate friends(只與直接的朋友通訊),一個(gè)對(duì)象的"朋友"包括他本身(this)险毁、它持有的成員對(duì)象制圈、入?yún)?duì)象、它所創(chuàng)建的對(duì)象畔况。
  • 盡量少發(fā)布public的變量和方法鲸鹦,一旦公開的屬性和方法越多,修改的時(shí)候影響的范圍越大跷跪。
  • "是自己的就是自己的"馋嗜,如果一個(gè)方法放在本類中,既不產(chǎn)生新的類間依賴吵瞻,也不造成負(fù)面的影響葛菇,那么次方法就應(yīng)該放在本類中。

迪米特法則的意義:

迪米特法則的核心觀念就是類間解耦橡羞,也就降低類之間的耦合眯停,只有類處于弱耦合狀態(tài),類的復(fù)用率才會(huì)提高卿泽。所謂降低類間耦合莺债,實(shí)際上就是盡量減少對(duì)象之間的交互,如果兩個(gè)對(duì)象之間不必彼此直接通信,那么這兩個(gè)對(duì)象就不應(yīng)當(dāng)發(fā)生任何直接的相互作用齐邦,如果其中的一個(gè)對(duì)象需要調(diào)用另一個(gè)對(duì)象的某一個(gè)方法的話椎侠,可以通過第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用。簡(jiǎn)言之措拇,就是通過引入一個(gè)合理的第三者來(lái)降低現(xiàn)有對(duì)象之間的耦合度我纪。但是這樣會(huì)引發(fā)一個(gè)問題,有可能產(chǎn)生大量的中間類或者跳轉(zhuǎn)類儡羔,導(dǎo)致系統(tǒng)的復(fù)雜性提高宣羊,可維護(hù)性降低。如果一味追求極度解耦汰蜘,那么最終有可能變成面向字節(jié)碼編程甚至是面向二進(jìn)制的0和1編程仇冯。

舉個(gè)很簡(jiǎn)單的例子,體育老師要知道班里面女生的人數(shù)族操,他委托體育課代表點(diǎn)清女生的人數(shù):

public class Girl {
    
}

public class GroupLeader {

    private final List<Girl> girls;

    public GroupLeader(List<Girl> girls) {
        this.girls = girls;
    }

    public void countGirls() {
        System.out.println("The sum of girls is " + girls.size());
    }
}

public class Teacher {

    public void command(GroupLeader leader){
        leader.countGirls();
    }
}

public class App {

    public static void main(String[] args) throws Exception {
        Teacher teacher = new Teacher();
        GroupLeader groupLeader = new GroupLeader(Arrays.asList(new Girl(), new Girl()));
        teacher.command(groupLeader);
    }
}

這個(gè)例子中苛坚,體育課代表就是中間類,體育課代表對(duì)于體育老師來(lái)說(shuō)就是"直接的朋友"色难,如果去掉體育課代表這個(gè)中間類泼舱,體育老師必須親自清點(diǎn)女生的人數(shù)(實(shí)際上就數(shù)人數(shù)這個(gè)功能,體育老師是不必要獲取所有女生的對(duì)象列表)枷莉,這樣做會(huì)違反迪米特法則娇昙。

小結(jié)

說(shuō)實(shí)話,設(shè)計(jì)模式的七大原則理解是比較困難的笤妙,我們?cè)谠O(shè)計(jì)模式的學(xué)習(xí)和應(yīng)用中經(jīng)常會(huì)聽到或者看到"XXX模式符合XXX原則"冒掌、"YYY模式不符合YYY原則"這樣的語(yǔ)句。因此蹲盘,為了分析設(shè)計(jì)模式的合理性和完善我們?nèi)粘5木幋a股毫,掌握和理解這七大原則是十分必要的。

參考

  • 《Java設(shè)計(jì)模式》
  • 《設(shè)計(jì)模式之禪-2nd》
  • 《設(shè)計(jì)模式-可復(fù)用面向?qū)ο筌浖幕A(chǔ)》

(本文完)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末召衔,一起剝皮案震驚了整個(gè)濱河市铃诬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苍凛,老刑警劉巖趣席,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異毫深,居然都是意外死亡吩坝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門哑蔫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钉寝,“玉大人弧呐,你說(shuō)我怎么就攤上這事∏陡伲” “怎么了俘枫?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)逮走。 經(jīng)常有香客問我鸠蚪,道長(zhǎng),這世上最難降的妖魔是什么师溅? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任茅信,我火速辦了婚禮,結(jié)果婚禮上墓臭,老公的妹妹穿的比我還像新娘蘸鲸。我一直安慰自己,他們只是感情好窿锉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布酌摇。 她就那樣靜靜地躺著,像睡著了一般嗡载。 火紅的嫁衣襯著肌膚如雪窑多。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天洼滚,我揣著相機(jī)與錄音埂息,去河邊找鬼。 笑死遥巴,一個(gè)胖子當(dāng)著我的面吹牛耿芹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挪哄,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼琉闪!你這毒婦竟也來(lái)了迹炼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤颠毙,失蹤者是張志新(化名)和其女友劉穎斯入,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛀蜜,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刻两,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滴某。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片磅摹。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡滋迈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出户誓,到底是詐尸還是另有隱情饼灿,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布帝美,位于F島的核電站碍彭,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏悼潭。R本人自食惡果不足惜庇忌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舰褪。 院中可真熱鬧皆疹,春花似錦、人聲如沸抵知。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)刷喜。三九已至残制,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掖疮,已是汗流浹背初茶。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浊闪,地道東北人恼布。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像搁宾,于是被迫代替她去往敵國(guó)和親折汞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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

  • 參考資料:菜鳥教程之設(shè)計(jì)模式 設(shè)計(jì)模式概述 設(shè)計(jì)模式(Design pattern)代表了最佳的實(shí)踐盖腿,通常被有經(jīng)驗(yàn)...
    Steven1997閱讀 1,174評(píng)論 1 12
  • 設(shè)計(jì)模式之六大原則(轉(zhuǎn)載) 關(guān)于設(shè)計(jì)模式的六大設(shè)計(jì)原則的資料網(wǎng)上很多...
    霄霄霄霄閱讀 899評(píng)論 0 1
  • title: 設(shè)計(jì)模式簡(jiǎn)介categories: 設(shè)計(jì)模式tags: 設(shè)計(jì)模式date: 2017-05-03 0...
    九命丿相柳閱讀 584評(píng)論 0 0
  • 90年代初翩腐,光明村有一方姓人家憑著勤勞節(jié)儉鸟款,就率先在自家宅基地上矗立了一幢二層小洋房,在四周的土坯房中著實(shí)耀眼茂卦。 ...
    劉苔米閱讀 374評(píng)論 2 1
  • 題記:為充滿生命活力的孩子們等龙,提供成長(zhǎng)的陽(yáng)光处渣、空氣伶贰、和養(yǎng)分是老師的責(zé)任所在…… 一、回顧往昔 2017年9月我有幸...
    沅陵270余瑤閱讀 1,011評(píng)論 2 2