重構 (refactoring) 是指不改變軟體 (software) 外部行為下,對程式原始碼 (source code) 進行整理、修改,不外是希望提升原始碼的可讀性及更易於維護
簡單說,只要感覺原始碼有些不對勁就該重構,尤其在一個大的開發團隊中,每個小組負責的部份可能都不同,因而常常會發生識別字 (identifier) 命名不一致或是太多類別 (class) 有共通特性的情況。所以需要適時的重構原始程式碼,這也是軟體開發過程中一項重要的工作。
常見的重構技術如下
- 對屬性 (field) 進行封裝 (encapsulation) ,並將屬性及方法 (method) 名稱更改為具有一致性。
- 移動屬性或方法,讓其出現在更適合的位置。
- 使型態 (type) 可以更具通用性,或挑出共通的屬性、方法成父類別 (superclass) 及子類別 (subclass) 。
有許多專書討論重構技術,由於本書篇幅有限,不可能塞太多東西,所以這裡僅是初步介紹而已。
重構通常需要搭配單元測試 (unit testing) ,確保軟體各部份的功能沒有改變。
我們的 Encrypt 類別的 __init__() 方法其實有點問題
006 | def __init__(self): |
007 | self.setcode() |
這樣的做法是建立 Encrypt 物件 (object) ,同時建立隨機的密碼表,雖說目前 Encrypt 類別符合我們開發的需求,倒是當我們想要擴充功能的時候就會發生問題。怎麼說呢?例如有次編碼結果不錯,我們想要下次繼續沿用相同的密碼表,這時候就需要把密碼表儲存起來了,存檔有兩種選擇,一種是儲存整個物件,另一種則是儲存密碼表的字串就好。
儲存整個物件比較麻煩,相對儲存密碼表字串 (string) 比較簡單,原因不外是字串太常用了,所以 Python 程式庫 (library) 就提供了簡單的方式來儲存字串。感覺到問題出在哪裡的嗎?是的,如果我們把密碼表字串儲存到某一的純文字檔案中,重新讀取回來就得設定 code 屬性,可是我們目前的版本無法替我們做到這一點。
還有就是我們承接好幾個單元發展出的 encrypt07.py 也太不 Python 啦! Python 是以簡潔、豐富程式庫而著稱的程式語言 (programming language) ,我們連續用了 chr() 、 ord() 、 str() 、 len() 等,雖說是內建函數 (function) 沒錯,可是卻是拿 Unicode 編碼來思考轉換,一點也沒用 Python 思考說。
下面我們直接介紹 Encrypt 類別的最終版本,並且說明重構的原因。
完整程式請參考「範例程式篇」的 encrypt.py 。
首先刪去 setcode() ,替 __init__() 新增一個參數 (parameter) str ,同時把 code 屬性改為串列 (list) ,另外新增一個串列屬性 alph
005 | # 建立 Encrypt 物件同時建立密碼表 |
006 | def __init__(self, str=None): |
007 | # 設定 code |
008 | if str == None: |
009 | self.code = [chr(i) for i in |
010 | range(97, 123)] |
011 | shuffle(self.code) |
012 | else: |
013 | self.code = list(str) |
014 | |
015 | # 設定 alph |
016 | self.alph = [chr(i) for i in |
017 | range(97, 123)] |
code 屬性改成串列的原因很簡單,因為串列是 Python 的工作馬,幾乎程式裡大大小小所有的工作都交給串列比較方面,另外串列是可變的,這給了我們很大的方便,留意這兩行
009 | self.code = [chr(i) for i in |
010 | range(97, 123)] |
這是串列的綜合運算 (comprehension) ,直接在中括弧中寫運算式建立串列元素值,由於 range() 回傳參數指定範圍的整數序列,因此 i 在 for 迴圈取得的第一個值為 97 , chr(i) 回傳單一字元的字串 "a" ,因此 code 會得到 26 個英文小寫字母的串列。
下面一行
011 | shuffle(self.code) |
shuffle() 直接攪亂串列裡元素的順序,同樣在 random 模組 (module) 中,所以要先 import 進來
001 | from random import shuffle |
參數 str 用來直接設定 code 屬性,注意這裡用內建函數 list() 將 str 轉換成串列,至於另一個屬性 alph 則是英文小寫字母表,這在重構 toEncode() 及 toDecode() 的地方會用到。
然後我們也新增 __str__() 來代替 getcode()
019 | # 回傳密碼表字串 |
020 | def __str__(self): |
021 | code = "".join(self.code) |
022 | return "code: " + code |
因為類別預設的 __str__() 為物件的字串形式,留意這裡是用空字串加上 join() 方法連結 code 中的所有元素。
toEncode() 重構版本的編碼迴圈 (loop) 如下
029 | # 利用迴圈走完參數字串的所有字元 |
030 | for i in str: |
031 | # 判斷該字元是否為英文小寫字母 |
032 | # 若是英文小寫字母就進行編碼轉換 |
033 | if i in self.code: |
034 | j = self.alph.index(i) |
035 | result += self.code[j] |
036 | else: |
037 | result += i |
之前的版本是利用計算編碼值,重構後的版本則是用串列的 index() 方法找索引值,也就是簡單用兩個串列的對應索引值來作替換,這點同樣用在解碼的迴圈中
047 | # 利用迴圈走完參數字串的所有字元 |
048 | for i in str: |
049 | # 判斷該字元是否為英文小寫字母 |
050 | # 若是英文小寫字母就進行解碼轉換 |
051 | if i in self.code: |
052 | j = self.code.index(i) |
053 | result += self.alph[j] |
054 | else: |
055 | result += i |
兩個迴圈幾乎一樣,除了把 self.alph 換成 self.code 以外,原本的巢狀迴圈 (nested loop) 也不需要了。
重新仔細看一下 encrypt.py 是不是更容易理解了呢?接下來我們就要進入 GUI 的部份了,在此之前先來認識一下標準模組庫 (standard library) 吧!
中英文術語對照
重構 | refactoring |
軟體 | software |
原始碼 | source code |
識別字 | identifier |
類別 | class |
屬性 | field |
封裝 | encapsulation |
方法 | method |
型態 | type |
父類別 | superclass |
子類別 | subclass |
單元測試 | unit testing |
物件 | object |
程式庫 | library |
程式語言 | programming language |
函數 | function |
參數 | parameter |
串列 | list |
綜合運算 | comprehension |
模組 | module |
迴圈 | loop |
巢狀迴圈 | nested loop |
標準模組庫 | standard library |
重點整理
- 重構是指重新整理程式碼,讓程式更易於維護。
- 常見的重構技術包括整理屬性、方法,挑出類別的共通特性定義父類別等等。
- Encrypt 類別的重構包括刪去 setcode() ,重新定義 __init__() ,增加 __str__() ,修改 toEncode() 與 toDecode() 等。
問題與討論
- 什麼是重構?為什麼要替開發好的程式進行重構?
- Encrppt 類別進行了哪些重構?為什麼要作這些重構?
練習
- 承接上一個單元的猜數字遊戲,將新程式寫在 exercise2001.py 中,替 __init__() 新增一個實體屬性 length ,並用參數 digit 來設定, digit 預設為 None ,當 digit 為 None 或小於 3 、大於 6 之時, length 就設定為 4 ,不然就設定為 digit 。
- 承上題,將新程式寫在 exercise2002.py 中,修正 set_code() 裡的一個潛在錯誤,讓 GuessGame 能符合用 digit 設定猜測長度的設定。
the end
沒有留言:
張貼留言