要不是前輩們逼著自己學 Class 物件的實作,可能我到現在都還不會寫任何 Class
我一直覺得 Class 很難,看著前輩們各種高超的物件使用,我覺得我大概一輩子都學不會吧?
接到的任務是使用 Class Builder 重構專案的程式碼,雖然沒有我理解 Decorator 這麼的困難,大概花了我半天(?)還是一天的時間嘗試
雖然說花了一天有實作出初步的 Class 物件,但沒想到我模仿前輩做出來的 Class 設計是Abstract Factory 設計模式
不過剛做出來真的很開心!! 但卻慘遭前被打槍 XDD
後來前輩說要做的是Builder 模式,一知半解的自己還是再帶著這個網站跟關鍵字嘗試了約莫一天才生出初版免強能看的 Builder
後續我實作了一個 Class Builder
取代我原先的實作,結果 500 行 code 可以刪除 200 多行,節省近乎一半的程式碼。
從學會到現在快兩個月,我已經在專案上有使用至少 3 次 Builder 來實現需求或重構,我也認為 Builder 很好用,不愧是設計模式,比起小小的變數, 迴圈跟函式,設計模式能夠夠有效的安排程式碼更簡單的實現需求呢!
雖然說是物件導向程式語言 Python
,但也可以不用物件導向來寫 Python, 我先前完全不會用 Class 物件製作,常常使用函式跟一般功能也能達成需求!
Builder Pattern + Chain Function
本篇我想用兩個範例來介紹 Builder!
組SQL WHERE 語句
Builder 被我拿來實現 自動判斷
+ 組裝
的需求,剛剛又回想了一下,真的短短兩個月用了至少 3 個!很想介紹更多範例,但避免文太長就忍痛只分享兩個 XD
組SQL WHERE 語句
當初我只記得前輩給了我一串如下的函式, 但我怎麼查就是查不到 XD,查了兩三個小時覺得十分困惑,後來只好問一下同事,原來這個叫做 Chain Function
composed_sql = SQLComposer().filter(description, column, table).build()
需求: 根據不同的描述(description) 自動判斷需要組成什麼where sql 語句
以下是我寫的簡單的 builder
class SQLComposer(object):
def __init__(self):
# 利用 list存取每個字串, 所以形式會像:
"""
['date BETWEEN 2018/09/01 AND 2019/09/01', 'fruit = "apple"']
"""
self.filter = []
# 我也有設計order, 可以塞 order by 跟 Top/ Bottom
self.order = []
def query_by_time(self, column):
params = ..
...
# 組好語句就塞list內
sql = "%(column)s BETWEEN '%(start)s' AND '%(end)s'" % params
self.stmt.append(sql)
# 最後記得回傳self 因為要執行Chain Function
return self
def query_by_value(self, column):
...
sql = "%(column)s = '%(value)s'" % params
self.stmt.append(sql)
return self
def order_by_top_bottom(self, column):
params = ...
.....
sql = "ORDER BY %(column)s %(order)s LIMIT %(value)s)" % params
self.order.append(sql)
return self
def filter_sql(self, description, column):
...
for element in description:
if element in description:
# 執行函式
return self
# 最後run這個函式可以將先前存在list內的眾多sql語句 build 起來
def build(self):
composed_sql = "WHERE "
for sql in self.stmt:
composed_stmt += "%s AND " % sql
return composed_stmt
稍微介紹一下,這是組 sql 的 builder,根據送進來的 description 來判斷需要組什麼 sql 語句
呼叫的函式是長這樣
composed_sql = SQLComposer().filter_sql(description, column).build()
當然也可以設計成這樣
composed_sql = SQLComposer().query_by_time(column).query_by_value(column).build()
這樣利用多個函式組成的 Chain Function, 主要是提倡靈活度,專看不同的 需求
去做 設計
若今日是需要 builder 中不同的函式帶不同的變數,抑或是靈活度自由度要高一點就可以像上面的設計
只是我舉的範例,我依照 description 自動判斷,所以我統一利用 filter_sql
來抓判斷 description 內的值自動執行對應的函式!
Builder 就如其名是 建築建造,通常是 Class 內有提供幾個函式來 拼湊零件
,最後寫一個 build
函式將這些零件組裝起來
要記得若需要用到鎖鏈 chain 函式,每個函式都要 return self
才能夠繼續執行下個動作
解說
解說一下我的作法
def __init__(self):
# 利用 list存取每個字串, 所以形式會像:
"""
['date BETWEEN 2018/09/01 AND 2019/09/01', 'fruit = "apple"']
"""
self.filter = []
# 我也有設計order, 可以塞 order by 跟 Top/ Bottom
self.order = []
剛開始就設定 init 有兩個空的 list, 我的想法是每執行一個函式就將輸出的 sql 語句存進 list
當初的想法是想設計 filter 存 Where 語句
, order 存 order by
的語句,最後再 build 成完整的一句 sql !!
也就是說每 run 一個函式就會把答案加進 init 設的變數中
def query_by_time(self, column):
params = ..
...
# 組好語句就塞list內
sql = "%(column)s BETWEEN '%(start)s' AND '%(end)s'" % params
self.stmt.append(sql)
# 最後記得回傳self 因為要執行Chain Function
return self
def query_by_value(self, column):
...
sql = "%(column)s = '%(value)s'" % params
self.stmt.append(sql)
return self
這個是 Builder 的零件,設計上就是多個零件(函式),看組裝需要哪些零件
def filter_sql(self, description, column):
...
for element in description:
if element in description:
# 執行函式
return self
這個是我額外寫的,專門依據 description 自動判斷需要哪些零件(需要跑哪些函式),所以我寫的函式最後只會有 filter_sql
, SQLComposer
和 builder
會給外面 call
def build(self):
composed_sql = "WHERE "
for sql in self.stmt:
composed_stmt += "%s AND " % sql
return composed_stmt
最後的 builder 函式在 Chain 要擺在最後面,因為他是驅動將全部執行函式後所產生的輸出組成符合需求的答案!
其它用法 ~
像 Builder 除了拿來組 sql 語句外,我也會拿來 組複雜的字典結構
, 還有生成單元測試所需的測試資料
個人也用了這個方法重構程式,將以前只有一堆函式, 變數撰寫的 code 除去,十分方便!
也讓我開始重視研究其他設計模式!
技術小筆記: *args
最後是想介紹與 Builder 一同學習時,所學到的 *args 用法
想當初這個*args 讓我在大二物件導向吃了不少苦頭,因為大二我就開始很混了,然後就看不懂**args 之類的變數,接著內心就產生更高更厚的牆讓我覺得 Class 很難,學不會 XD
當初學 builder 時因為想實現帶進來變數直接塞進 datetime 格式
我們看一下 datetime 格式
datetime.datetime(2019, 1, 2)
當初我在用 builder 時想要實現在外 call 的函式能自由帶入 日期 變數 生成日期
SQLBuilder().time(2019, 1, 2).build()
def time(date):
return datetime.datetime(date)
在我嘗試過函式帶 list
, tuple
, set
後發現無法實現這個功能
原先以為既然是 ( )
那用 tuple 能不能解決 XDDD,事後發現根本無法
你看 datetime(2019, 1, 2)
長得這麼像 (2019, 1, 2)
< tuple
感覺就是能塞 tuple 進去 work 的啊 QQQQ!!
好吧嘗試了大多的時間,終於被前輩點了一下,使用 *args
來實現
順便也學習到的 *args
怎麼個神奇法, 居然這樣就能 work
SQLBuilder().time(2019, 1, 2).build()
def time(*args):
return datetime.datetime(args)
!!!!! 我的天,浪費了青春小女子好多時間嗚嗚, 是說再過幾年我也變大齡剩女了 qq
起因是 args 可以夾帶多個變數且不改變變數的型態,可以直接使用,也可透過定義多個變數來從 *args 取得內部資訊
我做兩個小範例,一樣針對 datetime,大家可以看著,然後試試看改變程式改成符合你需求的功能
def test(*args):
year, month, day = args # 實際上是存三個數字, 而非list, set, tuple
# 所以同樣也可以分別定義三個變數為三個數字,分別處理
return datetime.datetime(year, month, day)
def test(*args):
# 實際上就也是三個數字 2019, 1, 2 所以可以直接使用!
return datetime.datetime(args)
最後提供兩個網站做參考,還有個 Kwargs 是可以帶字典,但我... 沒有去研究也不會使用就是 XD
那我這篇就介紹到這,感謝大家!