寫給Python初學者的設計模式入門

jobbole發表於2014-03-13

有沒有想過設計模式到底是什麼?通過本文可以看到設計模式為什麼這麼重要,通過幾個Python的示例展示為什麼需要設計模式,以及如何使用。

 

設計模式是什麼?

設計模式是經過總結、優化的,對我們經常會碰到的一些程式設計問題的可重用解決方案。一個設計模式並不像一個類或一個庫那樣能夠直接作用於我們的程式碼。反之,設計模式更為高階,它是一種必須在特定情形下實現的一種方法模板。設計模式不會繫結具體的程式語言。一個好的設計模式應該能夠用大部分程式語言實現(如果做不到全部的話,具體取決於語言特性)。最為重要的是,設計模式也是一把雙刃劍,如果設計模式被用在不恰當的情形下將會造成災難,進而帶來無窮的麻煩。然而如果設計模式在正確的時間被用在正確地地方,它將是你的救星。

起初,你會認為“模式”就是為了解決一類特定問題而特別想出來的明智之舉。說的沒錯,看起來的確是通過很多人一起工作,從不同的角度看待問題進而形成的一個最通用、最靈活的解決方案。也許這些問題你曾經見過或是曾經解決過,但是你的解決方案很可能沒有模式這麼完備。

雖然被稱為“設計模式”,但是它們同“設計“領域並非緊密聯絡。設計模式同傳統意義上的分析、設計與實現不同,事實上設計模式將一個完整的理念根植於程式中,所以它可能出現在分析階段或是更高層的設計階段。很有趣的是因為設計模式的具體體現是程式程式碼,因此可能會讓你認為它不會在具體實現階段之前出現(事實上在進入具體實現階段之前你都沒有意識到正在使用具體的設計模式)。

可以通過程式設計的基本概念來理解模式:增加一個抽象層。抽象一個事物就是隔離任何具體細節,這麼做的目的是為了將那些不變的核心部分從其他細節中分離出來。當你發現你程式中的某些部分經常因為某些原因改動,而你不想讓這些改動的部分引發其他部分的改動,這時候你就需要思考那些不會變動的設計方法了。這麼做不僅會使程式碼可維護性更高,而且會讓程式碼更易於理解,從而降低開發成本。

設計出一個優雅的、易於維護的程式難點在於發現我所說的“變化的向量”(在這裡,“向量”指的是最大的梯度變化方向(maximum gradient),而並非指一個容器類)。意思是找出系統中變化的最重要的部分,或者換句話說,發現影響系統最大的花銷在哪裡。一旦你發現了變化的向量,你就可以圍繞這個重點設計你的程式。

所以設計模式的目的就是分離程式碼中的可變部分。如果你這麼去審視這個問題,你會立刻看到多個設計模式。舉個例子,物件導向的繼承(inheritance)可以看做一種設計模式(雖然是由編譯器實現的)。它允許通過同樣的介面(不變的部分)來表現不同的行為(變化的部分)。組合也可以被認為是一種設計模式,因為它允許通過動態或靜態的方式改變實現類的物件以及他們的行為。

另一個常見的設計模式例子是迭代器。迭代器自Python出現伊始就已經隨for迴圈的使用而存在了,並且在Python2.2版本的時候被明確成為其一個特性。一個迭代器隱藏了容器內部的具體實現,提供一個依次訪問容器物件內每個元素的方式。所以,你能夠使用通用的程式碼對一個序列的每個元素做對應操作而不用去理會此序列是怎麼建立的。所以你的程式碼能夠對任何能夠產生迭代器的物件有效。

這裡列舉了三種最基本的設計模式:

  1. 結構化模式,通常用來處理實體之間的關係,使得這些實體能夠更好地協同工作。
  2. 建立模式,提供例項化的方法,為適合的狀況提供相應的物件建立方法。
  3. 行為模式,用於在不同的實體建進行通訊,為實體之間的通訊提供更容易,更靈活的通訊方法。

我們為什麼要使用設計模式?

從理論上來說,設計模式是對程式問題比較好的解決方案。無數的程式設計師都曾經遇到過這些問題,並且他們使用這些解決方案去處理這些問題。所以當你遇到同樣的問題,為什麼要去想著建立一個解決方案而不是用現成的並且被證明是有效的呢?

例子

假定現在有一個任務,需要你找到一個有效的方法合併兩個做不同事情的類,在已有系統中這兩個類在許多不同的地方被大量使用,所以移除這兩個類或是改動已有的程式碼都是異常困難的。不僅如此,更改已有的程式碼會導致大量的測試工作,因為在這樣一種依賴大量不同元件的系統中,這些修改總是會引入一些新的錯誤。為了避免這些麻煩,你可以實現一個策略模式(Strategy Pattern)和介面卡模式(Adapter Pattern)的變體,這兩種模式能夠很好的處理這種問題。

很簡單是吧?現在讓我們來仔細研究一下策略模式。

策略模式

策略模式是一種與行為相關的設計模式,允許你在執行時根據指定的上下文確定程式的動作。你可以在兩個類中封裝不同的演算法,並且在程式執行時確定到底執行哪種策略。

在上面的例子中,策略是根據例項化時context變數的值來決定的。如果給定context變數的值是“class_one”,將會執行class_one,否則就會執行class_two。

我在那裡使用它?

假定你現在正在寫一個類能夠更新或建立一條新的使用者記錄,接收同樣的輸入引數(諸如姓名、地址、手機號等),但是根據不同的情況會呼叫對應的更新或是建立方法。當然,你可能會用一個if-else判斷處理這個問題,但是如果你需要在不同的地方使用這個類呢?那麼你就得不停地重寫if-else判斷。為什麼不簡單地通過指定上下文來解決這個問題。

常規的策略模式涉及到將演算法封裝到另一個類中,但如果這樣的話,那個類就太浪費了。切記不要死記模板,把握住核心概念靈活的變通,最重要是解決問題。

 

介面卡模式

介面卡模式是一個結構性的設計模式,允許通過不同的介面為一個類賦予新的用途,這使得使用不同呼叫方式的系統都能夠使用這個類。

也可以讓你改變通過客戶端類接收到的輸入引數以適應被適配者的相關函式。

怎麼使用?

另一個使用介面卡類的地方是包裝器(wrapper),允許你將一個動作包裝成為一個類,然後可以在合適的情形下複用這個類。一個典型的例子是當你為一個table類簇建立一個domain類時,你能夠將所有的對應不同表的相同動作封裝成為一個介面卡類,而不是一個接一個的單獨呼叫這些不同的動作。這不僅使得你能夠重用你想要的所有操作,而且當你在不同的地方使用同樣的動作時不用重寫程式碼。

比較一下兩種實現:

不用介面卡的方案

如果我們需要在不同的地方做同樣的事,或是在不同的專案中重用這段程式碼,那麼我們需要重新敲一遍。

使用包裝類的解決方案

看看我們怎麼反其道而行:

在這種情況下,我們通過一個包裝類來實現賬戶domain類:

這樣的話,你就能夠在你需要的時候使用賬戶domain了,你也可以將其他的類包裝到domain類下。

工廠模式

工廠模式是一種建立型的設計模式,作用如其名稱:這是一個就像工廠那樣生產物件例項的類。

這個模式的主要目的是將可能涉及到很多類的物件建立過程封裝到一個單獨的方法中。通過給定的上下文輸出指定的物件例項。

什麼時候使用?

使用工廠模式的最佳時機就是當你需要使用到單個實體的多個變體時。舉個例子,你有一個按鈕類,這個按鈕類有多種變體,例如圖片按鈕、輸入框按鈕或是flash按鈕等。那麼在不同的場合你會需要建立不同的按鈕,這時候就可以通過一個工廠來建立不同的按鈕。

讓我們先來建立三個類:

然後建立我們的工廠類:

譯註:globals()將以字典的方式返回所有全域性變數,因此targetclass = typ.capitalize()將通過傳入的typ字串得到類名(Image、Input或Flash),而globals()[targetclass]將通過類名取到類的類(見元類),而globals()[targetclass]()將建立此類的物件。

