[Design Pattern-Python]設計模式-前言

Daniel Chiang
12 min readJan 17, 2021

--

寫作動機

之前花了一些時間看書學習design pattern,光看當然是不夠的,實作練習對於學程式的人是最重要的捷徑。不過當時看的書是用Java實作,我是使用我熟悉的python 實作。在我寫這篇文章時,還沒什麼找到多少python 實作design pattern 的教學文章(老實講,我起初也沒想要買書來學的)。因此有了想法,竟然我都已經自己實作過這些設計模式,我只要把模式的概念以及程式的範例解釋就能成一系列文章,希望我提供的線上教學文章能幫助到想學習的人。

為什麼該學設計模式

我相信有興趣來看這篇文章的人,可能對於設計模式有一些認識,但是我們還是得提一下學習設計模式帶來的優點。

  1. 溝通: 別忽略了溝通的重要性,描述寫程式這件事本身很抽象,所以當開發者擁有了"共通的詞彙",就能更快知道彼此設計的想法。每個模式都有其背後的內容、特性、限制,團隊之間就不容易有誤解,新進開發者也更容易向資深開發者學習。
  2. 維護: 學習設計模式就像是站在巨人的肩膀上一樣,這些模式都是前人遇到重複性的問題,透過經驗累積統整出的原則(工具)。現實開發環境很常會在你現有的程式中增加新功能,進而衍生出一堆bug。設計模式就是要解決這樣的問題,降低你重構的困難度,並方便統一開發者的風格,使你的程式在光陰歲月摧殘之下仍然有維護的可能。

我覺得這部影片講得清楚: 軟體工程師為什麼要學 Design Pattern? | 物件導向 | SOLID | 工程師 Nic

使用時機

這是個好問題,很有可能你今天所有模式都學完了還不會使用,怎麼,想退出了嗎?別忘了,這些都是前人經驗留下的結晶,因此你不太可能在開發經驗不足的情況下就能善用每個模式。我認為學習設計模式的最大目的就是讓你用"模式思考"的方式開發。
學習完模式後會有幾種人
第一種人是無論如何都想套用設計模式到它的程式碼中,變成為了硬要符合設計模式而去修正了他的程式碼。當你在設計時,最基本的原則就是保持簡單,這裡的簡單有兩種解釋,第一是如果用很簡單易懂的程式碼就能解決問題就不是非要使用模式。另外一個簡單是讓你開發更有彈性,擴充更簡單。在設計模式中很容易產生額外的類別及物件,增加了設計的複雜度。所以別認為沒使用模式就不專業。
第二種人是不知道該在何時使用模式,在學習模式時通常使用的例子比較生活化,但在現實情形可能更複雜。別忘了前面提到的用模式思考,這才是你該學到的,你在設計時並不需要糾結每個環節都符合模式,永遠記得保持簡單的原則,當你在設計時,可以自行取捨如何使用模式,當你認真把思緒放在設計上,模式很自然的就會出現。

設計模式是一種工具,它只會在該使用的地方出現,就像你砍樹會用斧頭,但切水果會用水果刀。

學習

現在回來聊聊我會使用怎樣的教學方式,按照順序是情形直覺的寫法套用模式的解法模式概念說明

  1. 情形: 運用生活上的一些案例,讓開發的情形就像故事一樣會幫助你更好理解。
  2. 直覺的寫法: 就像前面說的簡單原則,如果這個情形沒有必要使用模式那就表示這是一個好的寫法。
  3. 套用模式的寫法: 好壞是比較出來的,為了教學,情境的走向一定會以套用模式的寫法是更合適的。我認為有像這樣重構的過程,會幫助你更加理解模式該如何使用。
  4. 模式概念: 我把概念放在最後面,我認為一開始很用力地解釋概念,你可能也無法真的理解,反而是例子實作後你搞不好就知道模式的概念,最後我在做解釋當作總結會讓你更印象深刻。

這個系列主要會介紹最基本且最常用的模式

模式清單:

Python 物件導向程式設計(Object-oriented programming,OOP)

在開始前你必須想了解python的物件導向設計,如果你還不熟悉,這邊簡單的幫你複習一下。

認識Python的類別與物件

# 車子類別(Class)
class Car():
# 建構式(Contructor)
def __init__(self, color):
self.color = color # 顏色屬性(Attribute)
# 開車方法(Method)
def drive(self):
""" implement drive behavior"""
red_car = Car('red') # 車子類別的物件(Object)

留意一下名詞,這是python開發者的共同詞彙,通常在建立類別名詞開頭使用大寫區隔。

  • 類別(Class): 物件的設計圖,你可以從中知道這一類的物件有什麼特性、功能。
  • 物件(Object): 由類別建立的實體。
  • 建構式(Contructor): 建立物件時會自動執行的方法(Method),通常用來初始化物件。
  • 方法(Method): 物件的行為
  • 屬性(Attribute): 物件的資料

繼承

# 公車繼承車子類別
class Bus(Car):
def __init__(self, color):
super().__init__(color)

def stop_button(self):
""" implement stop button behavior"""

bus = Bus('red')
bus.drive()
print(bus.color)
>> red

