Python 入門指南 V2.00 - 單元 20 - 重構




重構 (refactoring) 是指不改變軟體 (software) 外部行為下,對程式原始碼 (source code) 進行整理、修改,不外是希望提升原始碼的可讀性及更易於維護



簡單說,只要感覺原始碼有些不對勁就該重構,尤其在一個大的開發團隊中,每個小組負責的部份可能都不同,因而常常會發生識別字 (identifier) 命名不一致或是太多類別 (class) 有共通特性的情況。所以需要適時的重構原始程式碼,這也是軟體開發過程中一項重要的工作。


常見的重構技術如下


  1. 對屬性 (field) 進行封裝 (encapsulation) ,並將屬性及方法 (method) 名稱更改為具有一致性。
  2. 移動屬性或方法,讓其出現在更適合的位置。
  3. 使型態 (type) 可以更具通用性,或挑出共通的屬性、方法成父類別 (superclass) 及子類別 (subclass) 。

有許多專書討論重構技術,由於本書篇幅有限,不可能塞太多東西,所以這裡僅是初步介紹而已。

重構通常需要搭配單元測試 (unit testing) ,確保軟體各部份的功能沒有改變。

我們的 Encrypt 類別的 __init__() 方法其實有點問題


006def __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 物件同時建立密碼表
006def __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 的工作馬,幾乎程式裡大大小小所有的工作都交給串列比較方面,另外串列是可變的,這給了我們很大的方便,留意這兩行


009self.code = [chr(i) for i in 
010             range(97, 123)]

這是串列的綜合運算 (comprehension) ,直接在中括弧中寫運算式建立串列元素值,由於 range() 回傳參數指定範圍的整數序列,因此 ifor 迴圈取得的第一個值為 97chr(i) 回傳單一字元的字串 "a" ,因此 code 會得到 26 個英文小寫字母的串列。


下面一行


011shuffle(self.code)

shuffle() 直接攪亂串列裡元素的順序,同樣在 random 模組 (module) 中,所以要先 import 進來


001from random import shuffle

參數 str 用來直接設定 code 屬性,注意這裡用內建函數 list()str 轉換成串列,至於另一個屬性 alph 則是英文小寫字母表,這在重構 toEncode()toDecode() 的地方會用到。


然後我們也新增 __str__() 來代替 getcode()


019# 回傳密碼表字串
020def __str__(self):
021   code = "".join(self.code)
022   return "code: " + code

因為類別預設的 __str__() 為物件的字串形式,留意這裡是用空字串加上 join() 方法連結 code 中的所有元素。


toEncode() 重構版本的編碼迴圈 (loop) 如下


029# 利用迴圈走完參數字串的所有字元
030for 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# 利用迴圈走完參數字串的所有字元
048for 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

重點整理


  1. 重構是指重新整理程式碼,讓程式更易於維護。
  2. 常見的重構技術包括整理屬性、方法,挑出類別的共通特性定義父類別等等。
  3. Encrypt 類別的重構包括刪去 setcode() ,重新定義 __init__() ,增加 __str__() ,修改 toEncode()toDecode() 等。

問題與討論


  1. 什麼是重構?為什麼要替開發好的程式進行重構?
  2. Encrppt 類別進行了哪些重構?為什麼要作這些重構?

練習


  1. 承接上一個單元的猜數字遊戲,將新程式寫在 exercise2001.py 中,替 __init__() 新增一個實體屬性 length ,並用參數 digit 來設定, digit 預設為 None ,當 digitNone 或小於 3 、大於 6 之時, length 就設定為 4 ,不然就設定為 digit
  2. 承上題,將新程式寫在 exercise2002.py 中,修正 set_code() 裡的一個潛在錯誤,讓 GuessGame 能符合用 digit 設定猜測長度的設定。

the end

沒有留言: