作者 | 劉俊啓
導讀
在軟件開發中,經常會遇到一些代碼問題,例如邏輯結構複雜、依賴關係混亂、代碼冗餘、不易讀懂的命名等。這些問題可能導致代碼的可維護性下降,增加維護成本,同時也會影響到開發效率。這時通常通過重構的方式對已有代碼結構進行改進和優化。在重構的工作中,大部分的工作是人工的方式完成,是一個耗時且容易出錯的過程。對於研發人員來講,在不改變軟件的功能和行為的前提下,保證質量和效率完成對已有功能的重構,是一個極大的挑戰。本文以Python實現自動化的工具,支持代碼重構過程的實踐。
全文7641字,預計閲讀時間20分鐘。
在軟件開發中,經常會遇到一些代碼問題,例如邏輯結構複雜、依賴關係混亂、代碼冗餘、不易讀懂的命名等。這些問題可能導致代碼的可維護性下降,增加維護成本,同時也會影響到開發效率。
這時通常通過重構的方式,在不改變軟件的功能和行為的前提下,對軟件的代碼進行重新組織和優化。達到增強代碼的可讀性,降低維護成本,提升研發效率和質量的目的。通過合理的重構,可以大大提高軟件的可維護性和可擴展性,從而延長其生命。
本系列的內容介紹了百度App搜索側業務如何使用Python腳本實現自動化工具,以支持百度App配置數據項調用方式升級為數據通路的重構過程。通過Python腳本,我們實現一些自動化的工具,包括配置數據項調用關係分析、配置數據項接入數據通路的實現、數據項使用方接入數據通路的適配等,以期提高工作效率、減少出錯率。
本文為第一篇,將詳細講講配置數據項調用關係分析展現。
01 代碼重構時的關鍵步驟及挑戰
在代碼重構過程中,需要考慮重構的效率和重構後的代碼質量。與其相關的關鍵的步驟如下,這些步驟先後依賴,相互影響:
熟悉業務及技術現狀:在開始重構之前,研發首先要理解業務邏輯和流程,熟悉業務能力及技術實現時存在的問題,確定重構的範圍。
確定重構方案:基於對業務邏輯和現有代碼問題的理解,確定重構方案,重點關注有兩點,有問題的代碼如何重構和依賴於該代碼的調用如何適配。
分階段實施:根據重構方案,分階段的修改代碼,並測試代碼的功能是否正常。在修改過程中,應該儘量避免影響到不相關模塊,這樣可以更好地控制風險。
效果評估及監控:重構方案開發完成,線下對實現的效果進行評估,線上對實現的效果進行監控,及時發現異常止損和重構的效果。
在重構的工作中,大部分的工作是人工的方式完成,是一個耗時且容易出錯的過程。對於研發人員來講,在不改變軟件的功能和行為的前提下,保證質量和效率完成對已有功能的重構,是一個極大的挑戰。
02 百度App(iOS)搜索側的配置數據項重構
為了更好的提升系統穩定性和降低配置數據項變更時對上層依賴方組件的影響, 我們決定對百度App(iOS)搜索側的配置數據項進行重構。重構過程的關鍵節點中有超過80%的工作是由自動化工具完成,支持重構工作上線後零bug,和全部的配置數據項接口內斂,提升了系統的安全性和穩定性。
2.1 重構背景
百度App(iOS)-搜索側的配置數據項,大部分集中在一個類(XXXSetting)中管理。該類(XXXSetting)以獨立組件的方式發佈,被超過30個其它組件依賴。
如圖-1 所示數據項使用模塊直接調用數據項提供模塊(XXXSetting),是直接依賴的關係,數據項的增刪相當於接口的變更,對上層的依賴方會產生影響,當接口存在不兼容變更時,連帶上層的依賴方組件也需要二次的發佈。且該組件中的數據項主要為實驗類開關,變動較為頻繁,影響面也被放大,故需使用比較穩定的方式實現不同模塊之間的數據項共享。
△圖-1
2.2 技術方案
在技術實現的層面,主要分為兩步
1、第一步為實現多模塊之間數據通訊的模塊,在本系列的內容中以數據通路代指該模塊。
2、第二步為基於數據通路提供的能力,XXXSetting組件為作數據提供方接入數據通路,原使用XXXSetting組件的使用方接入數據通路,這樣就完成了XXXSetting組件中的數據項遷移。
數據通路的實現,目標實現以Key-Value的方式讀取及更新配置項,需要從無到有的構建,在本系列的其它章節中內容會有介紹。但XXXSetting組件對應的重構工作,是基於已有的線上能力的改造,Setting中的數據項超過百個,外部的調用點也是以百為計算單位,涉及的組件有30+。影響面如何評估,如何保證重構的過程質量和效果是可控的?結合對重構過程的理解,我們採用了Python腳本來支持第二步的工作。
2.3 使用Python支持重構過程
要規避以人工方式為主的重構過程,引入錯誤的風險,提升重構過程的質量及效率。需要引入Python腳本實現自動化工具支持重構過程的工作。下面以重構的關鍵步驟,自動化工具的應用目標進行列舉。
1、在熟悉業務及技術現狀階段,可以使用自動化工具對工程中現有的代碼、技術架構進行分析,獲取當前需要重構的代碼的依賴和調用關係信息,確定重構過程的變動影響,使用自動化的方式會更加的精準。
2、在確定重構方案階段,可以基於自動化工具產生的數據,支持重構方案的決策,包括是否需要重構,如何重構,調用方如何適配等。
3、在分階段實施階段,可以使用自動化的方式支持代碼的重構工作,包括需要重構的模塊的升級、調用方代碼的適配等。對比IDE提供的查找、替換等基礎工具,自動化工具可以批量處理更加複雜的重構工作。同時實施的階段通常是繁瑣且容易出錯的,但使用自動化的方式可以自動完成這些任務,並減少人為錯誤。
4、在效果評估及監控階段,可以使用自動化的方式對重構前後的代碼進行對比測試保證功能的一致性,收集關鍵指標數據,發現指標的異常。
03 用Python腳本實現模塊的調用關析分析
在實際的配置數據項的調用關係來看,公開的數據項可為幾種情況,對應的重構方案可有不同。
1、配置數據項僅在XXXSetting模塊內使用,這部分數據項不需要接入數據通路。
2、配置數據項在XXXSetting模塊內使用,也在其它的模塊中使用,這類數據項在XXXSetting模塊中維護,數據項需要接入數據通路。
3、配置數據項在XXXSetting模塊內沒有使用,只在一個模塊中使用,這類數據項應該遷移到使用該數據項的模塊中。
4、配置數據項在XXXSetting模塊內沒有使用,但在一個以上模塊中使用,這類數據項可以在XXXSetting模塊中維護,但數據項需要接入數據通路。
基於這樣的改造,XXXSetting模塊的數據項接口就可以全部不公開,對於配置數據項的變更,隻影響依賴配置數據項的模塊。那麼每個數據項的調用應該是如何重構呢,用手動查找及分析的方式成本過高,在項目實際過程評估及修改出錯的概率也會增高,我們使用Python腳本實現了調用關係的分析工具,為重構工作提前進行數據支持及決策。
3.1 提取公開數據項及類型
在分析數據項的外部調用情況之前,需要先提取XXXSetting類中所有公開的數據項。
3.1.1 公開數據項在OC類中的寫法
Setting文件由OC語言開發,在Setting頭文件件中公開的數據項的定義,OC類中成員變量的定義,書寫方式如下
@property (nonatomic, assign) BOOL value;
@property (nonatomic, copy) NSString *value1
3.1.2 提取的是變量類型和變量的名稱
因頭文件中,包含其它非成員變量的代碼,比如include、前置聲明、類定義、空代碼行、註釋、函數等,需要預處理下代碼及使用正則表達式變量定義代碼段,依次的讀取.h文件中的每一行代碼,以相關實現及的關鍵代碼如下。
-
去除註釋
因代碼中的註釋寫法存在不確定性,會對後面的正則匹配產生影響,故先把註釋刪除。
# 原代碼行 @property (nonatomic, copy) NSString *value1; // 註釋 ; * () 這些字符都有可能有,會影響後面的正則判斷
newline = re.sub(r'//.+', "", line)
# 處理過後的代碼行 @property (nonatomic, copy) NSString *value1;
-
提取數據項類型及數據項
去除註釋代碼之後,下一步為提取成員變量名稱及類型,可以使用正則中的分組匹配的能力,提取變量類型及變量名。這裏使用了正則的原因是代碼的寫法存在不確定性,@property的寫法也會因變量類型不同而變化,故通過分組匹配的方式來實現。
# 原代碼行 @property (nonatomic, copy) NSString *value1;
matchObj = re.match(r"@property.+\)\s+(.*)", line, re.M|re.I)
if matchObj:
# matchObj.group(1) 是成員變量類型和變量名 -- NSString *value1;
- 去除無用字符
這時的代碼行,因為寫法的不同及變量的不同,需要進行標準化,才能提取出變量類型及變量名,主要為去除 星號(*)。代碼行頭中的空格已經過濾(上行代碼中的\s+)。
# 原代碼行 NSString *value1;
newline = line.replace('*', '')
# 處理後的代碼行 NSString value1;
- 提取標準化後的數據項類型及數據項
這時代碼行中只剩下類型 空格 變量名 分號,使用正則的分組匹配,提取類型及變量名。
# 原代碼行 NSString value1;
# 正則表達式中\s匹配任何空白字符,包括空格、製表符、換頁符等等, 等價於[ \f\n\r\t\v],\s+代表一個或多個這類的字符
matchObj = re.match(r"(.*)\s+(.*);", line, re.M|re.I)
if matchObj:
# valueType = NSString
valueType = matchObj.group(1)
# valueName = value1
valueName = matchObj.group(2)
到這了一步,公開可訪問的數據項及類型的提取就已級完成,這時就可以轉換代碼,如果這時轉換代碼,會存在冗餘,因為如果公開的變量在其它模塊中沒有使用,那實際上就不需要使用數據通路進行封裝,下一步應該分析調用關係之後,再進行。
3.2 數據項關聯調用組件
確定了公開的數據項之後,需要在工程源碼中查找每個數據項的調用點,之後再跟據調用點數據確定每個數據項在不同的組件中調用的情況。
數據項調用代碼常見於以下寫法,OC中也有其它的寫法,本文中以下寫法作為示例介紹調用關係的生成。
[XXXSetting share].value1
3.2.1. 查找每個數據項在文件中的調用
- 原始數據項調用字串使用數據通路的數據項綁定。
- 整體的思路為,依次的從每個文件中,全字匹配字符串,查找到一次,算作調用一次,保存到字典中,統一輸出到表格中。
# 定義個全局字典,存放每個數據項在不同的文件中調用的次數
# {數據項:{文件名:該文件內數據項調用的次數}}
valueCallInfoDic = {}
# 使用上節中,提取出來的數據項名,拼裝為實際調和時的寫法
realValueName = '[XXXSetting share].' + valueName
# fileNameList 為所有源碼文件(.m 和 .mm)
for fileName in fileNameList:
# 記錄該文件調用數據項的次數
callNum = 0
# 記錄文件每個文件調用該數據項的次數信息
fileCallInfoDic = {}
# 依次的讀取源文件的每一行,匹配調用情況,記錄調用次數,及文件名,line 為代碼行
for line in f:
# 使用正則全字匹配,查找替換
regAbKey = realValueName.replace('[', '\[')
regAbKey = regAbKey.replace(']', '\]')
regAbKey = regAbKey.replace('.', '\.')
# pattern = \[XXXSetting share\]\.value1\b 主要為了防止數據項名有子串的情況
pattern = r'' + fromstr + r'\b'
matchObj = re.match(r'.*' + regAbKey +'', line, re.M|re.I)
if matchObj:
callNum = callNum + 1
if callNum > 0
fileCallInfoDic[fileName] = str(callNum)
# 如果有調用關係,則存儲
if len(fileCallInfoDic)
valueCallInfoDic[valueName] = fileCallInfoDic
3.3 輸出為excel表格文件
使用Python分析的數據還是以機器語言的形式表式,需要以人類語言描述,將數據輸出為excel表格,這樣就可以藉助於表格工具進行數據的查看及分析。
3.3.1 數據項的詳細使用情況輸出
表格的輸出Python沒有使用有excel操作的相關庫,使用 ,(逗號)作為分隔符,存儲為.csv文件,在excel中導入csv文件使用。
具體的實現為依次的將每個數據項的使用的組件,使用的文件及在這個文件文件中使用次數,輸出到.csv文件中。
# 表頭分別為,數據項,使用的組件,使用的文件,文件中使用次數
outfiledata = 'value , uselib , usefile , usenum\n'
# 遍歷全局字典valueCallInfoDic,獲取每個數據項 及數據項的調用信息
# {數據項:{文件名:該文件內數據項調用的次數}}
for.valueName , valueInfo in valueCallInfoDic.items():
# 從數據項的調用信息中獲取,文件名和該文件內數據項調用的次數
# {文件名:該文件內數據項調用的次數}
for.fileName , callNum in valueCallInfoDic.items():
outfiledata += valueName + " , "
# libByFile 函數,實現根據文件獲取所在的組件名
outfiledata += libByFile(fileName) + " , "
outfiledata += fileName + " , "
outfiledata += callNum + " \n"
- 表格數據示例
基於輸出的表格數據,可以比較容易的判斷每個數據項的優化影響範圍,下表為表格數據的示例。
△注:表格數據非真實業務場景數據
3.3.2 數據項的預分析統計輸出
基於數據的調用關係數據,確定每個數據項被每個組件使用的情況,並確定重構的方式。
同樣,表格的輸出Python沒有使用有excel操作的相關庫,使用 ,(逗號)作為分隔符,存儲為.csv文件,在excel中導入csv文件使用。
具體的實現為依次的讀取數據項,計算每個數據項被組件的使用情況,並將結果輸出到.csv文件中。
# 表頭分別為 ,數據項 ,使用的組件 ,組件中總使用次數 , 使用類型
outfiledata = 'value , uselib , usenum , usetype \n'
# 遍歷全局字典,獲取每個數據項 及數據項的調用信息
# {數據項:{文件名:該文件內數據項調用的次數}}
for.valueName , valueInfo in valueCallInfoDic.items():
libCallInfo = {}
# 從數據項的調用信息中獲取,文件名和該文件內數據項調用的次數
# {文件名:該文件內數據項調用的次數}
for.fileName , callNum in valueCallInfoDic.items():
# libByFile 函數,實現根據文件獲取所在的組件名
libName = libByFile(fileName)
if libName in libCallInfo:
libCallInfo[libName] = int(libCallInfo[libName]) + int(callNum)
else:
libCallInfo[libName] = callNum
# 每個組件的使用XXXSetting 的數據項情況
hasSelfCall = False
useType = ""
for.libName in libCallInfo:
if libName == "XXXSetting":
hasSelfCall = True
break
if len(libCallInfo) == 1:
if hasSelfCall:
# 配置數據項僅在XXXSetting模塊內使用,這部分數據項不需要接入數據通路。
useType = "selfCall"
else:
# 配置數據項在XXXSetting模塊內沒有使用,只在一個模塊中使用,這類數據項應該遷移到使用該數據項的模塊中。
useType = "otherCall"
else:
if hasSelfCall:
# 配置數據項在XXXSetting模塊內使用,也在其它的模塊中使用,這類數據項在XXXSetting模塊中維護,數據項需要接入數據通路。
useType = "selfAndOtherCall"
else:
# 配置數據項在XXXSetting模塊內沒有使用,但在一個以上模塊中使用,這類數據項可以在XXXSetting模塊中維護,但數據項需要接入數據通路。
useType = "othersCall"
for.libName , libCallNum in libCallInfo.items():
outfiledata += valueName + " , "
outfiledata += libName + " , "
outfiledata += libCallNum + " \n"
- 表格數據示例
基於輸出的表格數據,可以比較容易的判斷每個數據項應該如何整改,下表為表格數據的示例。
△注:表格數據非真實業務場景數據
04 小結
以上的內容,介紹了代碼重構過程的工作及挑戰,同時以Python腳本實現分析模塊的調用關係的統計,基於該腳本,在重構工作開始之前,可以精確統計每個XXXSetting類對外公開的類成員屬性,被其它組件使用的情況。基於統計的數據,可以感知對應的每個成員屬性在App中的使用情況,且可容易的評估XXXSetting數據項重構升級為數據通路工作所帶來的影響。
當這部分工作,使用人工的方式實現,依次查找每個成員屬性的在App中的使用情況及分類記錄,是一件重複性高,出錯概率高的工作。而使用自動化工具,很好的規避了這些問題,且長期可積累。
下一篇我們將介紹介入數據通路的實現和適配,感興趣的同學,可以持續關注。
——END——
推薦閲讀
CVPR2023優秀論文 | AIGC偽造圖像鑑別算法泛化性缺失問題分析
一文搞定專屬碼的設計與開發
AI原生應用速通指南
代碼理解技術應用實踐介紹
百度交易中台之內容分潤結算系統架構淺析