公車與車子有著垂值關係,簡單來說公車屬於一種車子,所以公車該具備車子的特性。子類別(公車)還可以自行擴充新的方法,像是公車有下車按鈕的功能但一般車子不會有。繼承能增加擴重能力以及減少重複的程式碼。

抽象類別、介面

python相比java,java是一套對於物件導向支援非常完整的語言;python是追求簡潔及可讀性,設計成快速開發的語言。使用java是更能了解物件導向的本質的,java嚴格區分了抽象以及介面。python沒有原生的抽象類別和方法,必須使用abc package實作抽象類別、介面。在Python不管是抽象類別還是介面都是繼承abc.ABC,這很容易讓初學者搞混,但兩者意義差很多。

在Python類別繼承了abc.ABC表示此類別為抽象類別,是無法實體化的。當有方法(Method)套上@abc.abstractmethod裝飾器(decorator)時,表示建立抽象方法,必須依靠子類別覆寫。

抽象類別: 是發現父類別與子類別有共同的東西

from abc import ABC, abstractmethodclass Animal(ABC):
def __init__(self, name="john", shout_num=1):
self.name = name
self.shout_num = shout_num
def run(self):
""" implement run behavior"""
print('run')
@abstractmethod
def shout(self):
pass

class Dog(Animal):
def __init__(self, name, shout_num):
super().__init__(name, shout_num)
def shout(self):
print('wo' * self.shout_num)
class Cat(Aniamal):
...
dog = Dog('daniel', 3)
dog.run()
>>> run
dog.shout()
>>> wowowo
animal = Animal()
TypeError: Can't instantiate abstract class Animal with abstract methods shout

可以看到我們定義了一個動物抽象類別,因為我們發現貓狗都會跑,只要貓狗繼承了動物類別,便能都執行跑的方法。但我們並不知道之後新增的其他動物類別會發出什麼聲音,因此shout定義為抽象方法,由子類別自行覆寫。

介面: 不需要預先知道子類別是什麼

from abc import ABC, abstractmethodclass Animal(ABC):
def __init__(self, name="john"):
self.name = name
class ShoutInterface(ABC):
@abstractmethod
def shout(self):
pass

class DogShout(ShoutInterface):
def shout(self):
print('wowowo')
class CatShout(ShoutInterface):
...

class Dog(Animal, DogShout):
def __init__(self, name):
super().__init__(name)
dog = Dog('daniel')
print(dog.name)
>>> daniel
dog.shout()
>>> wowowo

這個例子可以看到我把行為設成抽象類別,由子類別"覆寫"父類別的方法。
補充:雖然python上沒有嚴格限制,但基本上接口不能有任何的具體方法,也就是說ShoutInterface裡的shout方法不能實作任何行為。
※抽象(機底)類別與接口基本上可以花一整張篇幅來講,我這邊是用比較快的方式帶過。

「抽象類別」是類別的抽象化,而「接口」則是行為的抽象化。

多型(Polymorphism)
現在你已經了解了抽象類別以及介面,只要子類別繼承了一樣的介面,就能消除類別間的耦合性。什麼意思?簡單來說,你要使用某些行為的時候,你不需要知道這個行為怎麼實作的,只要知道它的介面方法就好了。
看看例子你會更好了解。

from abc import ABC, abstractmethodclass ShoutInterface(ABC):
@abstractmethod
def shout(self):
pass

class DogShout(ShoutInterface):
def shout(self):
print('wowowo')

class CatShout(ShoutInterface):
def shout(self):
print('meow')

class BirdShout(ShoutInterface):
def shout(self):
print('juju')
shout_list = []
shout_list.append(DogShout())
shout_list.append(CatShout())
shout_list.append(BirdShout())
for animal in shout_list:
animal.shout()

因為大家都繼承了ShoutInterface,我就可以無顧慮的使用shout方法,多型讓我們更好維護以及擴充。

封裝(Encapsulation)
封裝的目的是不希望外部有任何機會以我們非預期的方式去更改物件,封裝可以強化物件的強健性。
在python中有property 這個decorator可以將方法變成屬性,等於是封裝了這個屬性,外部不需要知道拿到這個屬性的實作方法,並且也不會被任意地更改。

class Product():
@property
def price(self):
return 100
Product().price
>>> 100
Product().price = 150
>>> AttributeError: can't set attribute

我們可以自行定義該如何修改價格。

class Product():
def __init__(self):
self._price = 100
@property
def price(self):
return self._price

@price.setter
def price(self, value):
self._price = value

product = Product()
product.price = 150
product.price
>>> 150

你已經學完了物件導向的重要特性,現在可以開始進入design pattern的世界了

總結

因為我覺得這個系列不會特別出一篇來做總結,所以我就做結論。
design pattern是以前人經驗整理出來的工具,因此我們也必須達到一定的經驗才能夠更活用它。初學者可能會瘋狂的套上設計模式,慢慢地你會察覺到有些地方並不適合使用設計模式,最後你設計模式會自然出現在你的程式中。
我相信當你在練習design pattern時,本身寫code能力也會不斷進步,你可以從中學到更多開發原則,或是看到你從沒想過的類別繼承、組合方式。
對你同事好一點,來學學設計模式吧。

訓練用模式思考

參考資料:

物件導向武功秘笈(2):招式篇 — Python與Java的物件導向編程介紹。
[Python物件導向]淺談Python類別(Class)

--

--