我們可以這麼使用工廠類:

輸出將是所有按鈕型別的HTML屬性。這樣罵你就能夠根據不同的情況指定不同型別的按鈕了,並且很易於重用。

裝飾器模式

裝飾器模式是一個結構性模式,允許我們根據情況,在執行時為一個物件新增新的或附加的行為。

目的是為給一個特定的物件例項應用擴充套件的函式方法,並且同時也能夠產生沒有新方法的原物件。它允許多裝飾器結合用於一個例項,所以你就不會出現例項同單個裝飾器相捆綁的情況了。這個模式是實現子類繼承外的一個可選方式,子類繼承是指從父類整合相應的功能。與子類繼承必須在編譯時新增相應的行為不同,裝飾器允許你在執行時根據需要新增新的行為。

可以根據以下步驟實現裝飾器模式:

  1. 以原元件類為基類建立裝飾器類。
  2. 在裝飾器類中新增一個元件類的指標域
  3. 將一個元件傳遞給裝飾器類的構造器以初始化元件類指標
  4. 在裝飾器類中,將所有的元件方法指向元件類指標,並且,
  5. 在裝飾器類中,重寫每個需要修改功能的元件方法。

相關維基百科(http://en.wikipedia.org/wiki/Decorator_pattern

什麼時候使用?

使用裝飾器模式的最佳時機是當你有一個根據情況需要新增新的行為的實體時。假設你有一個HTML連結元素,一個登出連結,並且你希望根據當前頁面對具體的行為做微小的改動。這種情況下,我們可以使用裝飾器模式。

首先,建立我們所需要的裝飾模式。

如果我們在主頁並且已經登入,那麼將登出連結用h2標籤標記。

如果我們在不同的頁面並且已經登入,那麼用下劃線標籤標記連結

如果已登入,用加粗標記連結。

一旦建立了裝飾模式,我們就可以開工了。

單例模式

單例模式是一個建立型的設計模式,功能是確保執行時對某個類只存在單個例項物件,並且提供一個全域性的訪問點來訪問這個例項物件。

因為對於呼叫單例的其他物件而言這個全域性唯一的訪問點“協調”了對單例物件的訪問請求,所以這些呼叫者看到的單例內變數都將是同一份。

什麼時候能夠使用?

單例模式可能是最簡單的設計模式了,它提供特定型別的唯一物件。為了實現這個目標,你必須控制程式之外的物件生成。一個方便的方法是將一個私有內部類的單個物件作為單例物件。

因為內建類是用雙下劃線開始命名,所以它是私有的,使用者無法直接訪問。內建類包含了所有你希望放在普通類中的方法,並且通過外層包裝類的構造器控制其建立。當第一次你建立OnlyOne時,初始化一個例項物件,後面則會忽略建立新例項的請求。

通過代理的方式進行訪問,使用__getattr__()方法將所有呼叫指向單例。你可以從輸出看到雖然看起來好像建立了多個物件(OnlyOne),但 __OnlyOne物件只有一個。雖然OnlyOne例項有多個,但他們都是唯一的 __OnlyOne物件的代理。

請注意上面的方法並沒有限制你只能建立一個物件,這也是一個建立有限個物件池的技術。然而在那種情況下,你可能會遇到共享池內物件的問題。如果這真是一個問題,那你可以通過為共享物件設計簽入“check-in”和遷出“check-out”機制來解決這個問題。

總結

在本文中,我只列舉了幾個我再程式設計中覺得十分重要的設計模式來講,除此之外還有很多設計模式需要學習。如果你對其他的設計模式感興趣,維基百科的設計模式部分可以提供很多資訊。如果還嫌不夠,你可以看看四人幫的《設計模式:可複用物件導向軟體的基礎》一書,此書是關於設計模式的經典之作。

最後一件事:當使用設計模式時,確保你是用來解決正確地問題。正如我之前提到的,設計模式是把雙刃劍:如果使用不當,它們會造成潛在的問題;如果使用得當,它們則將是不可或缺的。

相關文章