2018年9月7日 星期五

橋接模式 (Bridge)


最近拿起『深入淺出 設計模式』這本書,想看一下它對於拜訪者模式是如何說明的。結果打開目錄看時,發現這個模式被它分到第十四章-附錄。我當下好奇的看了一下,在附錄中是有那些模式,自己是不是知道,結果第一個看到的模式就是 橋接模式。

深入淺出的書印像中是屬於淺顯易懂系列的,但是我看它的橋接模式的說明真是讓我有「每一個字拆開來都知道意思,但合起來變成句子完全不知道再說些什麼」。

因此,這一篇就是要來說明我看了多本書後,對於橋接模式的心得。

使用時機

當透過繼承實作基底類別的方法時,有一個以上的原因會促使你產生新的類別完成此方法。這就代表有可能可以使用橋接模式來處理這個問題。

舉例

我們是一個小七家電部門,要為小七電視寫一個遙控器的類別,用來將小七電視換台。我們已知小七電視的遙控方式是紅外線的。
由以上的需求,我們很快的完成一個類別

public class RemoteControl
{
 public void SetChannel(int channel)
 {
  //...
 }
}
因為電視賣的太好了,要做一個以Wifi連線的版本。因此我們建立了一個基底抽像類別RemoteControlBase,內有一個抽像方法SetChannel,以及另外兩個類別做為紅外線遙控及Wifi遙控。

public abstract class RemoteControlBase
{
  public abstract void SetChannel(int channel);
}

public class InfraedRemoteControl : RemoteControlBase
{
  public override void SetChannel(int channel)
  {
    //..
  }
}

public class WifiRemoteControl : RemoteControlBase
{
  public override void SetChannel(int channel)
  {
   //..
  }
}
到這邊統計一下,目前有一個抽像類別及兩個實作它的類別。總共用了三個類別。目前會造成我們增加類別的原因只有通訊的方式改變這個理由。也就是若再來一個藍芽通訊的話,就再新增一個BluetoothRemoteControl的類別即可。目前為止,若世界總是想像的這麼美好,就沒有橋接模式出場的必要。接下來,我們就加一些場景,讓橋接模式可以因應而生。



由於小七電視賣太好了,本公司開始接代工,所以目前的遙控器除了要能遙控小七電視外,還要能遙控小八和小九電視。但是小八和小九電視在做換台動作時,有其他的前置作業需要處理。因此我們必須為小八和小九電視的換台方法寫不同的版本。所以,將小七、小八、小九及紅外線、Wifi一起考慮進去,我們想像會有以下 3 x 2 =6個類別,此類別分別是

SevenInfraedRemoteControl (小七 且 紅外線)

EightInfraedRemoteControl (小八 且 紅外線)

NineInfraedRemoteControl (小九 且 紅外線)

SevenWifiRemoteControl (小七 且 無線)

EightWifiRemoteControl (小八 且 無線)

NineWifiRemoteControl (小九 且 無線)



我們可以發現,若電視這樣大賣下去,你的分紅又沒有增加的話,你應該是希望這條產品線早早收掉,不然再來一個小十、小十一或是再來一個藍芽控制之類的。我們的類別都會爆增。

這邊的問題就在於我們在轉台這個方法內,由一個以上的原因會促使它改變。你有沒有覺得這段話很眼熟。沒錯,這就是前面提到的橋接模式的使用時機。

觀察一下目前的設計,轉台方法會受到通訊方式及電視廠牌兩個類型改變。因此它改變的原因會超過一個。

我們在這邊的作法是讓通訊方式或是電視廠牌其中一個帶來的改變從轉台的方法內消失。這邊說的消失,並不是讓程式碼移掉,而是讓他們做相同的事。看以下的例子,在SetChannel方法中,在呼叫Api.SetChannel(轉台)前,兩個類別做的事情是不一樣的,因為前面說到了不同的廠牌需要不同的前置處理。

public class SevenInfraedRemoteControl : RemoteControlBase
{
  public override void SetChannel(int channel)
  {
    Api.A();
    Api.B();
    Api.SetChannel(channel);
  }
}

public class EightInfraedRemoteControl : RemoteControlBase
{
  public override void SetChannel(int channel)
  {
    Api.B();
    Api.C();
    Api.SetChannel(channel);
  }
}
要把他們變成一樣的意思就是讓它們呼一樣的Code。先做一次修改

  public class SevenInfraedRemoteControl : RemoteControlBase
  {
    private void PreProcess()
    {
      Api.A();
      Api.B();
    }

    public override void SetChannel(int channel)
    {
      PreProcess();
      Api.SetChannel(channel);
    }
  }

  public class EightInfraedRemoteControl : RemoteControlBase
  {
    private void PreProcess()
    {
      Api.A();
      Api.B();
    }

    public override void SetChannel(int channel)
    {
      PreProcess();
      Api.SetChannel(channel);
    }
  }
這邊我們已經把SetChannel中,不一樣的地方,改成呼方法PrePrcoess,所以現在在SetChannel內看起來是一樣的了。不過,因為PrePrcoess的內容畢竟還是不同。我們現在要將PreProcess的方法內容從這兩個類別中移掉,這樣兩個類別就會長得一樣,長得一樣的話,我們就可以用一個類別就好。

以下新增了一個TvBrandBase的抽像類別,內有一個PreProcess的方法。我們讓剛剛的兩個類別內,都有一個TvBrandBase (mBrand)。因此我們可以將之前兩個類別內的PreProcess方法都拿掉,都改呼 TvBrandBase的PreProcess方法就好。

  public abstract class TvBrandBase
  {
    public abstract void PreProcess();
  }

  public class SevenInfraedRemoteControl : RemoteControlBase
  {
    TvBrandBase mBrand;

    public override void SetChannel(int channel)
    {
      mBrand.PreProcess();
      Api.SetChannel(channel);
    }
  }

  public class EightInfraedRemoteControl : RemoteControlBase
  {

    TvBrandBase mBrand;

    public override void SetChannel(int channel)
    {
      mBrand.PreProcess();
      Api.SetChannel(channel);
    }
  }
到這邊發現 SevenInfraedRemoteControl  及 EightInfraedRemoteControl 已經長得一樣。因此將這兩個類別再合起來重新變回一個InfraedRemoteControl類別,並針對小七和小八不同的 PrePrcoess方法改用SevenTvBrandBase 及EightTvBrandBase 兩個來放置。

  public abstract class TvBrandBase
  {
    public abstract void PreProcess();
  }

  public class InfraedRemoteControl : RemoteControlBase
  {
    TvBrandBase mBrand;

    public override void SetChannel(int channel)
    {
      mBrand.PreProcess();
      Api.SetChannel(channel);
    }
  }

  public class SevenTvBrandBase : TvBrandBase
  {
    public override void PreProcess()
    {
      Api.A();
      Api.B();
    }
  }

  public class EightTvBrandBase : TvBrandBase
  {
    public override void PreProcess()
    {
      Api.B();
      Api.C();
    }
  }

現在回過頭來看我們的設計。針對轉台這個方法,已知需要一個前置處理方法,然後再呼叫轉台。遇到有不同的前置處理方法,我們就去繼承TvBrandBase然後實作PreProcess方法。遇到有不同的通訊方法,就繼承RemoteControlBase,實作SetChannel方法。

我們透過在原本的RemoteControlBase類別內,引入另一個類別TvBrandBase來做到拆分SetChannel方法內的細節,這就是橋椄模式。

沒有留言:

張貼留言