一文理清21種設計模式:用實例分析和對比

設計模式不管是對於最底層的的編碼實現仍是較高層的架構設計都有着重要的指導做用。所謂光說不練假把式,今天我就把項目中常見的應用場景涉及到的主要設計模式及其相關設計模式總結一下,用實例分析和對比的方式在一片文章中就把最多見的21種設計模式梳理清楚。html

Redis發佈訂閱

在項目中經常使用redis的發佈/訂閱功能,用來實現進程間通訊甚至IM等業務。
使用 jedis 實現頻道訂閱的模式通常以下:前端

try( Jedis jedis =  RedisClient.getJedis() ) {
    JedisPubSub listener = new MySubListener();
    // 訂閱
    jedis.subscribe(listener, "channel");
}

其中 MySubListenerjava

class MySubListener extends JedisPubSub {
    // 取得訂閱的消息後的處理
    public void onMessage(String channel, String message) {
        logger.info("頻道:{},收到消息:{}",channel,message);
    }
    // 初始化訂閱時候的處理
    public void onSubscribe(String channel, int subscribedChannels) {
        logger.info("訂閱:{},總數:{}",channel,subscribedChannels);
    }
    // 取消訂閱時候的處理
    public void onUnsubscribe(String channel, int subscribedChannels) {
        logger.info("取消訂閱:{},總數:{}",channel,subscribedChannels);
    }
}

這裏使用了策略模式對算法的封裝,把使用算法的責任和算法自己分隔開,委派給不一樣的對象管理。策略模式一般把一系列的算法包裝到一系列的策略類裏面,做爲抽象策略類的子類)。
圖:
策略模式git

本例中,JedisPubSub抽象策略類,定義不一樣事件發生時的響應模式亦即所支持的算法的公共接口;MySubListener是一個具體策略類定義了一種具體的事件響應方式(簡單的打印);jedis是就是Context,負責維護調用者與策略之間的聯繫。這樣不一樣的調用者只須要傳入不一樣的事件響應具體算法如MySubListener一、二、3等便可(而不是去修改已有算法),實現了對擴展開放,對修改關閉的開閉原則
jedis 發佈事件的代碼以下:github

try {
    jedis.publish("channel","message to be published");
}

說到這就不得不說說狀態模式當一個對象內在狀態改變時容許其改變行爲, 這個對象看起來像改變了其類
圖:
狀態模式redis

Context定義客戶端須要的接口, 而且負責具體狀態的切換
State接口或抽象類,負責對象狀態定義,而且封裝Context以實現狀態切換;
ConcreteState每個具體狀態必須完成兩個職責:就是本狀態下要作的事情,以及本狀態如何過渡到其餘狀態
狀態模式和策略模式都是爲具備多種可能情形設計的模式,把不一樣的處理情形抽象爲一個相同的接口,符合開閉原則。可是狀態模式將各個狀態對應的操做分離開來,即不一樣的狀態由不一樣的子類實現具體操做,狀態切換由子類實現,當發現傳入參數不是本身這個狀態所對應的參數,則本身給Context類切換狀態,也就是說客戶端並不知曉狀態;而策略模式是直接依賴注入到Context類的參數進行選擇策略,不存在切換狀態的操做,也就是說狀態和策略是由客戶端本身定的算法

回到本例,發佈/訂閱自己就是觀察者模式定義對象間一種一對多的依賴關係,使得每當一個對象改變狀態,則全部依賴於它的對象都會獲得通知並被自動更新)的運用。
圖:
觀察者模式數據庫

能夠結合Redis設計與實現查看redis實現發佈訂閱的原理。本例中,JedisPubSub抽象觀察者MySubListener具體觀察者抽象主題沒有顯式定義,可是咱們知道它的標準就是可以添加、刪除、通知觀察者(如調用onMessage方法),具體主題就是redis裏面包含"channel"這個模式的頻道。這就把消息生產者和消費者解耦了,消費者不用管生產者如何產生消息,生產者不用管消費者如何處理消息,二者直接是鬆耦合的,也就是說二者僅依賴於通知機制進行交互而不知道對方的實現細節,這樣只要保持通知機制,雙方均可以隨意擴展。編程

