往期回顧
TALK 6:編程的技術(shù)|藝術(shù)|術(shù)術(shù)(上篇)骨灰級(jí)程序員的心路歷程
前面兩篇里,骨灰級(jí)程序員梁峻墅給大家介紹了他的心路歷程,他談了程序員文化和武林文化的理解,將編程與孫子兵法對(duì)照,闡釋編程的藝術(shù)性表達(dá)以及哲學(xué)思考。本篇將不再務(wù)虛,而是直接上代碼,讓梁老師帶著你解讀牛逼代碼的高明之處。
務(wù)虛的事都講完了,現(xiàn)在得真的要講講務(wù)實(shí)的事了。前面講的那些是武功秘籍的目錄,而真正的武功秘籍在代碼里。實(shí)踐出真知,只有虛實(shí)結(jié)合,才能感同身受。
我找了一段Zero-Day(編者注:下文簡(jiǎn)稱0day)組織幾乎每個(gè)程序都要用到的一段代碼作為示例。
0day,用過(guò)盜版軟件的朋友應(yīng)該都很熟悉,它是全球最牛B的盜版組織,里面高手如云,都是Richard Stallman的追隨者。任何一個(gè)被他們盯上的大廠軟件,只要敢早上發(fā)布,中午的發(fā)布會(huì)招待宴還沒吃完,破解版就已經(jīng)在各大盜版網(wǎng)站上可以下載了,平均破解時(shí)間就是兩三個(gè)小時(shí),承諾破解時(shí)間不超過(guò)24小時(shí),所以叫0day,當(dāng)天解決,童叟無(wú)欺。我們就來(lái)看看這些全球頂尖黑客是怎么寫代碼的。
我找的這段代碼的功能很簡(jiǎn)單,就是一個(gè)基于文件的記錄日志類,其C++版本加上頭文件,總代碼行數(shù)不超過(guò)200行,而核心代碼不到100行,但就在這方寸之間,隱藏著十一個(gè)戰(zhàn)術(shù)思想,三個(gè)戰(zhàn)略思想,還有三個(gè)核彈級(jí)思想。就是個(gè)日志文件功能,如果是你設(shè)計(jì),能有什么想法?而往往是簡(jiǎn)單中蘊(yùn)含的偉大,才能更加讓人震撼?,F(xiàn)在咱們就按圖索驥,開始一段與頂尖高手同行的代碼探險(xiǎn)之旅。
這段代碼是個(gè)標(biāo)準(zhǔn)的C++類,為方便演示,我使用的是其Windows平臺(tái)的版本,此類可以在所有Visual Studio的C++應(yīng)用中使用,就兩個(gè)文件:LogFile.h和LogFile.cpp。
可以先總覽一下:
LogFile.h
// Log.h: interface for the CLog class.
//
//////////////////////////////////////////////////////////////////////
class CLogFile {
public:
CLogFile(LPCTSTR pszPathName4User = _T(""));
virtual ~CLogFile();
bool Record(LPCTSTR pszFormat, ...);
bool SetPathName4Host(LPCTSTR pszPathName4Host);
bool SetPathName4User(LPCTSTR pszPathName4User);
bool SetFileName4Host(LPCTSTR pszFileName4Host);
bool SetFileName4User(LPCTSTR szFileName4User);
bool SetHeader(LPCTSTR szHeader);
LPCTSTR GetPathName4Host();
LPCTSTR GetPathName4User();
LPCTSTR GetFileName4Host();
LPCTSTR GetFileName4User();
LPCTSTR GetPathName();
LPCTSTR GetFileNameFullPath();
protected:
SYSTEMTIME m_tSystemTime;
TCHAR m_szPathName4Host[MAX_PATH + 1];
TCHAR m_szPathName4User[MAX_PATH + 1];
TCHAR m_szFileName4Host[MAX_PATH + 1];
TCHAR m_szFileName4User[MAX_PATH + 1];
TCHAR m_szPathName[MAX_PATH + 1];
TCHAR m_szFileNameFullPath[MAX_PATH + 1];
TCHAR m_szHeader[MAX_LENGTH_CONTENT_PER_LINE + 1];
TCHAR m_szLine[MAX_LENGTH_CONTENT_PER_LINE + 1];
bool IsPathOrFileExist(LPCTSTR pszPathOrFileName);
bool BuildFilePath();
bool BuildPathAndFilePath();
};
// Log.cpp: implementation of the CLog class.
//
//////////////////////////////////////////////////////////////////////
CLogFile::CLogFile(LPCTSTR pszPathName4User) {
ZERO_MEMORY(m_szPathName4Host);
ZERO_MEMORY(m_szPathName4User);
ZERO_MEMORY(m_szFileName4Host);
ZERO_MEMORY(m_szFileName4User);
ZERO_MEMORY(m_szPathName);
ZERO_MEMORY(m_szFileNameFullPath);
ZERO_MEMORY(m_szLine);
ZERO_MEMORY(m_szHeader);
_tcscpy_s(m_szHeader, MAX_LENGTH_CONTENT_PER_LINE, _T("F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15"));
::GetModuleFileName(NULL, m_szFileNameFullPath, MAX_PATH);
_tsplitpath(m_szFileNameFullPath, m_szPathName4Host, m_szPathName, m_szFileName4Host, NULL);
_tcscat_s(m_szPathName4Host, MAX_PATH, m_szPathName);
if (pszPathName4User) {
if (*pszPathName4User) {
_tcscpy_s(m_szPathName4User, MAX_PATH, pszPathName4User);
}
else {
*m_szPathName4User = _T('.');
_tcscat_s(m_szPathName4User, MAX_PATH, m_szFileName4Host);
}
}
BuildPathAndFilePath();
}
CLogFile::~CLogFile() {
}
bool CLogFile::BuildFilePath() {
GetLocalTime(&m_tSystemTime);
int iReturn = _sntprintf(m_szFileNameFullPath, MAX_PATH, _T("%s%s.%s.%04d%02d%02d.%p.txt"),
m_szPathName, m_szFileName4Host, m_szFileName4User,
m_tSystemTime.wYear, m_tSystemTime.wMonth, m_tSystemTime.wDay, this);
return 0 < iReturn;
}
bool CLogFile::BuildPathAndFilePath() {
bool bReturn = 0 < _sntprintf(m_szPathName, MAX_PATH, _T("%s%s"), m_szPathName4Host, m_szPathName4User);
if (bReturn) {
bReturn = BuildFilePath();
}
return bReturn;
}
bool CLogFile::Record(LPCTSTR pszFormat, ...) {
int iReturn = 0;
FILE* pFile = NULL;
do {
if (!IsPathOrFileExist(m_szPathName)) {
break;
}
if (!BuildFilePath()) {
break;
}
bool bIsNew = !IsPathOrFileExist(m_szFileNameFullPath);
pFile = _tfopen(m_szFileNameFullPath, _T("a"));
if (!pFile) {
break;
}
if (bIsNew) {
iReturn = _ftprintf(pFile, _T("Time User %s
"), m_szHeader);
if (0 >= iReturn) {
break;
}
}
va_list vlArgs;
va_start(vlArgs, pszFormat);
iReturn = _vsntprintf(m_szLine, MAX_LENGTH_CONTENT_PER_LINE, pszFormat, vlArgs);
va_end(vlArgs);
if (0 > iReturn) {
break;
}
iReturn = _ftprintf(pFile, _T("%02d:%02d:%02d.%03d %s %s
"), m_tSystemTime.wHour,
m_tSystemTime.wMinute, m_tSystemTime.wSecond, m_tSystemTime.wMilliseconds,
m_szFileName4User, m_szLine);
} while (false);
if (pFile) {
fclose(pFile);
}
return 0 < iReturn;
}
bool CLogFile::IsPathOrFileExist(LPCTSTR pszPathOrFileName) {
return (0 == _taccess(pszPathOrFileName, 0));
}
bool CLogFile::SetPathName4Host(LPCTSTR pszPathName4Host) {
_tcsncpy(m_szPathName4Host, pszPathName4Host, MAX_PATH);
return BuildPathAndFilePath();
}
bool CLogFile::SetPathName4User(LPCTSTR pszPathName4User) {
_tcsncpy(m_szPathName4User, pszPathName4User, MAX_PATH);
return BuildPathAndFilePath();
}
bool CLogFile::SetFileName4Host(LPCTSTR pszFileName4Host) {
_tcsncpy(m_szFileName4Host, pszFileName4Host, MAX_PATH);
return BuildFilePath();
}
bool CLogFile::SetFileName4User(LPCTSTR szFileName4User) {
_tcsncpy(m_szFileName4User, szFileName4User, MAX_PATH);
return BuildFilePath();
}
bool CLogFile::SetHeader(LPCTSTR szHeader) {
_tcsncpy(m_szHeader, szHeader, MAX_PATH);
return true;
}
LPCTSTR CLogFile::GetPathName4Host() {
return m_szPathName4Host;
}
LPCTSTR CLogFile::GetPathName4User() {
return m_szPathName4User;
}
LPCTSTR CLogFile::GetFileName4Host() {
return m_szFileName4Host;
}
LPCTSTR CLogFile::GetFileName4User() {
return m_szFileName4User;
}
LPCTSTR CLogFile::GetPathName() {
return m_szPathName;
}
LPCTSTR CLogFile::GetFileNameFullPath() {
return m_szFileNameFullPath;
}
先從戰(zhàn)術(shù)思想談起,有十一個(gè),容我逐一道來(lái)。
戰(zhàn)術(shù)思想一:全名命名規(guī)則
代碼的第一眼感覺,沒有注釋!這幫高手果然很清高啊,他們不會(huì)幫助你看懂,因?yàn)檫@個(gè)世界上總有人不配看懂。只能沉下心來(lái),自力更生了。再仔細(xì)看代碼,會(huì)發(fā)現(xiàn)代碼中的變量名、方法名都很長(zhǎng)。第一個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:全名命名規(guī)則。命名使用單詞全名,而很多程序員喜歡使用縮寫,而縮寫并不一定能與所有人達(dá)成共識(shí),導(dǎo)致命名的意義大打折扣。良好的命名可以代替注釋,且效率更高。微軟的函數(shù)命名平均長(zhǎng)度是13個(gè)字母,而0day代碼中的命名平均長(zhǎng)度是16.8個(gè)字母,超過(guò)微軟水準(zhǔn)將近30%??梢哉f(shuō),命名平均長(zhǎng)度能夠作為代碼段位的參考之一。
戰(zhàn)術(shù)思想二:前綴命名規(guī)則
再仔細(xì)看每一個(gè)命名,第二個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:前綴命名規(guī)則。所有的變量名都有類型前綴,如字符串變量的前綴是“sz”,字符串指針變量的前綴是“psz” ,整型(int)變量的前綴是“i” ,布爾型(bool)變量的前綴是“b”,這些類型前綴雖然使用了縮寫,但這些縮寫都是C/C++程序員所共識(shí)的。還有,所有的類成員級(jí)變量在類型前綴前再加上“m_”前綴指示作用域,m是member的縮寫,其實(shí)還有一個(gè)“g_”前綴代表全局作用域,但全局變量只有在C代碼中很常見,而在C++代碼中幾乎從不使用。
戰(zhàn)術(shù)思想三:名詞前置命名規(guī)則
再再仔細(xì)看每一個(gè)命名,第三個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:名詞前置命名規(guī)則。例如類成員字符串變量“宿主路徑名”命名為m_szPathName4Host,“用戶路徑名”命名為m_szPathName4User,如果按人類正常思維應(yīng)該是m_szHostPathName和m_szUserPathName才對(duì),但他們卻名詞前置,形容詞動(dòng)詞后置。其目的是為了給相關(guān)命名進(jìn)行分類,最早是為了能在代碼統(tǒng)計(jì)工具的報(bào)告中,能把相關(guān)命名在α排序中排在一起,以便進(jìn)行代碼分析;而在后來(lái)的現(xiàn)代IDE的代碼編輯器中,都有自動(dòng)完成功能,根據(jù)輸入的部分字母自動(dòng)提示可能的輸入,按名詞前置命名規(guī)則,提示內(nèi)容將把相關(guān)命名排在一起,便于程序員選擇。如鍵入“m_szP”,將提示出m_szPathName4Host和m_szPathName4User,方便程序員在使用相關(guān)變量或方法時(shí)提高效率。
戰(zhàn)術(shù)思想四:介詞縮寫命名規(guī)則
在上面提到的命名中,都有一個(gè)阿拉伯?dāng)?shù)字4,這是什么鬼?第四個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:介詞縮寫命名規(guī)則。用4的英文諧音代替介詞“for”,原命名應(yīng)為m_szPathNameForHost,介詞作為前置命名分類與后置形容詞、動(dòng)詞的分界線被大量使用,為節(jié)約鍵擊次數(shù)而在組織內(nèi)約定的縮寫。類似還有2,諧音英文的“to”,因?yàn)樵诔绦蛑懈鞣N轉(zhuǎn)換也非常多,如BinToHex(二進(jìn)制轉(zhuǎn)十六進(jìn)制),可以縮寫為Bin2Hex。這可以理解為長(zhǎng)命名思想與少鍵擊思想的辯證統(tǒng)一。
戰(zhàn)術(shù)思想五:對(duì)稱命名規(guī)則
看上面的成員變量和對(duì)應(yīng)的設(shè)置方法名和獲取方法名,第五個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:對(duì)稱命名規(guī)則。如此整齊劃一的命名,不但能幫助閱讀者在沒有注釋的情況下快速理解各方法的意圖,還能讓使用者無(wú)需翻看源碼就能準(zhǔn)確調(diào)用。
大家看看,一個(gè)小小的命名,已經(jīng)是殺機(jī)四伏,下足了功夫。十一個(gè)戰(zhàn)術(shù)思想接近一半,都是在談命名。就是因?yàn)?strong>命名是代碼的基石,它是多米諾骨牌效應(yīng)里的第一塊骨牌,每塊磚不做好,將會(huì)影響整個(gè)大廈的安危。這些命名規(guī)則的終極目標(biāo)都是為了用空間換時(shí)間。在你的每一次鍵擊中,每個(gè)思想可能只為你節(jié)約了0.1秒,但經(jīng)不住長(zhǎng)年累月的積累,你的有效編程時(shí)間就是比別人多,還沒開始比賽,你就已經(jīng)勝過(guò)了。
戰(zhàn)術(shù)思想六:使用制表符縮進(jìn)
代碼中還有一個(gè)不易察覺的細(xì)節(jié),其代碼縮進(jìn)使用的是制表符(TAB鍵),第六個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:使用制表符縮進(jìn)。關(guān)于縮進(jìn)使用制表符還是空格,業(yè)界一直爭(zhēng)論不斷,且沒什么定論,主要原因就是覺得這是個(gè)小問題,無(wú)傷大雅,大家隨意,開心就好。但這些頂尖高手只用制表符,原因很暖心,僅僅是為了尊重同行!制表符最早出現(xiàn)是為了控制打印機(jī)在打印時(shí)的左邊距,當(dāng)時(shí)定義為8個(gè)空格,可視化編程出現(xiàn)后才用于代碼縮進(jìn),但當(dāng)時(shí)顯示器的分辨率是320*200,一行最多顯示80個(gè)字符,這8個(gè)空格實(shí)在是太長(zhǎng)了,于是就在編輯器中定義為4個(gè)空格,但后來(lái)有人覺得2個(gè)才好,還有人覺得1個(gè)更好,最后干脆作為編輯器配置項(xiàng),根據(jù)喜好自定義吧。所以使用制表符縮進(jìn)的代碼在編輯器中的顯示樣式將會(huì)符合當(dāng)前使用者的習(xí)慣,而使用空格縮進(jìn)的代碼將可能會(huì)導(dǎo)致當(dāng)前使用者不適。多么細(xì)致的人文關(guān)懷,面向人性編程,面向開發(fā)者編程,時(shí)刻謹(jǐn)記。
戰(zhàn)術(shù)思想七:調(diào)用必須有返回值
觀察代碼中的每一個(gè)方法,發(fā)現(xiàn)都有返回值,哪怕是返回固定值!
第七個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:調(diào)用必須有返回值。絕大多數(shù)編程語(yǔ)言都允許調(diào)用沒有返回值,但這幫頂級(jí)精英為什么在可以用這個(gè)規(guī)則的情況下還是不用呢?這就是接口的藝術(shù),為了向下兼容,未雨綢繆,面向未來(lái)編程!因?yàn)檎l(shuí)也無(wú)法預(yù)測(cè),隨著代碼的不斷迭代,這個(gè)方法的使用條件可能會(huì)發(fā)生變化,而有返回值的調(diào)用是可以兼容沒有返回值的調(diào)用的,這樣可保持接口的歷史一致性,進(jìn)退自如。這樣的設(shè)計(jì)一旦在public調(diào)用中發(fā)揮過(guò)一次作用,可就不是節(jié)約0.1秒的事了。
戰(zhàn)術(shù)思想八:減少嵌套深度
上面是Record方法中的一段代碼,使用了一個(gè)do-while循環(huán)語(yǔ)句,但循環(huán)條件是個(gè)固定布爾值false,意味著這個(gè)循環(huán)永遠(yuǎn)只會(huì)執(zhí)行一次,但為什么還要用循環(huán)語(yǔ)句呢?如果不用循環(huán)語(yǔ)句,正常的寫法應(yīng)該是這樣的:
第八個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:減少嵌套深度。嵌套深度決定了人類大腦的思考深度,而思考深度則決定了消耗的能量和思考的難度。所以嵌套深度較低的代碼,讓人思考起來(lái)會(huì)比較輕松且不易出錯(cuò),而重度嵌套的代碼則更容易讓人疲倦且增加產(chǎn)生bug的幾率。
戰(zhàn)術(shù)思想九:辯證使用goto
這段代碼使用do-while循環(huán)語(yǔ)句的結(jié)構(gòu),并配合break語(yǔ)句來(lái)減少邏輯嵌套。第九個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:辯證使用goto。break語(yǔ)句的本質(zhì)是goto語(yǔ)句,只是受限而已。而goto語(yǔ)句在早期面向過(guò)程編程的時(shí)代,由于其高效的操作效率而被濫用,把代碼寫的像面條一樣,扯不清,理還亂,這導(dǎo)致了上世紀(jì)60年代的軟件危機(jī),并最終引發(fā)了軟件工程革命。在面向?qū)ο缶幊痰臅r(shí)代,業(yè)界統(tǒng)一的共識(shí)是禁止使用goto。但goto語(yǔ)句的操作效率確實(shí)很高,所以善用break這種閹割版goto可以起到魚與熊掌兼得的效果。
戰(zhàn)術(shù)思想十:同一函數(shù)代碼不要跨屏
觀察Record方法的代碼行數(shù)達(dá)到36行,但業(yè)界一般的說(shuō)法是每個(gè)函數(shù)的代碼行數(shù)不要超過(guò)30行,理由是人類的腦容量問題。但0day的判斷標(biāo)準(zhǔn)是,第十個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:同一函數(shù)代碼不要跨屏。只要任意函數(shù)的所有代碼在當(dāng)前流行屏幕尺寸大小下能夠完全顯示即可。理由是只要整個(gè)代碼邏輯在人的靜態(tài)目視范圍之內(nèi),程序員的腦容量都?jí)蛴?。除了靠減少代碼行數(shù)來(lái)防止縱向滾動(dòng)屏幕,前面說(shuō)的減少邏輯嵌套還能防止橫向滾動(dòng)屏幕。代碼邏輯禁止跨屏規(guī)則能在很大程度上降低bug產(chǎn)生的幾率。
再觀察Record方法,發(fā)現(xiàn)一段有趣的代碼:
戰(zhàn)術(shù)思想十一:盡量使用順序代碼結(jié)構(gòu)代替判斷代碼結(jié)構(gòu)
BuildFilePath方法用于構(gòu)造日志文件名,其中使用了系統(tǒng)日期作為文件名的一部分,目的就是把日志文件按天分隔,以防止文件過(guò)大。這意味著每次寫日志,都應(yīng)判斷是否該更換文件名,但這種更換每天只發(fā)生一次。而這段代碼并沒有根據(jù)日期是否更改而構(gòu)造文件名,而是每次都按當(dāng)前日期構(gòu)造文件名,這意味著文件名在一天內(nèi)的調(diào)用中都是重復(fù)構(gòu)造相同的文件名,這不是做無(wú)用功嗎?第十一個(gè)戰(zhàn)術(shù)級(jí)思想浮出水面:盡量使用順序代碼結(jié)構(gòu)代替判斷代碼結(jié)構(gòu)。判斷語(yǔ)句是bug產(chǎn)生的源泉,盡量不要使用,哪怕代碼看上去有點(diǎn)愚蠢。不認(rèn)同的人可以試試,使用判斷語(yǔ)句來(lái)修改這段代碼,讓其看起來(lái)更有效率。當(dāng)你被源源不斷的bug改到懷疑人生時(shí),你才能真切地體會(huì)到這個(gè)思想的精妙之處。對(duì)這個(gè)未知世界,心存敬畏,才能保你福如東海,壽比南山。
前面談到了十一個(gè)戰(zhàn)術(shù)思想,每一個(gè)戰(zhàn)術(shù)思想,可能看過(guò)來(lái)都不復(fù)雜,但偉大往往都藏在細(xì)節(jié)中。十一個(gè)戰(zhàn)術(shù)思想,處處都在體現(xiàn)著:以空間換時(shí)間,面向人性編程,面向開發(fā)者編程,面向未來(lái)編程,以及對(duì)這個(gè)未知世界心存敬畏。接下來(lái)我們談?wù)剳?zhàn)略級(jí)思想。
戰(zhàn)略級(jí)思想一:贈(zèng)人玫瑰,手有余香
再觀察Record方法的定義,使用了非常罕見的不定長(zhǎng)參數(shù),第一個(gè)戰(zhàn)略級(jí)思想橫空出世:贈(zèng)人玫瑰,手有余香。作為一個(gè)日志文件類的主要方法,通常就是把傳入的字符串參數(shù),存儲(chǔ)到日志文件里就好了。為什么要使用一個(gè)非常冷門的技術(shù)?原因就是尊重傳統(tǒng),方便你的同行,讓調(diào)用者更干、更爽、更安心。如果參數(shù)是一個(gè)字符串,則意味著調(diào)用方必須在調(diào)用此方法前,拼裝好字符串:
使用不定長(zhǎng)參數(shù),則可以這樣調(diào)用:
一行搞定!把方便留給別人,把困難留給自己,雷鋒精神時(shí)刻謹(jǐn)記。面向人性編程,面向開發(fā)者編程,面向開源編程。
戰(zhàn)略級(jí)思想二:簡(jiǎn)單通用
通覽整體代碼,系統(tǒng)調(diào)用只使用過(guò)一次Windows API,其余均使用C運(yùn)行時(shí)庫(kù)函數(shù),第二個(gè)戰(zhàn)略級(jí)思想橫空出世:簡(jiǎn)單通用。作為一個(gè)工具類,會(huì)被廣泛使用,包括跨平臺(tái)應(yīng)用。如果使用Windows API,此類要移植到Unix/Linux平臺(tái)上將付出巨大代價(jià)。而C運(yùn)行時(shí)庫(kù)函數(shù)是語(yǔ)言標(biāo)準(zhǔn)而非平臺(tái)標(biāo)準(zhǔn),在功能表現(xiàn)上所有平臺(tái)都是一致的,所以移植成本要低的多。而且代碼中還使用了C運(yùn)行時(shí)庫(kù)函數(shù)的自適應(yīng)字符集宏定義版本,使得此工具類無(wú)論編譯目標(biāo)應(yīng)用是MBCS字符集還是Unicode字符集都無(wú)需修改一行代碼!事實(shí)上,此工具類在組織內(nèi)不但有多平臺(tái)版本,甚至還有多語(yǔ)言版本,包括C#、java、VB等。受益于使用語(yǔ)言標(biāo)準(zhǔn)的設(shè)計(jì)思想,各語(yǔ)言、平臺(tái)版本的代碼一致性很高,產(chǎn)生bug的幾率很小,移植成本非常低。
現(xiàn)在我們正式開始通過(guò)瀏覽代碼來(lái)理解代碼邏輯,先看類構(gòu)造函數(shù):
戰(zhàn)略級(jí)思想三:默認(rèn)值的藝術(shù)
這個(gè)初始化還是相當(dāng)復(fù)雜的,對(duì)關(guān)鍵類成員變量的默認(rèn)值進(jìn)行了規(guī)劃和設(shè)計(jì),第三個(gè)戰(zhàn)略級(jí)思想橫空出世:默認(rèn)值的藝術(shù)。為便于理解程序的設(shè)計(jì)思想,我寫了一個(gè)測(cè)試程序,使用不同的參數(shù)調(diào)用構(gòu)造方法,然后調(diào)用對(duì)應(yīng)的成員變量獲取方法,以查看成員變量的內(nèi)容:
從以上結(jié)果可以看出,構(gòu)造函數(shù)通過(guò)傳遞不同的參數(shù),將成員變量初始化為不同使用理念的數(shù)據(jù)套。目的就是讓調(diào)用者在構(gòu)造完類后,即可使用Record方法開始記錄日志,而無(wú)需任何配置!還是那句老話:把方便留給別人,把麻煩留給自己,雷鋒精神時(shí)刻謹(jǐn)記。
三個(gè)戰(zhàn)略級(jí)思想,以小搏大,已經(jīng)從簡(jiǎn)單到人性上升到墨家的兼愛和利他主義,這其實(shí)就是面向開源的編程思想內(nèi)核。下面我再談三個(gè)核彈級(jí)思想。
核彈級(jí)思想一:即時(shí)熱調(diào)試
繼續(xù)仔細(xì)研讀測(cè)試結(jié)果,可了解代碼初始化意圖:給構(gòu)造函數(shù)傳參空指針(NULL),則日志文件路徑自動(dòng)配置為當(dāng)前可執(zhí)行文件路徑,緊接著調(diào)用Record方法即可產(chǎn)生日志文件,nice!如果給構(gòu)造函數(shù)傳參非空字符串,如示例中是“l(fā)og”,則自動(dòng)配置日志文件路徑為當(dāng)前可執(zhí)行文件路徑后再附加“l(fā)og”路徑,enn…如果傳參是空字符串或不傳任何參數(shù)(這是默認(rèn)情況,應(yīng)該是該類建議的主要使用方式),則自動(dòng)配置日志文件路徑為當(dāng)前可執(zhí)行文件路徑后再附加帶前綴“.”的不包括擴(kuò)展名的可執(zhí)行文件名,what?
代碼是看懂了,但為啥?難道要自動(dòng)創(chuàng)建如此詭異的路徑?但在Record方法中,不但沒有找到創(chuàng)建路徑的方法,還看到了這樣一段代碼:
這段代碼的意思就是當(dāng)日志文件路徑不存在時(shí),將退出Record功能,什么也不干!這個(gè)類的作用不就是記錄日志嗎?居然在某些情況下還不應(yīng)記錄?What the fuck!
第一顆核彈君臨天下:即時(shí)熱調(diào)試。像C/C++這種接近硬件底層的編譯型語(yǔ)言,預(yù)定義有兩種編譯應(yīng)用的形態(tài):debug版本和release版本。debug版本用于在開發(fā)環(huán)境中調(diào)試,尤其是單步調(diào)試功能可以解決硬核的技術(shù)問題,而release版本用于正式發(fā)布,沒有調(diào)試功能。但代碼調(diào)試時(shí),除了技術(shù)問題,還有更大量的業(yè)務(wù)邏輯問題需要調(diào)試。如果使用單步調(diào)試效率太低了,所以絕大多數(shù)C/C++程序員在debug版本中通過(guò)輸出日志調(diào)試業(yè)務(wù)邏輯。這些日志通過(guò)宏定義控制只在debug版本中編譯,而在release版本中忽略,因?yàn)檎桨l(fā)布的軟件不能在用戶方產(chǎn)生大量調(diào)試日志,否則日積月累會(huì)塞滿用戶的存儲(chǔ)空間。但是,誰(shuí)也不能保證在debug版本中能調(diào)試完所有的業(yè)務(wù)邏輯問題,如果在用戶方部署的release版本出錯(cuò),大家束手無(wú)策。在互聯(lián)網(wǎng)發(fā)明以前,這個(gè)問題到也不太重要,因?yàn)榧词乖谟脩舴桨l(fā)現(xiàn)程序錯(cuò)誤,程序員也沒辦法到達(dá)現(xiàn)場(chǎng)解決問題。但現(xiàn)在的互聯(lián)網(wǎng)技術(shù)可以支撐遠(yuǎn)程登錄服務(wù)器或者個(gè)人計(jì)算機(jī),賦予了技術(shù)支持人員可以在任何時(shí)間、任何地點(diǎn)、使用任何設(shè)備到達(dá)錯(cuò)誤現(xiàn)場(chǎng)的能力,但老舊的編譯時(shí)debug和release機(jī)制,在新時(shí)代下也沒什么卵用。0day的精英們與時(shí)俱進(jìn),設(shè)計(jì)了這個(gè)動(dòng)態(tài)debug和release機(jī)制:如果在當(dāng)前可執(zhí)行文件的目錄下,存在一個(gè)特別指定的目錄,則程序進(jìn)入debug狀態(tài),并在那個(gè)目錄下生成日志;否則程序保持release狀態(tài),不輸出日志。牛B的思想閃耀星空!把代碼的debug和release狀態(tài)確認(rèn)由編譯時(shí)后移到運(yùn)行時(shí),這意味著當(dāng)程序發(fā)生業(yè)務(wù)邏輯問題,程序員可直接登錄到現(xiàn)場(chǎng),程序都不用重啟,直接建立指定目錄,即可知道當(dāng)前程序正在干什么,找到問題后,再把目錄一刪,揮一揮衣袖,不帶走一片云彩!
這個(gè)頂級(jí)設(shè)計(jì)還有一些非常貼心的細(xì)節(jié)設(shè)計(jì),第一是關(guān)于那個(gè)指定的目錄。默認(rèn)是不包括擴(kuò)展名的當(dāng)前可執(zhí)行文件名,前面還有一個(gè)“.”。這是為了保持跨平臺(tái)操作的一致性,因?yàn)閁nix/Linux平臺(tái)下的可執(zhí)行文件沒有擴(kuò)展名,如果單純使用當(dāng)前可執(zhí)行文件名,則因?yàn)橹孛鵁o(wú)法創(chuàng)建目錄,所以前面加個(gè)“.”來(lái)保證不重名,還順便成為隱藏目錄,因?yàn)閁nix/Linux的文件系統(tǒng)定義以“.”開頭的目錄或文件具備隱藏屬性。雖然Windows平臺(tái)下不存在這些問題,但0day的絕大多數(shù)精英都是Windows平臺(tái)和Unix/Linux平臺(tái)雙料王牌,經(jīng)常需要在多平臺(tái)間切換工作,為保持操作一致性,只好委屈一下Windows平臺(tái)了。但也無(wú)需焦慮,這個(gè)指定目錄可以在初始化或運(yùn)行時(shí)隨便修改。修改指定目錄還有一個(gè)使用技巧,比如在同一目錄下有A1,A2,A3,B1,B2共5個(gè)應(yīng)用程序,其中A1,A2,A3是有鉤稽關(guān)系的第一組應(yīng)用,B1,B2是有鉤稽關(guān)系的第二組應(yīng)用,可以設(shè)計(jì)為建立A目錄,則在A目錄中同時(shí)產(chǎn)生A1,A2,A3的日志,建立B目錄,則在B目錄中同時(shí)產(chǎn)生B1,B2的日志,達(dá)到相關(guān)應(yīng)用群日志自動(dòng)分組的目的。
第二是關(guān)于日志文件名。整個(gè)文件名分為5個(gè)部分:第一部分是應(yīng)用程序名,這個(gè)很容易理解,一看就知道這個(gè)日志是哪個(gè)應(yīng)用產(chǎn)生的;第二部分是一個(gè)自定義的名字,這個(gè)作用比較硬核,咱門后面再講;第三部分是日志產(chǎn)生的日期,為了防止文件過(guò)大,每個(gè)應(yīng)用程序每天只有一個(gè)日志文件;第四部分比較特殊,是運(yùn)行時(shí)日志文件類實(shí)例的內(nèi)存地址,what?這能干啥用?使用實(shí)例的內(nèi)存地址意味著每次啟動(dòng)這個(gè)類,文件名就會(huì)發(fā)生變化,可用于指示這個(gè)應(yīng)用程序在這個(gè)日期下的不同啟動(dòng)批次。第五部分是固定擴(kuò)展名“txt”,指示系統(tǒng)可用文本編輯器打開此文件。整個(gè)設(shè)計(jì)考慮了使用上的方方面面,盡量讓使用者更方便、更舒適,愛心媽媽,呵護(hù)全家。面向人性編程,面向開發(fā)者編程,面向開源編程。
核彈級(jí)思想二:統(tǒng)計(jì)日志
再看向文件寫入內(nèi)容的代碼中使用制表符“ ”作為輸出內(nèi)容的分隔符,第二顆核彈石破天驚:統(tǒng)計(jì)日志。為了能理解這個(gè)設(shè)計(jì),咱們先看看調(diào)用方是如何使用這個(gè)類的,典型調(diào)用像這樣:
意圖就是把需要輸出的狀態(tài)、數(shù)據(jù),如調(diào)用的方法名、錯(cuò)誤描述等,組織成類似表格字段的方式分隔輸出。使用制表符可以保證用表格軟件打開日志文件或把文本復(fù)制到表格軟件里,效果是這樣的:
在第一個(gè)核彈的淫威下,再加上日志的記錄時(shí)間精確到毫秒的加持,程序員們徹底放開了,寫日志跟不要錢似的,瘋狂輸出,幾乎每個(gè)函數(shù)在返回前都會(huì)把當(dāng)前處理結(jié)果輸出到日志里,面對(duì)這樣的海量日志,用眼睛找bug會(huì)看瞎的。所以創(chuàng)造性的利用表格的相關(guān)排序、分類匯總、透視圖等統(tǒng)計(jì)功能,快準(zhǔn)狠地定位查找目標(biāo)。比如示例那個(gè)日志,用透視圖看是這樣的:
或者是這樣的:
就說(shuō)你想查啥?咋樣都行,就是拖拖拽拽的事。bug往哪里躲?它太難了…羽扇綸巾,談笑間,強(qiáng)擼灰飛煙滅。
核彈級(jí)思想三:調(diào)試多線程
還記得前面講到文件名的第二部分嗎?就是代碼里的成員變量m_szFileName4User,它是干什么用的?看遍代碼的上上下下,也看不出個(gè)所以然。我們對(duì)其賦值“robot”,看看出現(xiàn)啥情況:
日志文件名第二部分變成“robot”,日志文件中user列里面填充“robot”,仍然一頭霧水!第三顆潛射核彈韜跡隱智:調(diào)試多線程。多線程調(diào)試是程序員的噩夢(mèng),因?yàn)槿祟惖拇竽X無(wú)法精確模擬計(jì)算機(jī)多線程的運(yùn)行過(guò)程。所以多線程程序所產(chǎn)生的bug,尤其是無(wú)法必現(xiàn)的bug,常常讓人束手無(wú)策。在前面兩顆核彈的加持下,給解決這個(gè)問題帶來(lái)了希望。如果在日志文件類中加入同步機(jī)制,多個(gè)線程共享同一個(gè)日志文件類實(shí)例,則會(huì)導(dǎo)致多線程程序在調(diào)試狀態(tài)下被強(qiáng)制串行化為單線程程序,由于運(yùn)行環(huán)境的變化很可能觸發(fā)不了那個(gè)多線程bug,所以每個(gè)線程必須單獨(dú)使用各自的日志文件。在分析時(shí),將相關(guān)的所有線程日志全部拷貝到一個(gè)表格文件中,利用排序功能就能知道每個(gè)時(shí)刻,各個(gè)線程都正在干什么。這個(gè)user列就是用于區(qū)分這條記錄來(lái)自于哪個(gè)線程。多線程調(diào)試就這么被輕松地搞定了!說(shuō)了那么多偉大,我都厭倦了:“老婆,快出來(lái)看上帝?!?/p>
核彈級(jí)思想是高手隱藏在編程中的不容易直接悟出來(lái)的彩蛋。但你一旦悟出來(lái),一定會(huì)有醍醐灌頂?shù)臅晨炝芾臁?br>
我已把這段代碼上傳到gitee,訪問地址https://gitee.com/aeye/CTools/,歡迎大家共建,也可應(yīng)用于項(xiàng)目中,給大家在代碼的海洋中探險(xiǎn)時(shí)提供一把趁手的兵器。
最后,希望同學(xué)們也能夠創(chuàng)造出有思想,有靈魂,舉手投足之間都透露出優(yōu)雅的代碼:
<本文完>
原文標(biāo)題:河套IT TALK——TALK 12:編程的技術(shù)|藝術(shù)|術(shù)術(shù) 下篇:對(duì)著代碼解讀編程的哲學(xué)
文章出處:【微信公眾號(hào):開源技術(shù)服務(wù)中心】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
開源技術(shù)
+關(guān)注
關(guān)注
0文章
389瀏覽量
7942 -
OpenHarmony
+關(guān)注
關(guān)注
25文章
3723瀏覽量
16340
原文標(biāo)題:河套IT TALK——TALK 12:編程的技術(shù)|藝術(shù)|術(shù)術(shù) 下篇:對(duì)著代碼解讀編程的哲學(xué)
文章出處:【微信號(hào):開源技術(shù)服務(wù)中心,微信公眾號(hào):共熵服務(wù)中心】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論