本文主要介紹設計模式的主要設計原則和常用設計模式。
一、UML畫圖
1.類圖
2.時序圖
二、設計模式原則
1.單一職責原則
就是一個方法、一個類只做一件事;
2.開閉原則
就是軟件的設計應該對拓展開放,對修改關閉,這在java中體現最明顯的就是訪問控制修飾符;
3.里式替換原則
就是在設計父子關系時,就是如果能用子類,則一定也能用父類替代;
4.接口隔離
每個接口的設計要高內聚,低耦合;
5.迪特米原則
這個也叫最小知道原則,就是設計一個類的時候,要保證這個對象知道的最小;
6.依賴倒置原則
主要是為了解決類之間的耦合性,這個在spring中體現的很詳細,就是類間的依賴通過接口來實現;
三、常用設計模式
設計模式分為三種類型,共23種:
創建型模式:單例模式、抽象工廠模式、建造者模式、工廠模式、原型模式。
結構型模式:適配器模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式。
行為型模式:模版方法模式、命令模式、迭代器模式、觀察者模式、中介者模式、備忘錄模式、解釋器模式、狀態模式、策略模式、責任鏈模式、訪問者模式。
創建型模式
1.原型模式(singleton)
1) 概述
解決問題:在系統中其實只需要該類的一個對象,為避免頻繁創建和銷毀其他對象,只給該類一個單例對象供使用;解決方法:給該類一個全局靜態變量來保存單例,只提供一個共有方法來獲取該實例,不提供構造方法來實例化對象;使用場景:1.只需要一個全局實例的,比如說生產全局UUID的生成器;
2) 具體說明
普通模式:重點注意由于只有一個實例化對象,所以實例變量是靜態成員變量;再就是構造方法要是private修飾,排除new方法創建新的對象;還有就是創建實例對象的方法要是public static,這樣才能通過類名直接創建;
餓漢式:就是創建一個靜態常量作為實例,這樣類一加載就完成了實例對象的初始化操作;劣勢也很明顯,就是沒法懶加載,但后面沒有用到該單例時候也被加載進來的了;
懶漢式:就是在調用getInstance()方法時才實例出單例對象,并且之前創建單例模式的方法都沒有考慮到并發情況,當同時進入到getInstance()創建實例時候,沒有辦法保證只創建一個實例,所以動用synchronized和volatile關鍵字來保證方法的調用并發執行;
雙重檢驗式:在創建單例之前判斷單例是否存在,再進入加鎖階段,和懶漢式有區別的是加鎖階段后還會再次判斷是否單例存在,這是生產環境常用的模式。
IoDH方式:就是通過內部靜態類的方式來避免懶漢式類加載的時候就創建了實例的問題;
3) 代碼實現
/*** @author yangnk* @desc* @date 2023/08/13 16:46**/
public class Singleton {//volatile 保障可見性private volatile static Singleton singleton;//private構造方法,無法通過new來實例化對象private Singleton() { }public static Singleton getSingleton() {if (singleton == null) {//用synchronized加鎖,只允許并發情況下進行一個實例化操作synchronized (Singleton.class) {//需要再次判斷singletonTest == null,避免指令重排導致初始化和分配內存失序if (singleton == null) {singleton = new Singleton();}}}return singleton;}public static void main(String[] args) {for (int i = 0; i < 3; i++) {Singleton singleton = Singleton.getSingleton();System.out.println("singletonTest.hashCode() = " + singleton.hashCode());}}
}
4) 總結
優點:1、只有一個對象在內存中,不用占用過多堆內存;2、只有一個實例,可以嚴格控制訪問控制;缺點:1、違法類設計模式的單一指責的原則,一個實例就能做好所有的事?肯定不行;
2.工廠模式(factory)
1) 概述
解決問題:將產品實例的創建過程封裝在工廠類中實現,將產品的聲明和創建分離開來,提供統一的接口來創建不同的產品實例,解決需要創建多種繼承同一接口的產品;解決方法:每個產品實現同一抽象產品類,每個工廠類實現同一抽象工廠類,通過多態特性實現不同的產品由不同的產品工廠類來實現;使用場景:工廠設計模式在各大框架中是運用最多的,在自己之前寫過的代碼中,日志記錄也用到了這種設計模式,他可以拓展其他記錄日志的方式;再通過反射和配置的形式,可以做到不需要修改代碼就能實現不同記錄日志的功能;
2) 具體說明
**主要角色:**
抽象工廠類:聲明了一組用于創建產品對象的方法,每個方法對應一種產品類型。抽象工廠可以是接口或抽象類。
具體工廠類:生產具體產品的類,實現了抽象工廠類;
抽象產品類:是具體產品類的父類,封裝了具體產品的方法;
具體產品類:實現具體方法,實現了抽象產品類;
幾種不同的工廠模式:
簡單工廠模式:工廠方法的思路是通過工廠類來創建相應的實體對象,簡單工廠的邏輯是具體產品類實現抽象或接口產品類的方法,外部想要使用該產品需要通過工廠類來獲取,該類工廠模式通過判定來生產相應的具體產品類;
抽象工廠模式:當碰到所需生產的具體產品太多的時候,它只需要實現相應的具體產品類,但是在工廠類中,他是需要修改代碼來實現的,這就違背了設計模式修改閉合的原則,所以借鑒產品類有抽象或接口的思路,工廠類也有相應的抽象類或接口,如果新增了具體產品,就新增相應產品工廠類。
3) 代碼實現
4) 總結
優勢:1、向用戶隱藏了產品生產的邏輯,符合Java良好封裝性的特征;2、如果想要再加新的產品類,不需要修改其他代碼,只需要實現他們的接口就好了;劣勢:1、如果產品很多那就很麻煩了,所以工廠模式不太適合產品超過5個的設計;
3.原型模式(prototype)
1) 概述
模式定義:允許通過拷貝原始對象的屬性創建一個相同的對象,而不需要關注這個對象怎么創建的。
解決問題:不需要關注怎么創建一個新的對象。
解決方案:在Java中年通過實clone()方法,在Spring中通過聲明bean為Prototype類型來實現。
應用場景:通過Java中的clone方法可以根據模板對象克隆一個原型對象,這就是原型模式的應用。在使用原型模式的時候要著重關注淺克隆還是深克隆。比較適合創建一個實例化比較復雜的時候,比如創建網絡鏈接或數據庫連接; 原型創建的對象一般會緩存起來,在運行時在緩存中獲取即可。
2) 具體說明
原型的設計模式在于系統中有一個原型實例,之后在需要只需要**復制**他的副本就好了。原型設計模式最初由原型抽象類和原形實現類組成,其中最關鍵的在于要實現原型副本的拷貝,在Java中這是很容易實現的,直接重寫clone方法就好了,但這個要注意Java里的深復制和淺復制,如果成員變量有對象的話,需要實現通過流來實現深復制。之后原型進化出另外一個組件,是原型管理器,其將原型保存到一個map中,等到其他類需要的話,再從這個map中拷貝。
3) 代碼實現
4) 總結
優勢:提高實例化對象的性能,不需要關注構造函數和初始化的過程,一般創建好的原型對象會放在緩存中,后續直接從緩存中獲取對象即可 。劣勢:如果類的成員變量引用比較深的話,那樣clone起來就比較麻煩了;參考資料:深度分析:java設計模式中的原型模式,看完就沒有說不懂的:https://segmentfault.com/a/1190000023831083
4.構建者模式(Builder)
1) 概述
- 解決問題:當實例化一個對象的時,需要配置多個參數,并且參數的組合類型還非常多的情況,可以考慮使用使用構建者模式。
- 解決方案:通過Builder統一給需要的對象來進行構建。
- 應用場景:應用于一個實例需要通過配置 多個參數才能構建,并且這樣的參數搭配有很多種,這種情況可以將產品本身和產品的創建解耦,產品的創建就交給builder來實現,實踐中一般構造函數中的參數超過4個就可以考慮使用構建者模式了;
2) 具體說明
構建者模式講的是這樣一件事,就是如果一個實例有很多類組件或屬性共同組成,而且沒有固定套路,這樣的的組合有很多種,所以就需要一種套餐的形式去構建,就像kfc套餐一樣,漢堡和飲料作為套餐,具體類別你可以再選擇;
構建者模式有三大組件,包括builder接口類、builder實現類還有director。他們的邏輯關系是這樣的,builder實現類負責組合各類產品,最合成一個套餐,這個套餐其實在他的接口中就已經制定好了,而director就負責構建這樣一個套餐;
3) 代碼實現
簡單構建者模式的實現:
public class Computer {private final String cpu;//必須private final String ram;//必須private final int usbCount;//可選private final String keyboard;//可選private final String display;//可選private Computer(Builder builder){this.cpu=builder.cpu;this.ram=builder.ram;this.usbCount=builder.usbCount;this.keyboard=builder.keyboard;this.display=builder.display;}public static class Builder{private String cpu;//必須private String ram;//必須private int usbCount;//可選private String keyboard;//可選private String display;//可選public Builder(String cup,String ram){this.cpu=cup;this.ram=ram;}public Builder setUsbCount(int usbCount) {this.usbCount = usbCount;return this;}public Builder setKeyboard(String keyboard) {this.keyboard = keyboard;return this;}public Builder setDisplay(String display) {this.display = display;return this;}public Computer build(){return new Computer(this);}}
}
4) 總結
優勢:針對很多參數組成的實例,將對象的構建和使用分離,能夠非常靈活的構建一個對象;
劣勢:1.他有一定的局限性,就是builder的實例是同一個類別的,或者說他是實現同一個接口的;2.使用構建者模式進行對象創建適合參數比較多得情況,參數較少不建議使用;
參考資料:秒懂設計模式之建造者模式(Builder pattern) shusheng007:https://zhuanlan.zhihu.com/p/58093669
結構性模式
5.適配器模式(adapter)
1) 概述
解決問題:已有的對象和需要的對象無法直接對接,需要通過適配器來進行中轉適配。
解決方案:適配器繼承或者依賴適配者,在適配器中實現需要提供的功能;
使用場景:適配器類的使用場景還是很廣的, 在SpringMVC中就有廣泛的使用,比如Handler和HandlerAdapter的關系;
2) 具體說明
主要角色:
- 適配器接口類:規定適配器需要實現的接口方法;
- 適配器類:為目標類實現功能匹配的類;
- 適配者接口類:目標類,原始還未適配的類的接口;
- 適配者類:目標類的實現類;
適配器模式是利用一個適配器類來將接口無法適配的類對接起來。常用組件是一個適配器類和一個適配器接口類外加一個適配者類,適配者類上面可以有個是適配者類接口,要做的就是外部通過適配器就能夠直接調用適配者的方法,具體實現是適配器類實現適配接口類的方法,適配器類接口種定義了外部需要的接口,適配器類可以繼承適配者,這樣就能從其身上繼承他的方法,另一種方式是組合適配者對象引用。總的來說,適配器模式的關鍵在于實現他的適配器接口類,繼承適配者類。他的UML類圖如下所示:
3) 代碼實現
4) 總結
優勢:1、可以通過一個適配器適配多個適配者,這樣可以彌補前期設計沒有考慮周全的問題;?
劣勢: 加了中間一層適配器類轉發,導致代碼關系變復雜了;
一文徹底弄懂適配器模式(Adapter Pattern):https://segmentfault.com/a/1190000040524953
6.裝飾者模式(decorator)
1) 概述
解決問題:在不改變目標類的前提下拓展功能。
解決方案:通過將目標類包裝在修飾器內,在能夠實現目標類的功能同時,修飾器類也能拓展其他功能;
應用場景:裝飾者設計模式在Java IO中運用的十分普遍,為什么在IO中引用普遍呢?在于IO為了兼顧不同應用場景和需求有太多不同類型的IO流了,所以利用修飾者模式的話,只需要在基礎IO流上進行功能補充就好了;
2) 具體說明
主要組件:
構建接口:目標類的接口類,定義目標類需要實現的功能;
具體構建類:構建接口的具體實現;
修飾器接口:持有對構建類的引用,并實現了構建接口的方法,能夠統一對外提供目標類的方法;
具體修飾器類:實現了修飾器接口,作為修飾器具體實現類還會新增其他功能;
修飾者模式又稱為包裝器模式,出現的原因在于有在基礎類上進行增加功能的需求,如果直接修改當然違背了對修改閉合的原則,所以就想出了在原本的類上新增特性的修飾者類。實現該種模式的邏輯是:原始的被目標類實現對應的接口,修飾者也實現這個接口,當然這不是真正的修飾者類,其真正的修飾者類繼承了修飾者接口,將其補充功能在真正的修飾者類中實現。
3) 代碼實現
4) 總結
優勢:1、修飾者是類似于繼承的一種增強其他類的一種方式,但是他更靈活,要增加什么功能可以自己增加;2、通過配置和運行時動態生成的方式,可以讓他變得更靈活;
劣勢:變復雜了;
7.門面模式(facade)
1) 概述
解決問題:訪問系統中存在多個子系統,訪問起來非常復雜;解決方案:為客戶端提供一個統一的門面類型,之后只需要訪問門面即可訪問子系統中方法;使用場景:Spring的日志框架中用到了,slf4j統一了豐富多樣的日志實現;
2) 具體說明
主要角色:
門面類:通過門面統一向外界提供服務;
子系統接口:規定子系統需要實現的邏輯;
子系統類:實現子系統接口,實現各自的業務邏輯;
門面模式又稱外觀模式,用以解決在多個外部系統要和多個內部系統進行通信,可能存在非常多的聯系,提升系統交互的復雜度,通過引入門面統一負責和外部系統的通信,減少系統的復雜程度;在門面模式中存在門面和子系統兩個角色。
3) 代碼實現
4) 總結
優點:1.化繁為簡,將對多個不同的子系統訪問轉換為會單個門面的訪問;缺點:1.違反了開閉原則,組件間相互依賴,修改子系統會涉及修改門面 ;
8.代理模式(proxy)
1) 概述
解決問題:通過代理類來實現對委托類的操作,可以出現在委托類無法獲得或者不便獲得,也可以用以增強對委托類的操作;
解決方案:JDK和Spring源碼中有很多實現;
2) 具體說明
主要角色:
委托者接口:定義目標類需要實現的方法;
委托者實現:具體實現委托類中的方法;
代理者:代理類中有委托類的引用,在代理類中也需要實現委托類的方法,所以可以執行委托類的本身的方法外,除此之外還能夠在該方法前后進行增加;
代理模式有3個組件,包括:委托者接口、委托者還有代理者,他們的關系是:定義一個委托者接口,委托者和代理者都要實現他的方法,在代理類中還需要創建委托者的對象引用,這樣在對委托者類方法進行重寫時候可以直接調用。動態代理的UML類圖:
代理模式經常用在(1)委托類不想被直接調用,可以通過調用他的代理類來實現,像房產中介一樣;(2)還有一種情況是可以增強委托類的能力,像Java中的動態代理,這中在spring aop中應用廣泛;
3) 代碼實現
4) 總結
優勢:降低調用者和被調用者的耦合關系,怎么說呢?就是被調用者想要拓展新功能,可以在代理類中實踐,不需要在自身類中進行修改,符合設計模式的開閉原則;
劣勢:1對于JDK的動態代理,他需要委托類有接口,通過CgLib來動態代理,可以不需要;
行為型模式
9.責任鏈模式(chain of responsibility)
1) 概述
解決問題:一條處理鏈中的每個角色都能夠處理請求,避免請求方和相應方的耦合關系,沿著這條處理鏈處理請求,直到最后處理得到結果。使用場景:Spring的Filter中使用了責任鏈,這個適合于可以進行一系列鏈式處理的場景;
2) 具體說明
對于處理流程是線性的比較合適,每個handler中包含目前節點需要處理的業務,也包括下一個節點的引用。
3) 代碼實現
4) 總結
10.觀察者模式(Observer)
1) 概述
2) 具體說明
觀察者模式又稱訂閱發布模式,所以他適合發布訂閱的場景,當目標對象有事件發生,就會通知觀察者。
3) 代碼實現
4) 總結
四、代碼實現
GitHub:https://github.com/yangnk/MyComputerScience/tree/07f4ea1e6c06437ba5fa552a288e67a1adae3cf9/src/main/java/designPattern
TODO
- 需要完善已經寫的設計模式;
- 需要補充新的還未加上的設計模式;
參考資料
類圖、時序圖怎么畫:https://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#id2
設計模式gitbook上的一本書,講的很好:https://gof.quanke.name
菜鳥教程上的教程:http://www.runoob.com/design-pattern/design-pattern-tutorial.html
本文由博客一文多發平臺 OpenWrite 發布!