請注意上面代碼的Jedis jedis = RedisClient.getJedis()是一個靜態工廠方法模式或者說簡單工廠模式經過專門定義一個類使用靜態方法來負責建立其餘類的實例,被建立的實例一般都具備共同的父類或者父接口)的使用。
圖:
靜態工廠方法模式bootstrap

RedisClient主要代碼以下:

public final class RedisClient {
    
    private static JedisPool jedisPool;

    public static void construct(Properties p){
        try {
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxTotal(Integer.parseInt(p.getProperty("jedis.pool.maxTotal")));
            jedisPool = new JedisPool(config,p.getProperty("redis.host"), Integer.parseInt(p.getProperty("redis.port")),
                    Integer.parseInt(p.getProperty("redis.timeOut")),p.getProperty("redis.auth"), Integer.parseInt(p.getProperty("redis.db")));
        }
    }
    public static Jedis getJedis(){
        return jedisPool.getResource();
    }
    public static void destruct(){
        jedisPool.close();
    }
}

本例中類RedisClient就是Creator,返回的redis客戶端Jedis就是ConcreateProduct,因爲目前只用了 jedis 這一種 java redis client 因此沒有設置抽象的Product,若是有多種client那麼就要設置抽象的Product(這些Product都要有set、hset等redis通用操做),而後再在getJedis函數中去根據須要產生不一樣的client(if else 或者 switch case)。
靜態工廠方法的好處在於:增長新的Product類(好比新的java redis client)的時候老的類不須要改變,調用者因爲只依賴於接口(抽象的Product)也不用改變,亦即把變化封裝到工廠內部了;可讀性更強(好比getJedis你就知道他要幹啥,而不是使用不知因此的構造函數);緩存加強性能(好比上面的jedisPool就一直存在着,避免每次獲取鏈接時新建立鏈接池);代碼簡潔等等。

靜態工廠方法模式的缺點就是新加入一個Product類的時候,其工廠方法自己須要改變(好比多一個判斷的case分支),解決辦法就是採用每種具體Product對應一個具體工廠的工廠模式定義一個用於建立對象的接口, 讓子類決定實例化哪個類。 工廠方法使一個類的實例化延遲到其子類
圖:
工廠模式

工廠模式把每一種 product 類的實例化過程都封裝到了一個對應的工廠類中,新加入product的時候不須要改任何的舊代碼,只須要同時添加對應的具體工廠類便可。高層模塊只須要知道產品的抽象類,其餘的具體實現類都不須要關心,符合迪米特法則,依賴倒置原則,里氏替換原則

而後就不得不說說抽象層次更高、更具通常性的抽象工廠模式爲建立一組相關或相互依賴的對象提供一個接口, 並且無須指定它們的具體類)了
圖:
抽象工廠模式

抽象工廠與工廠方法的區別就是可能有多個抽象Product,也就是說每個具體工廠可以生產一個產品族而不僅是一個產品,能夠把抽象工廠簡單理解爲工廠方法+簡單工廠,每個具體工廠都是一個簡單工廠。

說到工廠模式就不得不說原型模式用原型實例指定建立對象的種類, 而且經過拷貝這些原型建立新的對象
圖:
原型模式

原型模式的核心是Override Object 類的 clone 方法,經過該方法進行對象的拷貝,因爲是內存二進制流的拷貝,因此比直接new性能好不少,特別是要在一個循環體內產生大量的對象時,原型模式能夠更好地體現其優勢。在實際項目中,原型模式不多單獨出現,通常是和工廠方法模式一塊兒出現,經過clone的方法建立一個對象, 而後由工廠方法提供給調用者。

繼續觀察上面的RedisClient類,咱們知道,鏈接池jedisPool在整個應用中是隻須要一個實例的,也就是說咱們要使用單例模式確保某一個類只有一個實例, 並且自行實例化並向整個系統提供這個實例
圖:
單例模式

因此咱們的代碼要修改一下:

public final class RedisClient {
    
    private static volatile JedisPool jedisPool;

