Design Patterns @ Python -- Bridge 橋接模式筆記

Bridge Pattern - 橋接模式

前言:

什麼是橋接模式:

  • 舉例來說,在一個電商平台會販賣書、電子產品、衣服、食品等等,在將需要購買放入購物車之後,最後我們要付款完成交易,而付款方式,可以是信用卡支付、APP支付(例如:支付寶、微信支付、Apple Pay、Line Pay等)、線上支付平台、現金支付等結帳方式。如以傳統的方式來寫這樣的線上電商結帳程式,可能的情形,是每一產品類底下都有各種付款方式的 method / function implement,如下圖所示:
    傳統程式架構
  • 這樣的方式有兩個缺點:
    • 當產品越多,付款方式越多,對於程式開發者就會遇到 Cartesian product complexity explosion 卡爾迪爆炸問題(當兩類物件中有許多子物件,而這兩類物件要形成組合時,子類物件越多,會造成組合項目等比級數增加,也會造成重複的程式碼大量增加,而讓程式變得冗餘)
    • 此外每當要新增某一個項目時,以剛所提的電商案例來說,比方說要增加一個新的支付方式,就得把所有產品項目都挖出來,然後在每個產品項目都去實作出這個新支付形式的 method / function 出來,這嚴重違反了 SOLID 的 OCP 原則(對擴展開放,對修改封閉),這容易讓本來已經正常運作的這些產品類別因為進行新支付方式實作而便得不穩定!
  • 為了解決這樣問題,於是需要搬出橋接模式來解決~
    • 橋接模式的目的是將實作抽象解除耦合,使得兩邊都得以獨立的變化,那實作上是如何讓兩邊來解除耦合呢?
      • 以剛剛的電商支付案例來說,這些販賣的書、電子產品、衣服等都是產品抽象後的具體 – 抽象的具體實作(首先將產品建構成一個抽象類別物件,讓所有具體的產品來繼承這個抽象產品類別,從而形成一個具體實作後的物件類別)
      • 而支付方式則看做是一個介面物件,主要是作為一個抽象實作介面當作參數來傳入產品的抽象類別中~
        • 如此一來,所有各種的支付方式,都可透過繼承這個支付介面的抽象物件來實作支付的 function / method
      • 這樣每當一個產品要增加新的支付方式時,只需要 將此新的支付方式繼承支付介面物件 ,就能透過參數傳遞,傳入商品物件中,使剛商品具有這個新的支付方法
  • 以上的橋接模式說明看似複雜,但具體的架構其實只需要透過以下流程圖的圖示說明,就能更容易的消化整個觀念:

範例說明:

  • 接著讓我們來看橋接模式的範例,會更加清楚應用的方式:
範例1. Shape Rander Example
# Circles and squares
# Each can be rendered in vector or raster form

class Renderer():
    ''' 定義一個 Renderer 的 interface,並且定義一個 render_circle 的 common method 來實作 '''
    def render_circle(self, radius):
        pass


class VectorRenderer(Renderer):
    ''' 定義一個 VectorRenderer 的 class,並且實作 render_circle 的 method '''
    def render_circle(self, radius):
        print(f'Drawing a circle of radius {radius}')


class RasterRenderer(Renderer):
    ''' 定義一個 RasterRenderer 的 class,並且實作 render_circle 的 method '''
    def render_circle(self, radius):
        print(f'Drawing pixels for circle of radius {radius}')


class Shape:
    ''' 定義一個 Shape 的 父類別物件,並且將 renderer 物件當作為參數傳入 
        並且定義 draw 與 resize 的 abstract method 讓後面繼承的子類別物件
        來完成實作~
    '''
    def __init__(self, renderer):
        self.renderer = renderer

    def draw(self): pass
    def resize(self, factor): pass


class Circle(Shape):
    ''' 定義一個 Circle 的 子類別物件,其繼承自 Shape 父類別, 因此 renderer 物件被當作參數傳入,
        並於 draw 中使用 renderer 物件的 render_circle method 來完成實作
    '''
    def __init__(self, renderer, radius):
        super().__init__(renderer)
        self.radius = radius

    def draw(self):
        self.renderer.render_circle(self.radius)

    def resize(self, factor):
        self.radius *= factor


if __name__ == '__main__':
    raster = RasterRenderer()
    vector = VectorRenderer()
    circle = Circle(vector, 5)
    circle.draw()
    circle.resize(2)
    circle.draw()
# Execute Result:

Drawing a circle of radius 5
Drawing a circle of radius 10
範例2. Online Shopping Example
(更新範例加入了組合模式創建的購物車類別)
  • 另一個範例是剛剛上面一開始提到的電商平台消費的範例:
  • 基於讀會上 Sky 兄拋出的購物車問題,原先範例只是單純用一個購物行為來解釋橋接模式,以下的更新範例則是 加入了使用組合模式建構購物車的解題方法實作和結果。
  • 而要實現購物車的實作,其實只要簡單使用下一節的組合模式 (Composite Pattern) 就能解決,組合模式可以看作是一個橋接模式的進階應用,就是原本的橋接模式再加上迭代的手法來解決問題。組合模式很適用於類似物件集合的行為模式,特別是有樹狀架構的物件集合。
# 另一個範例是電商平台的範例
from abc import ABC, abstractmethod

class Payment(ABC):
    '''定義一個付費方式的介面(抽象類別)'''
    @abstractmethod
    def processPayment(self):
        pass

class CreditCardPayment(Payment):
    '''信用卡支付-->繼承 Payment Interface '''
    def processPayment(self):
        print("Processing credit card payment")

class WeChatPayment(Payment):
    ''' WeChat支付-->繼承 Payment Interface '''
    def processPayment(self):
        print("Processing wechat payment")
        
class CashPayment(Payment):
    ''' Cash 現金支付-->繼承 Payment Interface '''
    def processPayment(self):
        print("Processing cash payment")        

class Product(ABC):
    '''定義一個產品的抽象類別,並且於初始化中傳入付費方式的物件,
         並且定義一個 purchase 的抽象方法,讓後面繼承的子類別物件來完成實作
    '''
    def __init__(self, payment: Payment):
        self.payment = payment

    @abstractmethod
    def purchase(self):
        pass


class ShoppingCart():
    '''使用組合模式創建購物車類別'''
    def __init__(self):
        self.products = []

    def __iter__(self):
        yield self

    def addProduct(self, product):
        '''加入產品到購物車,並且判斷加入的東西是否為產品或購物車才能加入'''
        if isinstance(product, ShoppingCart | Product):
            self.products.append(product)
        else:
            errMsg = "Input argument error: The shopping cart only for product or another shopping cart"
            raise Exception(errMsg)

    def purchase(self):
        '''付款結帳'''
        for product in self.products:
               #  如果是產品就直接付款
            if isinstance(product, Product):
                product.purchase()
            else:
               #  如果是購物車就遞迴呼叫購物車中的產品付款
                product.products.purchase()


class Book(Product):
    '''書籍類別-->繼承 Product 抽象類別 '''
    def __init__(self, payment: Payment):
        super().__init__(payment)

    def purchase(self):
        # 透過傳入的付費方式物件來完成付款的動作
        self.payment.processPayment()
        print("Purchased book")

class Electronics(Product):
    ''' 電子產品類別-->繼承 Product 抽象類別 '''
    def __init__(self, payment: Payment):
        super().__init__(payment)

    def purchase(self):
        # 透過傳入的付費方式物件來完成付款的動作
        self.payment.processPayment()
        print("Purchased electronics")

if __name__ == "__main__":
    print("1  Bridge pattern demo:")
    creditCardPayment = CreditCardPayment()
    wechatPayment = WeChatPayment()
    book = Book(creditCardPayment)
    book.purchase()
    electronics = Electronics(creditCardPayment)
    electronics.purchase()
    book2 = Book(wechatPayment)
    book2.purchase()
    cashPayment = CashPayment()
    book3 = Book(cashPayment)
    book3.purchase()
    electronics2 = Electronics(cashPayment)
    electronics2.purchase()
    print("\n2. Composite pattern demo:")
    shoppingCart = ShoppingCart()
    shoppingCart.addProduct(book)
    shoppingCart.addProduct(electronics)
    shoppingCart.addProduct(book2)
    shoppingCart.addProduct(book3)
    shoppingCart.addProduct(electronics2)
    shoppingCart.purchase()
# Execute Result:

1  Bridge pattern demo:
Processing credit card payment
Purchased book
Processing credit card payment
Purchased electronics
Processing wechat payment
Purchased book
Processing cash payment
Purchased book
Processing cash payment
Purchased electronics

2. Composite pattern demo:
Processing credit card payment
Purchased book
Processing credit card payment
Purchased electronics
Processing wechat payment
Purchased book
Processing cash payment
Purchased book
Processing cash payment
Purchased electronics

總結 – 橋接模式:

  • 特點:
    • 藉由抽象類別創建介面,並藉由介面功能實作物件類別解耦抽離
    • 隱藏(介面)實現細節
  • 優點:
    • 由於功能實作物件類別解耦,因此當需要增加實作功能時,不須每每進入到物件類別修改,這樣既不違反 SOLID 的 OCP 原則,也讓物件類別能夠穩定(越多的修改,會使得物件變得越不穩定)
    • 減少重複代碼,增加代碼的複用性
    • 解決 Cartesian product complexity explosion 卡爾迪爆炸 問題,讓 程式架構擴展性變靈活
  • 缺點:
    • 程式基於抽象繼承,讓程式迭代層次多,程式因此趨於複雜,也略為降低了程式的可讀性
    • 必須很清楚物件抽象的解構處理,否則 容易造成不必要的抽離,而增加程式的冗餘
2個讚

昨天 Sky 兄在讀書會上拋出了購物車問題,其實原圖靈星球論壇的作者只是單純為了想簡單解釋橋接模式才寫了原先的單純購物付款的範例來輔助說明,然而當然如果以實際購物行為,這樣做太沒效率了,更好的做法應該是用進階版的橋接模式–組合模式來實作購物車,這樣不論是直接點選購賣產品放入購物車付款結帳,或是多個購物車合併付款結帳,都會更有效率些。

說穿了,其實學到這裡,我慢慢有體會到設計模式的精隨,不是在於你要死板板的一定去硬性套用哪種模式來解題,共學的 Udemy 這門設計模式課程,老師的範例看似散亂複雜,但信手捻來的那種手感,是看得出來的,雖然相比之下我更欣賞 Fred 老師的嚴謹的方式和程式風格。

其實這些設計模式其實都是 SOLID 原則的實現方法,目的也都只有一個,就是讓我們重構整個程式碼,讓重複的程式碼更少、更精簡、更 pythonic~

這也是為何有的說設計模式只有八種,有的說是16種,更有的說是23種或更多,因為其實幾種不是重點,重點是怎樣貫穿 SOLID 的精神來精簡我們可能散亂或凌亂的程式架構。然而如果設計模式只是 SOLID 原則的實現方法,那學習設計模式能做什麼,學習設計模式能讓我們更熟悉哪些情境,可能可以使用哪些方式來精簡我們的程式架構,讓程式更具結構性,有感而發,說得多了些,感覺課程也走了一半,與大家共學夥伴共勉,繼續加油~

1個讚