作者 | 劉俊啓
導讀
在軟件開發中,經常會遇到一些代碼問題,例如邏輯結構複雜、依賴關係混亂、代碼冗餘、不易讀懂的命名等。這些問題可能導致代碼的可維護性下降,增加維護成本,同時也會影響到開發效率。這時通常通過重構的方式對已有代碼結構進行改進和優化。在重構的工作中,大部分的工作是人工的方式完成,是一個耗時且容易出錯的過程。對於研發人員來講,在不改變軟件的功能和行為的前提下,保證質量和效率完成對已有功能的重構,是一個極大的挑戰。本系列以Python實現自動化的工具,支持代碼重構過程的實踐。
在第一篇《通過Python腳本支持OC代碼重構實踐(一):模塊調用關係分析》的內容中,介紹了使用Python實現模塊調用關係的分析,確定了調用數據項的代碼塊超過了600處,如圖-1所示,這些調用點分佈在不同的組件中,是直接調用的關係。
△圖-1
在第二篇《通過Python腳本支持OC代碼重構實踐(二):數據項提供模塊接入數據通路的代碼生成》的內容中,重點介紹了使用Python實現了數據項提供模塊接入數據通路時,公開數據項相關的代碼生成(圖-2中的紅框部分),這時數據項讀寫由原來的直接讀寫方式改為通過數據通路的間接讀寫方式。
△圖-2
當數據項提供模塊接入到數據通路後,數據項使用模塊需要進行重構,以符合數據通路的標準。重構涉及到600多處調用代碼段的適配(圖-3中的紅框部分),手工重構方式成本高、出錯概率高,並且在測試時需要逐項驗證,成本也很高。為了解決這個問題,我們使用Python腳本實現了與數據通路的通訊代碼的生成,可自動的為每個數據項封裝讀寫函數,和自動將原有的代碼調用替換為升級後的代碼調用,支持不同數據項的升級。這樣做實現了本次重構工作在測試及上線階段零 Bug。
△圖-3
本篇內容闡述如何利用Python編寫的自動化工具,實現將原數據項使用模塊中直接對數據項提供模塊中數據項的讀寫方式,升級為通過數據通路間接讀寫。包括每個數據項讀寫類的封裝和數據項使用模塊的調用代碼段適配。
01 數據項讀寫類封裝
為了降低數據項的讀寫調用代碼的重構成本,在數據項使用模塊中創建一個封裝類。每個數據項的讀寫創建一個靜態函數來實現,可被數據項使用模塊中的數據項讀寫類使用。由於需要使用Python腳本實現工具,因此需要有明確的生成規則,以便工具的實現。規則如下:
1、數據項的讀取操作,函數返回類型,函數名,均與與數據項相同。
- 如:NSStringvalue1; 需要轉為 +(NSString )value1,包含函數定義及實現。
2、數據項的更新操作,set_ + 函數名:數據類型,均與數據項相同。
- 如:NSStringvalue1; 需要轉為 +(void)set_value1:(NSString)value,包含函數定義及實現。注意:參數名均為value
1.1 數據項讀取能力封裝
基於數據項讀取操作生成規則,分別實現函數頭、函數聲明、函數體,分別輸出至.m和.h文件。
- 函數頭及函數聲明實現示例
# 原代碼行示例 NSString value1;
matchObj = re.match(r"(.*)\s+(.*);", line, re.M|re.I)
if matchObj:
valuetype = matchObj.group(1)
valuename = matchObj.group(2)
# 因不同類型修飾的方式不同,在getReadFunReturnType進行類型映射;如NSString :NSString *
funname = '+ (' + getReadFunReturnType(valuetype) + ')' + valuename
# 函數聲明 .h文件 + (NSString *)value1;\n\n
hfunname = funname + ';\n\n'
# 函數定義 .m文件 + (NSString *)value1 {\n
mfunname = funname + ' {\n'
- 函數體示例,每個數據項跟據key與數據通路通信,讀取數據項
# 定義返回類型的變量,並賦值,代碼行為
# funbody為: NSString *res = [DataChannelReaderxxx
funbody = ' ' + getReadFunReturnValueType(valuetype) + 'res = [DataChannelReaderxxx '
# 不同類型的數據,數據通路提供的讀取的函數不同,由getReadFunName函數中映射,如:NSString :stringForKey
# funbody 為 NSString *res = [DataChannelReaderxxx stringForKey:@"
funbody += getReadFunName(valuetype) + ':@\"'
# key,類名_數據項名 className_value1
key = className + '_' + valuename
# funbody 為 NSString *res = [DataChannelReaderxxx stringForKey:@"className_value1"];\n
funbody += key + '\"];\n'
# 函數實現完成
funbody += ' return res;\n}\n\n'
- 分別存到.m文件和.h文件
# 函數數頭 .m文件
file_data += mfunname
# 函數體 .m文件
file_data += funbody
# 函數定義 .h文件
hfile_data += hfunname
- 文件生成:默認以XXXSettingReader作為文件名及類名作為前輟,XXX為使用方模塊名稱,這樣就比較清楚,是那個模塊中的數據項讀取能力封裝。
1.2 數據項更新能力封裝
基於數據項更新操作生成規則,分別實現函數頭、函數體,及.m和.h文件
- 函數頭及函數聲明實現示例
# 原代碼行示例 NSString value1;
matchObj = re.match(r"(.*)\s+(.*);", line, re.M|re.I)
if matchObj:
valuename = matchObj.group(2)
valuetype = matchObj.group(1)
# funname為: + (void)set_value1
funname = '+ (void)set_' + valuename
# 因不同類型修飾的方式不同,在getValueType進行類型映射;如NSString :NSString *
# funname為:+ (void)set_value1:(NSString *)value
funname += ':(' + getValueType(valuetype) + ')value'
# 函數聲明 .h文件 + (void)set_value1:(NSString *)value;\n\n
hfunname = funname + ';\n\n'
# 函數定義 .m文件 + (void)set_value1:(NSString *)value {\n
mfunname = funname + ' {\n'
- 函數體示例,每個數據項跟據key與數據通路通信,更新數據項
# 不同類型的數據,數據通路提供的更新的函數不同,在getUpdateFunName函數中映射,如:NSString :updateString
# funbody 為 [DataChannelWriterxxx updateString:value
funbody = ' [DataChannelWriterxxx ' + getUpdateFunName(valuetype) + ':value '
# key,類名_數據項名 className_value1
key = className + '_' + valuename
# funbody 為 [DataChannelWriterxxx updateString:value forKey:@"className_value1"];\n
funbody += 'forKey:@\"' + key + '\"];\n'
# 函數實現完成
funbody += ' }\n\n'
- 分別存到.m文件和.h文件
# 函數數頭 .m文件
file_data += mfunname
# 函數體 .m文件
file_data += funbody
# 函數定義 .h文件
hfile_data += hfunname
- 文件生成:默認以XXXSettingWriter作為文件名及類名作為前輟,XXX為使用方模塊名稱,這樣就比較清楚,是那個模塊中的數據項更新能力封裝。
02 數據項使用模塊調用代碼段適配
當數據提供模塊通過數據通路支持數據項的讀寫,在數據項使用模塊中也需要進行適配。原直接使用數據項,改為使用數據項讀寫類,這部分的代碼使用自動化方式完成。分為兩類,數據項更新調用代碼段適配和數據項讀取調用代碼段適配,因數據項更新和數據項讀取代碼段前輟相似,先執行更新後執行讀取。
2.1 數據項更新調用代碼段適配
2.1.1 代碼轉換OC代碼示例
數據項讀取的更改的主要思路為字符串匹配,查找替換。依次的拼裝每個數據項字串,再替換成每個數據項升級之後的寫法,如:
[XXXSetting share].value1 = @"str" => [XXXSettingWriter set_value1:@"str"]
2.1.2 關鍵的代碼實現
- 原始數據項調用字串使用數據通路的數據項綁定
# 定義個全局的字典
allwritepubvalue = {}
# 原代碼行示例 NSString value1;
matchObj = re.match(r"(.*)\s+(.*);", line, re.M|re.I)
if matchObj:
# valuename = value1
valuename = matchObj.group(2)
# key = [XXXSetting share].value1
key = '[XXXSetting share].' + valuename
# value = SettingWriter set_value1:,不同的模塊前面加上[XXX ,後面加運算符右側
value = ' XXXSettingWriter set_' + valuename + ':'
# 賦值 key = [XXXSetting share].value1,value = SettingWriter set_value1:
allwritepubvalue[key] = value
- 查找原調用方式,升級為數據通路的讀取方式
# 獲取當前工程中,所有源碼文件及對應的組件名
allfileandlib = {}
# allpubvalue 全局變量,字典
for key, value in allwritepubvalue.items():
# filename為文件名,libname為組件名
for filename, libname in allfileandlib.items():
# 當libname為XXX replacevalue = [XXXSettingWriter set_value1:
replacevalue = '[' + libname + value
# 實現個函數 重寫這個文件 將文件中 [XXXSetting share].value1 = YYY 替換為 [XXXSettingReader set_value1:YYY]
reWriteFile(filename, key, replacevalue)
- 文件重寫函數實現,需要實現全字的匹配,避免數據中存在相互為子串的情況。
# 定義一個輸出的數據,初始為空字串
outfiledata = ''
# 使用正則全字匹配,查找替換
regAbKey = fromstr.replace('[', '\[')
regAbKey = regAbKey.replace(']', '\]')
regAbKey = regAbKey.replace('.', '\.')
# pattern 為 .*\[XXXSetting share\]\.value1\s*=\s*([a-zA-Z0-9_\[\]\s\.]+),為了匹配賦值字串,但沒有考慮運算符右側有運算符的情況
pattern = r'.*' + regAbKey + '\s*=\s*([a-zA-Z0-9_\[\]\s\.]+)'
# 依次從文件中讀,正則全字查找及規換
for line in f:
matchObj = re.match(pattern, line, re.M|re.I)
if matchObj:
# 代碼中真實的寫法,去掉前面的一些代碼,比如 [XXXSetting share].value1 = YYY,變為[XXXSetting share].value1 = YYY
eqcode = re.sub(r'.*' + regAbKey, fromstr, matchObj.group())
# 如原代碼為 [XXXSetting share].value1 = YYY ,則 matchObj.group(1) 為YYY
# 把 [XXXSetting share].value1 = YYY 替換為 [XXXSettingWriter set_value1:YYY]
newline = line.replace(eqcode, tosrt + matchObj.group(1) +']')
outfiledata += newline
2.2 數據項讀取調用代碼段適配
2.2.1 代碼轉換OC代碼示例
數據項讀取的更改的主要思路為字符串匹配,查找替換。依次的拼裝每個數據項字串,再替換成每個數據項升級之後的寫法,如:
[XXXSetting share].value1 => [XXXSettingReader value1]
2.2.2 關鍵的代碼實現
- 原始數據項調用字串使用數據通路的數據項綁定
# 定義個全局的字典
allreadpubvalue = {}
# 原代碼行 NSString value1; 4.1生成的類型及變量名
matchObj = re.match(r"(.*)\s+(.*);", line, re.M|re.I)
if matchObj:
# valuename = value1
valuename = matchObj.group(2)
# key = [XXXSetting share].value1
key = '[XXXSetting share].' + valuename
# value = SettingReader value1] ,不同的模塊再加上[XXX
value = 'SettingReader ' + valuename + ']'
# 賦值 key = [XXXSetting share].value1,value = SettingReader value1]
allreadpubvalue[key] = value
- 查找原調用方式,升級為數據通路的讀取方式
# 獲取當前工程中,所有源碼文件及對應的組件名
allfileandlib = {}
# allpubvalue 全局變量,字典
for key, value in allreadpubvalue.items():
# filename為文件名,libname為組件名
for filename, libname in allfileandlib.items():
# 當libname為XXX replacevalue = [XXXSettingReader value1]
replacevalue = '[' + libname + value
# 實現個函數 重寫這個文件 將文件中 [XXXSetting share].value1 替換為 [XXXSettingReader value1]
reWriteFile(filename, key, replacevalue)
- 文件重寫函數實現,需要實現全字的匹配,避免數據中存在相互為子串的情況。
# 定義一個輸出的數據,初始為空字串
outfiledata = ''
# 使用正則全字匹配,查找替換
regAbKey = fromstr.replace('[', '\[')
regAbKey = regAbKey.replace(']', '\]')
regAbKey = regAbKey.replace('.', '\.')
# \[XXXSetting share\]\.value1\b
pattern = r'' + fromstr + r'\b'
# 依次從文件中讀,正則全字查找及替換
for line in f:
newline = re.sub(pattern, tosrt , line)
outfiledata += newline
03 小結
本篇是本系列的最後一篇,在本系列第一篇內容中介紹了通過Python腳本實現公開接口及調用關係的分析,用來支持重構工作量及影響面的評估。在第二擴篇內容中,介紹了通過Python腳本實現數據項提供模塊接入數據通路的代碼轉換。
本篇內容介紹使用 Python 編寫自動化工具,實現了將原數據項使用模塊中直接對數據項提供模塊中數據項的讀寫方式,升級為通過數據通路間接讀寫。包括每個數據項讀寫類的封裝和數據項使用模塊的調用代碼段適配。通過封裝每個數據項的讀寫類,併為每個數據項封裝了獨立的讀寫函數,和對原有調用代碼的自動替換,這些工作是IDE提供的相關工具不可支持及定製的,基於Python編寫的自動化工具,降低了重構成本,並在測試及上線階段實現了零 Bug。
歡迎加入百度搜索大前端團隊,持續招聘 iOS/Android/Web前端 研發工程師。簡歷歡迎投遞至joinefe@baidu.com
——END——
推薦閲讀
百度搜索深度學習模型業務及優化實踐
文生圖大型實踐:揭秘百度搜索AIGC繪畫工具的背後故事!
大模型在代碼缺陷檢測領域的應用實踐
通過Python腳本支持OC代碼重構實踐(二):數據項提供模塊接入數據通路的代碼生成
對話InfoQ,聊聊百度開源高性能檢索引擎 Puck