    public static void construct(Properties p){
        try {
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxTotal(Integer.parseInt(p.getProperty("jedis.pool.maxTotal")));
            if(jedisPool==null){
                synchronized (RedisClient.class){
                    if (jedisPool==null){
                        jedisPool = new JedisPool(config,p.getProperty("redis.host"), Integer.parseInt(p.getProperty("redis.port")),
                            Integer.parseInt(p.getProperty("redis.timeOut")),p.getProperty("redis.auth"), Integer.parseInt(p.getProperty("redis.db")));
                    }
                }
            }
        }
    }     
}

這裏用volatile+雙重檢查來實現單例模式,和標準的單例模式區別是,本例並不須要返回jedisPool實例,而是返回了一個jedis鏈接。

上面的JedisPool用到了享元模式使用共享對象來有效地支持大量的細粒度的對象
圖:
享元模式

JedisPool就是FlyweightFactoryjedis 就是 ConcreteFlyweight抽象的Flyweight在本例沒有設置,可是咱們知道它確定是封裝了常見的redis操做接口的,UNsharedConcreteFactory也沒有對應設置,由於jedis對客戶端都是同樣的,因此全部部分都不是不可分享的。經過池操做,使得固定數量N(甚至更少)的jedis對象能夠服務於遠超N個的客戶端對象,達到共享和複用的目的。

Netty的應用

咱們啓動Netty服務器的時候,服務端使用ServerBootstrap,通常而言代碼以下:

NioEventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(group) 
    .channel(NioServerSocketChannel.class) 
    .localAddress(new InetSocketAddress(port)) 
    .childHandler(new ChannelInitializer<SocketChannel>() { 
        @Override
        public void initChannel(SocketChannel ch)
            throws Exception {
            ch.pipeline().addLast(
                new EchoServerHandler());
        }
});
ChannelFuture f = b.bind().sync();

這裏使用了建造者模式將一個複雜對象的構建與它的表示分離, 使得一樣的構建過程能夠建立不一樣的表示
圖:
建造者模式

本例中ServerBootstrap具體建造者,其繼承的AbstractBootstrap抽象建造者,返回的ProductChannelFuture,咱們的調用代碼是Director。經過建造者模式,咱們在構建複雜對象的時候沒必要一次性肯定所有參數(碩大的構造函數),而是根據須要一步一步構建一個完整的對象(也比一個一個調用setter的方式節省代碼並且美觀),因此每個構造過程函數都要返回一個完整的對象this。說到這就不得不說一說裝飾器模式動態地給一個對象添加一些額外的職責。就增長功能來講, 裝飾模式相比生成子類更爲靈活
圖:
裝飾器模式

裝飾器模式的ConcreateDecorator能夠在不改變ConcreateComponent給其添加一些新功能或者新特性,就像建造者模式,每一步建造過程都在給自身添加新功能或者新特性,也就是說若是看作裝飾器模式,那麼ConcreateDecoratorConcreateComponent都是Builder自身,並且添加過程和獲得的結果都相對穩定,因此建造者模式是一種特殊的裝飾器模式。裝飾器模式在java的IO類中應用普遍。
與裝飾器模式很是類似的模式有適配器模式將一個類的接口變換成客戶端所期待的另外一種接口, 從而使本來因接口不匹配而沒法在一塊兒工做的兩個類可以在一塊兒工做
圖:
適配器模式

代理模式爲其餘對象提供一種代理以控制對這個對象的訪問
圖:
代理模式

外觀模式要求一個子系統的外部與其內部的通訊必須經過一個統一的對象進行外觀模式提供一個高層次的接口,使得子系統更易於使用
圖:
外觀模式

這4個模式都是將本來的對象進行包裝轉換實現另外一些功能,不一樣的是:

  • 裝飾器模式關注於在一個對象上動態的添加方法,增長新的行爲,實現新功能
  • 適配器模式關注於將一個類的接口轉換成客戶但願的另一個不一樣的接口,使得本來接口不兼容而不能一塊兒工做的那些類能夠兼容
  • 代理模式關注於爲其餘對象提供一種代理以實現對這個對象的訪問控制,代理與被代理對象實現相同的接口
  • 外觀模式關注於爲子系統中的一組接口提供一個一致的界面,此模式簡化接口,使得子系統更加容易使用

