重構 - 讀書筆記(Python示例)
去年十二月, 重讀時, 輸出了幾篇博文, 主要幾章重構技巧梳理 6/7/8/9/10/11, 這周重讀時, 從另一個角度總結一下
我們總是想著, 找個時間重構, 額, 其實, 重構更應該放在平時, 每一次去變更代碼時處理. 畢竟, 所謂的重構契機有時候太過遙遠; 而如果不做重構, 痛苦的是每時每刻維護代碼的自己
如果你發現自己需要為程序添加一個特性, 而代碼結構使你無法很方便地達成目的, 那就先重構那個程序, 使特性的添加比較容易進行, 然後再添加特性
另外, 如果可能, 盡量加單元測試, 哪怕一次只增加一兩個, 一段時間後, 你會發現, 你會感謝過去的自己
原則
- 小步前進, 頻繁測試
- 隔離變化
- 控制可見範圍, 讓變數/常量/函數/類等, 在最小的範圍內可見. 例如設為私有變數/私有函數, 移除不必要的設值函數
- 重構時, 不要關注性能. 到性能優化階段, 再關注性能. 不同階段關注點不一樣, 不要過早優化. 很多時候, 性能並不是瓶頸, 可讀性和可維護性更重要
- 任何時候, 都不要拷貝代碼, 拷貝類, 甚至拷貝源碼文件
1. 命名
- 好的名字, 清晰表達其含義. 命名至關重要
- 好的代碼應該清楚表達出自己的功能, 變數名稱是代碼清晰的關鍵
- 如果為了提高代碼的可讀性, 需要修改某些名字, 大膽去改!
- IDE/單元測試/好的查找替換工具
- 建議讀編寫可讀代碼的藝術這本書.
2. 常量和臨時變數
提取常量
你有一個字面數值, 帶有特別含義. 創建一個常量, 根據其意義為它命名, 並將上述字面數值替換為這個常量
def potential_energy(mass, height): return mass * 9.81 * height# toGRAVITATIONAL_CONSTANT = 9.81def potential_energy(mass, height): return mass * GRAVITATIONAL_CONSTANT * height
任何時候, 都不要拷貝常量, 當你發現要改一個數據, 要到非常多的文件去改字面值時, 你就需要意識到, 該提取常量了
加入: 引入解釋性變數
一個複雜的表達式, 將複雜表達式或其中一部分放入臨時變數, 以變數名稱來解釋表達式用途
if "MAC" in platform.upper() and "IE" in browser.upper() and was_initialized() and resize > 0: #do something# tois_macos = "MAC" in platform.upper()is_ie_browser = "IE" in browser.upper()was_resized = resize > 0if is_macos and is_ie_browser and was_initialized() and was_resized: # do something
分解: 分解臨時變數
某個臨時變數被賦值超過一次, 非循環變數, 也不用於收集計算結果.每次賦值, 創砸一個獨立, 對應的臨時變數
單一職責原則
tmp = 2 * (height * width)print tmptmp = height * widthprint tmp# toperimeter = 2 * (height * width)print perimeterarea = height * widthprint area
去除: 移除臨時變數
臨時變數僅被一個簡單表達式賦值一次, 可以去除這個臨時變數
臨時變數, 簡單表達式, 另外, 需要考慮使用次數, 如果僅使用一次, 可以去除, 如果多次, 則需謹慎考慮對可讀性的而影響
best_price = order.base_price()return best_price > 1000# toreturn order.base_price > 1000
移除: 控制標記
在一系列布爾表達式中, 某個變數帶有」控制標記」(control flag)的作用. 以break語句或return取代控制標記
def dosomething(): is_success = False if xxx: is_success = True if yyy: is_success = False ... return is_success# todef dosomething(): if xxx: return True if yyy: return True ... return False # 一定不要忘記
注意力相關.
這類邏輯中, 很痛苦的是, 你必須無時無刻關注這些控制標記的值, 追蹤變數在每一個邏輯之後的變化, 會帶來額外的思考負擔, 從而讓代碼變得不易讀.
3. 函數
拆分: Extract Method提煉函數
你有一段代碼可以被組織在一起並獨立出來, 將這段代碼放進一個獨立函數中, 並讓函數名稱解釋該函數的用途
def print_owing(double amount): print_banner() // print details print "this is the detail: " print "amnount: %s" % amount# todef print_details(amount): print "this is the detail: " print "amnount: %s" % amountdef print_owing(double amount): print_banner() print_details(amount)
去除: Inline Method內聯函數
一個函數的本體與名稱同樣清楚易懂, 在函數調用點插入函數本體, 然後移除該函數
小型函數, 函數太過簡單了, 可能只有一個表達式, 去除函數!
def is_length_valid(x): return len(x) > 10print the length is %s % (valid if is_length_valid(x) else invalid)# toprint the length is %s % (valid if len(x) > 10 else invalid)
合併: 合併多個函數, 使用參數
若干函數做了類似的工作. 但在函數本體中卻包含了不同的值. 建立單一函數, 以參數表達那些不同的值
def five_percent_raise(): passdef ten_percent_raise(): pass# todef percent_raise(percent): pass
副作用: 函數不應該有副作用
某個函數既返回對象狀態值, 又修改對象狀態. 建立兩個不同函數, 一個負責查詢, 一個負責修改.
單一職責原則, 一個函數不應該做兩件事, 函數粒度盡量小.
4. 表達式
guard(注意力相關)
過多的條件邏輯, 難以理解正常的執行路徑. 在python中的特徵是, 縮進太深
coolshell中曾經討論過的問題 如何重構「箭頭型」代碼, 而在python中的現象是, 縮進嵌套層級太深, 有時候甚至有十幾層縮進, 整體難以理解
而減少嵌套縮進的方式是, 使用guard語句, 儘早返回,
注意力相關, 儘早return, 你也就不用關心已經過去的邏輯了, 只需關注後面代碼的邏輯.
if _is_dead: result = dead_amount()else: if _is_separated: result = separated_amount() else: if _is_retired: result = retired_amount() else: result = normal_payamount()return result# toif _is_dead: return dead_amount()if _is_separated: return separated_amount()if _is_retired: return retired_amount()return normal_payamount()
合併: 合併條件表達式
你有一系列條件測試, 都得到相同結果. 將這些測試合併成一個條件表達式, 並將這個條件表達式提煉成為一個獨立函數
if _seniority < 2: return 0if _months_disabled > 10: return 0if _is_part_time: return 0# toif is_not_eligible_for_disability: return 0
分解: 分解複雜條件表達式
你有一個複雜的條件語句(if-then-else). 從if, the, else三個段落中分別提煉出獨立函數
if date < SUMMER_START) or date > SUMMER_END: charge = quantity * _winter_rate + _winter_servioce_chargeelse: charge = quantity * _summer_rate# toif not_summber(date): charge = winter_charge(quantity)else: charge = summber_charge(quantity)
提取: 合併重複的條件片段
在條件表達式的每個分支上有著相同的一段代碼. 將這段重複代碼搬移到條件表達式之外
if is_special: total = price * 0.95 send()else: total = price * 0.98 send()# toif is_special: total = price * 0.95else: total = price * 0.98send()
這是維護系統, 特別是中後期很容易忽略的問題. 很容易在代碼中出現, 特別是遇到那種加需求的地方, 通常, 會選擇不動原來的代碼, 加個分支, 複製代碼下來改. 但這樣的後果是, 逐步地, 會發現每個分支中都有重複代碼.
5. 參數及返回值
參數和返回值: 提取對象
如果參數/返回值是一組相關的數值, 且總是一起出現, 可以考慮提取成一個對象.
def get_width_height(): .... return width, heightdef get_area(width, height): return width, height# toclass Rectangle(object): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.heightdef get_shape(): .... return Rectangle(height, width)
類似的還有: start_time/end_time -> TimeRange /
減少參數
對象調用了某個函數, 並將所得結果作為參數, 傳遞給另一個函數. 而接受該參數的函數本身也能調用前一個函數. 讓參數接收者去除該參數, 並直接調用前一個函數
base_price = quantity * item_pricediscount_level = get_discount_level()final_price = discounted_price(base_price, discount_level)# tobase_price = quantity * item_pricefinal_price = discounted_price(base_price)
6. 類
搬移: 函數/欄位
- 搬移函數: 某個函數與所在類之外的另一個類有更多的交互, 調用或被調用(例如: 使用另一個對象的次數比使用自己所在對象的次數還多). 即, 跟另一個類更相關. 則搬移過去
- 搬移欄位: 某個欄位被其所在類之外的另一個類更多地用到
拆分: 拆分類
某個類做了應該由兩個類做的事. 類太大/太臃腫. 建立一個新類, 將相關欄位和函數從舊類版移到新類
特徵: 類中某些欄位是有關係的整體, 或者有相同的前綴
class Persion(object): def __init__(self, name, age, office_area_code, office_number): self.name = name self.age = age self.office_area_code = office_area_code self.office_number = office_number def get_phone_number(self): return "%s-%s" % (self.office_area_code, self.office_number)# toclass Person(object): def __init__(self, name, age, office_area_code, office_number): self.name = name self.age = age self.phone_number = PhoneNumber(office_area_code, office_number) def get_phone_number(self): return self.phone_number.get_number()class PhoneNumber(object): def __init__(self, area_code ,number): self.area_code = area_code self.number = number def get_number(self): return "%s-%s" % (self.area_code, self.number)
去除
一個類沒有做太多的事情, 不再有獨立存在的理由.
7. 模式
原則:
- 慎用
- 只使用你理解的模式
- 只在符合的業務場景使用對應模式
adapter
你需要為提供服務的類增加功能, 但是你無法修改這個類.
使用組合(推薦, 持有對象)/繼承(加子類), 持有該對象, 增加對應附加功能
adapter思維.
使用場景: 使用一些第三方庫處理外部依賴, 例如依賴一個系統, 業務A(requests)/es(Elasticsearch)/redis(redispy), 但是, 基於第三方系統, 你需要有自己業務相關的統一處理邏輯, 此時, 你可以建立一個XXClient, 持有第三方組件底層調用邏輯, 同時封裝自身業務邏輯, 在上層直接調用
facade
適配模式中舉的例子, 也有facade的思想, 將複雜的東西, 統一封裝, 對外提供相對簡單清晰地介面
template method
出現的次數也很高
裝飾器
python中最常用
其他
根據使用場景, 應用策略/橋樑/工廠/觀察者等等, 具體看業務場景
舉例
重構一個相對較大的django項目
- 明確業務對象, 對象概念, 對象邊界
- 明確分層
- 明確代碼目錄結構, 劃分模塊, 明確每個模塊可以放入的東西
- 粗粒度重構: 移動模塊/類/函數, 根據前幾步的劃分, 將模塊/類/函數等, 移動到對應模塊中, 同時, 修改import和調用點
- 中粒度重構: 根據django項目本身劃分, 移動函數
- 中粒度重構: Extract Method. 讀具體函數代碼, 遇到 重複代碼 / 過長函數 / 過大的類 / 超大的if-else或switch / 包含大段注釋的代碼 等, 思考, 提煉函數, 放入對應模塊
- 細粒度重構: 提取常量 / 提取枚舉 / 修改模塊名類名函數名變數名
舉例:
- 對於django項目, 原則fat models, helper modules, thin views, stupid templates
- fat model, 將對象本身相關的, 盡量放入models, 這個對象相關的, 可以加入補充一系列porperty/classmethod/staticmethod, 可以有效地降低使用這個對象時調用處的代碼複雜度. 例如, 每次取兌現改一個欄位都需要進行轉換, 則搞個property替換每次都需要的轉換邏輯. (找拿到model對象後的處理邏輯代碼中那些反覆出現的, 重複的)
- 將對象查詢相關的, 全部遷移到manager中, 需要先通過Model.objects查詢然後做各種事情的, 遷移放入到manager中
- utils, 將業務邏輯無關的工具函數等, 統一歸入utils模塊中; 將業務有關但多個application共用的utils放入到common.utils模塊中, 而將appication依賴的局部utils, 放入到application.utils中
- constants, 同上, 區分通用, 還是某個applications中使用
- thin view, 業務邏輯, 盡量瘦小簡短
- stupid template, 模板, 盡量傻瓜, 不要包含複雜計算/判斷邏輯, 將複雜遷移到後端代碼
其他
善用工具, 有方案設計評審, 平時通過pull request, 走code review, 有代碼風格自動檢查, 要求單元測試, 走cicd流程. 在平時, 就有意識地控制代碼質量
推薦閱讀:
※2017-2_林奈_《阿勒泰的角落》
※如何不受別人的影響?
※《象與騎象人》讀書筆記·一
※讀書筆記之201703:《傅雷家書》