類爆炸之Bridge模式

子路發表於2017-04-13

前言

以小說的筆法寫的設計模式系列文章,你絕對看得懂![首發於公眾號:"聊聊程式碼"]

設計模式系列·王小二需求歷險記(一)
設計模式系列·王小二需求歷險記(二)
設計模式系列·封裝、繼承、多型
設計模式系列·初探設計模式之王小二的疑問
設計模式系列·Facade模式之MVC的煩惱
設計模式系列·Adapter 模式之如何優雅的使用別人的輪子
設計模式系列·類爆炸之Bridge模式
設計模式系列·工廠方法模式之 Code Review
設計模式系列·抽象工廠模式

------華麗的分割線------

迷之微笑

經過C哥的精心指導,訊息中心終於上線!程式碼執行了半個月,穩定無bug。
王小二託著下腮,看著程式碼,一抹迷之微笑隨之閃現^_^。作為一名有追求的碼農,此時的快樂或許只有自己能懂。

訊息中心的重構

一天清晨,小二凝神聚力,手指在鍵盤間有節奏的敲擊著,一行行程式碼躍然屏上。不知不覺,老大在小二背後站了半天了...

"小二,之前訊息中心是你做的吧?"
"嗯嗯,是的。"

"好的,我們們現在正在搞服務拆分。而訊息中心又是一個通用的服務,所以我想把訊息中心拆出來,作為底層服務。"
"好啊,早應該這樣了!"

"嗯,具體傳送訊息的邏輯,這塊交給java組同學去寫。你只需要按照約定的資料格式,將資料push到佇列裡去,java那邊去消費就可以了。"
"嗯...可以,佇列用什麼實現呢?"

"關於佇列,這次需要你支援兩種方式:一種是redis、一種是mq"
"也就是說我既支援往redis佇列裡面push資料,也支援往mq裡push資料?"

"是的,就是這樣,這塊你好好設計下吧!"
"好的,放心吧老大!"

設計類圖

小二這兩天正在研究設計模式,既然接到了重構的新需求,那就好好大展一番身手吧!

不一會,小二就理出了大體的思路:

傳送訊息,分為3步:
1、不同的訊息(簡訊、微信)組裝各自的資料格式和內容;
2、訊息可以使用不同的方式(redis、mq)推送到佇列裡;
3、使用一個send()方法,先從步驟1獲取資料,再利用步驟2的方法push到相應的佇列裡。

思路清楚了,小二馬上畫出了類圖:

類爆炸之Bridge模式

小二反覆看了幾遍自己設計的類圖:
嗯,基本實現了需求。
1、訊息分為簡訊訊息和微信訊息(SmsMessage和WechatMessage)
2、相同的訊息既可以通過redis傳送,又可以通過Mq傳送。
沒毛病,great!

類爆炸

和往常一樣,比較大的設計,還是得請C哥把把關。
小二找到C哥,詳細介紹了自己的需求和設計。

"嗯...小二啊,問題是解決了,但設計看起來有點問題啊!"
"啊?有問題?請C哥指教"

"這個會引起類爆炸!"
"啥?類還會爆炸?你別逗我了"

"哈哈,不信?來,我讓你看看類怎麼爆炸的。假設需求要你新增Email訊息型別,你再設計下類圖"
"好的,C哥你等下,馬上設計出來"

不一會,王小二就設計出了新的類圖:

類爆炸之Bridge模式

"小二,紅色部分是你新增的3個類。"
"嗯嗯,是的!"

"好,在此基礎上,你再增加Mysql佇列的傳送方式"
"好的!"

小二拿著新的類圖找到了C哥:

類爆炸之Bridge模式

“小二,剛才只是讓你新增一種訊息型別和傳送方式,你看看一共增加了幾個類?”
“1.2.3..6,一共新增了6個類!”

"好,你現在一共有13個類,假設再讓你新增一種訊息型別和傳送方式,你又會新增多少個類?"
"嗯...會新增8個類,到時候就13+8=21個類了..."

“類太多了,爆炸了吧?哈哈,這就是類爆炸”
“確實是,類確實太多了!但是,怎麼解決呢?...”

Bridge模式登場

"小二啊,你還記不記得前面我給你講的四人團的三條建議?"
“嗯,記得:

 1、針對介面程式設計;
 2、優先使用物件組合,而不是類繼承;
 3、找到並封裝變化的點。複製程式碼

“對,就是這3點。你看看,你的設計就違背了上面的原則。”C哥說道。
"嗯?還真違反了???"王小二看了一會...

"哦...是的,C哥,確實是。違反了第2點,你看我類圖中使用的都是繼承,這個繼承間耦合性太高了,太龐大了!"
“是的,現在我們就用Bridge模式把他拆出來。”

"我先給你講講Bridge模式的基本定義吧!"
“好的,C哥!”

Bridge模式,也即橋樑模式,四人團的說法是:“將抽象部分與它的實現部分分離,使它們都可以獨立地變化。”

“啊?C哥,表示完全聽不懂...”
"哈哈,正常,你一下能聽懂才怪呢,這句話很容易使初學者產生誤解,我們邊實踐,邊解釋這個定義。"

“小二,你剛才不是說四人團建議:‘找到並封裝變化的點’嗎?你現在在你的設計中找到這些變化的點,並封裝起來。”
“好的,C哥,我想想...”

小二想了一會:“變化的點有2個。一個是訊息型別會變化,一個是傳送方式會變化。”
想好後,小二馬上畫了出來。

類爆炸之Bridge模式

"嗯,不錯,小二你解釋下吧"

小二解釋道:"
變化的點有2個:一個是訊息型別[Sms、Wechat...],一個是傳送方式[redis、mq...]。

所以我把他們各自都封裝了起來,成為2個獨立的抽象類:Message和SendType。

Message類負責組裝好自己訊息型別的資料(combine_data()),併傳送(send())出去。
SendType類負責將資料push(push_to_queue())進相應的佇列。"

"不錯嘛,小二,我在你類圖的基礎上擴充套件下,你就知道怎麼解決類爆炸的問題了。"
"哇塞,好的,C哥!"

不一會,C哥就在小二的基礎上,畫出了完整的類圖:

類爆炸之Bridge模式

"看不太懂,C哥你解釋下吧!"

C哥解釋道:"
小二你看,訊息有2種型別:簡訊和微信。

但不論是簡訊和微信,他們都應該知道自己的訊息格式和內容。
並且,他們得把自己傳送出去,也就是push到相應的佇列裡面去。

而如何push到佇列裡面去呢?這又有2種實現方式,一種是redis佇列,一種是mq佇列。
也就是,實現傳送這個動作,得知道如何傳送。

你看這裡,我沒有用你最初設計的類繼承的方法:
這裡的抽象部分:即是Message的抽象;
這裡的實現部分:即是SendType的實現。

在抽象部分與實現部分之間搭個橋,使抽象部分可以引用實現部分的物件,就是橋接模式。

這樣使用物件組合的方式,特別的靈活。"

"哇塞,C哥,這個橋接威力好大啊!"
"是啊,橋接模式比較難,但也更有用。你看,這樣不管你是增加一種新的訊息型別還是一種新的傳送方式,他們之間沒有耦合,可以獨立的變化。"

"是啊,這樣類爆炸的問題也就沒有了,冗餘減少了,程式碼更好維護!"
"是這樣的!"

程式碼實現

見證了bridge模式的威力之後,小二迫不及待的寫出了相應的虛擬碼:

"C哥,你幫我看下我寫的程式碼思路對嗎?"
"好的,我看看..."

<?php
//訊息抽象類
abstract class Message{
    //定義傳送方式物件與訊息資料
    public $send_type_obj;
    public $data;

    //建構函式
    public function __construct($send_type_obj,$data)
    {
        $this->send_type_obj=$send_type_obj;
        $this->data=$data;
    }

    //抽象類:不同的訊息來重寫此方法,以得到不同的訊息資料
    abstract public function combine_data();

    //橋接到外部物件(引用外部物件,push到相應的佇列)
    public function push_to_queue($data){
        if($this->send_type_obj instanceof SendType){
            $this->send_type_obj->push_to_queue($data);
        }
    }

    //完成傳送
    public function send(){
        $combined_data=$this->combine_data();
        $this->push_to_queue($combined_data);
    }
}

//簡訊訊息類
class SmsMessage extends Message {
    //傳送簡訊訊息資料
    public function combine_data(){
        return 'sms combined data:'.$this->data;
    }
}

//微信訊息類
class WechatMessage extends Message {
    //傳送微信訊息資料
    public function combine_data(){
        return 'wechat combined data:'.$this->data;
    }
}

//傳送方式抽象類
abstract class SendType{
    abstract public function push_to_queue($data);
}

//Redis傳送方式類
class RedisSendType extends SendType {
    //將訊息push到redis佇列裡,完成傳送
    public function push_to_queue($data)
    {
        echo  $data." has sent by redis queue\n";
    }
}

//Mq傳送方式類
class MqSendType extends SendType {
    //將訊息push到mq佇列裡,完成傳送
    public function push_to_queue($data)
    {
        echo  $data." has sent by mq queue\n";
    }
}

/************Test Case*************/

//例項化不同的傳送方式類
$redis_send_obj=new RedisSendType();
$mq_send_obj= new MqSendType();

//通過redis傳送簡訊
$sms_redis_obj=new SmsMessage($redis_send_obj,'123');
$sms_redis_obj->send();

//通過redis傳送微信
$wechat_redis_obj=new WechatMessage($redis_send_obj,'456');
$wechat_redis_obj->send();

//通過mq傳送簡訊
$sms_mq_obj=new SmsMessage($mq_send_obj,'789');
$sms_mq_obj->send();

//通過mq傳送微信
$wechat_mq_obj=new WechatMessage($mq_send_obj,'100');
$wechat_mq_obj->send();複製程式碼

"嗯,看起來沒毛病,我看看你的執行結果。"
"好的,C哥,這是執行結果"

類爆炸之Bridge模式

"哈哈,確實沒問題,不錯嘛小二!"
"C哥指點的好,謝謝C哥,又學習了一種強大的設計模式!"

結語

設計模式如此強大,從bridge就可見其不一般。
那到底什麼是設計模式呢?有沒有一個通俗的定義呢?

其實,通俗點說:

設計模式,是針對特定問題的,反覆出現的解決方案,這種方案被抽象化、模板化。並且隨著時間的流逝,被歷史證明這是優秀的解決方案。

所以,跟著王小二一起好好的學習設計模式吧,相信你終將邁入"左手程式碼右手詩"的天地!^_^

轉載宣告:本文轉載自「聊聊程式碼」,搜尋「talkpoem」即可關注。

關注「聊聊程式碼」,讓我們一起聊聊“左手程式碼右手詩”的事兒。

類爆炸之Bridge模式

設計模式系列·王小二需求歷險記(一)
設計模式系列·王小二需求歷險記(二)
設計模式系列·封裝、繼承、多型
設計模式系列·初探設計模式之王小二的疑問
設計模式系列·Facade模式之MVC的煩惱
設計模式系列·類爆炸之Bridge模式

相關文章