netty處理與客戶端之間的消息往來使用的ChannelPipelineChannelHandler模型是一個典型的命令模式將一個請求封裝成一個對象,從而讓你使用不一樣的請求把客戶端參數化,對請求、排隊或者記錄請求日誌,還提供命令的撤銷和恢復功能)的使用
圖:
命令模式

在java中,經常將ConcreteCommandReceiver合併爲一個對象,這樣每一個命令都完成一個職責,而不是根據接收者的不一樣完成不一樣的職責,client調用時就不用考慮接收者是誰。模式以下:

//非儉省模式定義 接收者 和 命令
Receiver receiver = new ConcreteReciver1(); Command command = new ConcreteCommand1(receiver);
//儉省模式 只定義一個發送給接收者的具體命令
Command command = new ConcreteCommand1();
//首先聲明調用者Invoker
Invoker invoker = new Invoker();
//把命令交給調用者
invoker.setCommand(command);
//執行
invoker.action();

java Runable就是一個絕佳的示例:

//定義一個具體的命令賦值給抽象的命令引用,裏面的run能夠理解爲receiver
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println(2);
    }
};
//聲明調用者Invoker並把命令交給調用者
Thread thread = new Thread(runnable);
//執行
thread.start();

netty中一個ChannelInboundHandler收到消息後調用ChannelHandlerContext(繼承了ChannelInboundInvoker)的fireChannelRead去調用下一個ChannelInboundHandlerchannelRead方法。
實際作法中,ChannelInboundInvoker是抽象的Invoker,而AbstractChannelHandlerContext纔是真正的具體Invoker,其 static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) 方法(也就是執行方法)調用了下一個(next)ChannelInboundHandler(也就是receiver)的channelRead方法(也就是具體命令

ChannelPipelineChannelHandler模型也是一個標準的責任鏈模式使多個對象都有機會處理請求,從而避免了請求的發送者和接受者之間的耦合關係。將這些對象連成一條鏈,並沿着這條鏈傳遞該請求,直到有對象處理它爲止
圖:
責任鏈模式

Handler就是netty中的ChannelHandler接口,消息處理的每個ConcreteHandler(通常由咱們本身實現)都會去調用下一個ConcreteHandler。

ChannelPipelineChannelHandler模型實際上仍是一個非典型的模板方法模式定義一個操做中的算法的框架, 而將一些步驟延遲到子類中,使得子類能夠不改變一個算法的結構便可重定義該算法的某些特定步驟
圖:
模板方法模式

也就是說,netty規定了處理客戶端的鏈接的算法是先用一些列抽象的ChannelInboundHandler處理(好比解碼、解密),而後再由一系列抽象的ChannelOutboundHandler處理(好比編碼、加密),可是具體的Handler實現是咱們本身加入的,如上面代碼改一下:

ch.pipeline().addLast(new DecodeHandler());
ch.pipeline().addLast(new EncodeHandler());
ch.pipeline().addLast(new BusinessHandler());

說他非典型主要是模板方法模式的算法的框架是肯定的(好比肯定了要解碼、存儲、編碼三個步驟),不肯定的只是細節,可是在netty中不只細節,算法框架自己咱們均可以本身修改(能夠加入不少的Handler)。

其餘

橋接模式抽象和實現解耦,使得二者能夠獨立地變化
圖:
橋接模式

Abstraction的主要職責是定義出該角色的行爲,同時保存一個對Implementor的引用,該角色通常是抽象類;
Implementor是接口或者抽象類,定義角色必需的行爲和屬性;
RefinedAbstraction引用Implementor對Abstraction進行修正;
ConcreteImplementor實現接口或抽象類定義的方法和屬性。
所謂將抽象和實現解耦就是說抽象與實現不是直接經過繼承來強耦合,而是經過對象組合構成的一座橋來實現弱耦合。
最經典的橋接模式就是JDBC,JDBC爲全部的數據庫提供通用的接口(Abstraction), 一個應用程序能夠根據須要選擇的驅動程序(Implementor), 經過具體的驅動程序(ConcreteImplementor)向的數據庫發起請求. 這個過程就是Abstraction把行爲委託給Implementor的過程,這樣一來應用程序和具體的驅動程序均可以獨立變化

中介模式用一箇中介對象封裝一系列的對象交互,中介者使各對象不須要顯示地相互做用,從而使其耦合鬆散,並且能夠獨立地改變它們之間的交互
圖:
中介模式

Mediator 定義統一的接口,用於各Colleague之間的通訊;
ConcreteMediator 經過協調各Colleague實現協做行爲,所以它必須依賴於各個Colleague;
Colleague 都知道Mediator,並且與其餘的Colleague的時候,都經過Mediator協做。
中介者模式的優勢就是減小類間的依賴,把原有的一對多的依賴變成了一對一的依賴,Colleague只依賴Mediator,下降了類間的耦合。
最經典的中介模式是MVC框架的運用,其中的C就是一箇中介者,把M和V隔離開,協調M和V協同工做,把M運行的結果和V表明的視圖融合成一個前端能夠展現的頁面,減小M和V的依賴關係

備忘錄模式在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象以外保存這個狀態,這樣之後就可將該對象恢復到原先保存的態
圖:
備忘錄模式

Originator 記錄當前時刻的內部狀態,負責定義哪些屬於備份範圍的狀態,負責建立和恢復備忘錄數據;
Memento 負責存儲Originator發起人對象的內部狀態,在須要的時候提供發起人須要的內部狀態;
Caretaker 對備忘錄進行管理、保存和提供備忘錄。
最經典的備忘錄模式就是jdbc的事務功能,由於要提供回滾,因此必然要用備忘錄模式。

訪問者模式封裝一些做用於某種數據結構中的各元素的操做, 它能夠在不改變數據結構的前提下定義做用於這些元素的新的操做
圖:
訪問者模式

Visitor 是抽象類或者接口,聲明訪問者能夠訪問哪些元素,具體到程序中就是visit方法的參數定義哪些對象是能夠被訪問的;
ConcreteVisitor 影響訪問者訪問到一個類後該怎麼幹,要作什麼事情;
Element 接口或者抽象類,聲明接受哪一類訪問者訪問,程序上是經過accept方法中的參數來定義的;
ConcreteElement 實現accept方法,一般是visitor.visit(this),基本上都造成了一種模式了;
ObjectStruture Element產生者,通常容納在多個不一樣類、不一樣接口的容器,如List、 Set、 Map等,在項目中,通常不多抽象出這個角色。
訪問者模式能夠將數據的構成與使用方法解耦,擴展性很好。

省略的設計模式

組合模式說白了就是個樹形結構;
迭代器模式基本沒有人會本身實現了;
解釋器模式使用的不多;

附錄——六大設計模式原則

全部的設計模式無非都是這幾個原則的體現(固然有些會違背),這些原則指導着咱們寫出更健壯、穩定、易維護的程序。

  • 單一職責原則:應該有且僅有一個緣由引發類的變動,可是這「一個緣由」怎麼定義須要咱們根據業務本身拿捏
  • 里氏替換原則:全部引用基類的地方必須能透明地使用其子類的對象,記住要確實有is-a的關係才用繼承,不然就使用依賴、彙集、組合的方式
  • 依賴倒置原則:高層模塊不該該依賴低層模塊(原子邏輯), 二者都應該依賴其抽象,抽象(接口)不該該依賴細節(實現類),細節應該依賴抽象,更加精簡的說法就是「面向接口編程」
  • 接口隔離原則:類間的依賴關係應該創建在最小的接口上,也就是說接口儘可能細化
  • 迪米特法則:也稱最少知識原則,一個對象應該對其餘對象有最少的瞭解,知道的越多耦合就越高就越不容易修改
  • 開閉原則:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉,就是說咱們的功能變化要經過擴展來實現而不是經過修改已有代碼實現,這樣系統穩定性才更高,也更靈活

感謝設計模式之禪HeadFirst設計模式,這兩本書隨便選一本看完均可以。
閱讀原文,做者:MageekChiu

相關文章
相關標籤/搜索