開發(fā)前要進(jìn)行好準(zhǔn)備工作拯田,這能減少后面寫代碼的時候折返跑的概率康聂。(架構(gòu)設(shè)計尚辑、需求完整瞄崇、考慮每個需求點的商業(yè)價值...)
架構(gòu)設(shè)計:(為了降低風(fēng)險)
※ 對實現(xiàn)80%功能的20%的類所說明
※ 數(shù)據(jù)庫設(shè)計(為啥用單個/多個數(shù)據(jù)庫、表設(shè)計勇哗、view設(shè)計昼扛、為啥要用數(shù)據(jù)庫而非文件)
※ 對資源的使用率例如線程帶寬以及極端情況處理
※ 安全性、性能以及風(fēng)險
※ 可伸縮性(用戶增長應(yīng)對)
※ 國際化
※ 錯誤以及容錯
※ 指明對哪些類必須健壯欲诺,而其他類可以勉強健壯抄谐,防止過度工程主要的構(gòu)建:代碼規(guī)范 & 測試用例單元測試 & merge前的統(tǒng)一工序
-
子系統(tǒng)之前的通信要避免過多渺鹦,這樣容易牽一發(fā)而動全身,避免形成環(huán):
亂七八糟的系統(tǒng)
隱藏業(yè)務(wù)規(guī)則(例如稅率)& 用戶界面 & 數(shù)據(jù)庫訪問 & 系統(tǒng)相關(guān)的接口(例如Windows)的實現(xiàn)蛹含,盡量通過接口訪問毅厚,這樣可以很快的對他們做修改替換
信息隱藏:如果有個變量是int的,你可以通過
int id = newID()
生成它浦箱,但是如果它變成string了呢吸耿,你要到程序里面所有地方把類型改掉。如果你typedef了一個idType
憎茂,然后程序里都用idType
就可以很輕松的改id的實際type了~ 所以要多思考我要隱藏些什么?封裝變化:把容易變化的搞成一個類锤岸,然后讓內(nèi)部的變化對外不可見竖幔。例如sdk的操作啥的,這樣如果以后要換一個sdk就很方便是偷。(例如業(yè)務(wù)規(guī)則拳氢、對硬件or系統(tǒng)的依賴、輸入輸出的文件格式蛋铆、非標(biāo)準(zhǔn)的語言特性馋评、困難設(shè)計區(qū)域、狀態(tài)變量刺啦、常量定義)
-
為測試而設(shè)計
設(shè)計check list 不要太多注釋說明的事兒留特,可以直接assert解決,如果錯誤使用就crash了
保持接口抽象一致性玛瘸、內(nèi)聚性蜕青。不要暴露過多接口,注意封裝糊渊,封裝如果做不好接口抽象也一般很難做好右核,隨之而來的可能就是耦合嚴(yán)重
如果你調(diào)用某個函數(shù)必須知道它的內(nèi)部實現(xiàn)邏輯,那么就是有問題的渺绒,這不是面向接口編程而是面向?qū)崿F(xiàn)了
有7-9個成員變量是一個類的合理大小贺喝,如果有更多的話最好拆成單獨的類
繼承的適用場景是,如果有個基類A和子類BCD宗兼,你在使用BCD的時候是無差別的躏鱼,不用考慮它具體是哪個類才是對的。如果C的某個方法和BD不一致殷绍,使用實例的時候還得先判斷是不是C挠他,如果是就do something,繼承就是不好的篡帕。
只有一個派生類的基類是沒啥用的可以不用提早過度設(shè)計做繼承殖侵;而有很多只覆蓋了幾個方法還不干活的派生類的基類也是有問題的贸呢。
減少繼承層數(shù),最多2-3層以內(nèi)
-
多重繼承
屏幕快照 2020-05-30 上午8.09.26.png 減少實例化對象的種類拢军、減少在實例化對象上調(diào)用不同子程序的數(shù)量楞陷、減少調(diào)用由子程序返回對象的子程序例如
new A().createB().getC().doSth()
避免只有數(shù)據(jù)或者只有行為的類。
(這里其實有點迷茉唉,model不是有一些就只有數(shù)據(jù)么固蛾,以及util一般都只有行為吖)繼承會增加復(fù)雜度,所以用組合好于繼承度陆,因為我們編程的目的是降低復(fù)雜度艾凯。
創(chuàng)建子程序(函數(shù))的原因:
降低復(fù)雜度讓調(diào)用者可以不必思考細(xì)節(jié)直接用、
引入中間易懂的抽象讓代碼更可讀懂傀、
避免代碼重復(fù)趾诗、
支持子類化、
隱藏順序如先A后B可以把AB都分為兩個子程序蹬蚁、
隱藏指針操作恃泪、
提高可移植性隔離有語言特性的代碼、
簡化復(fù)雜的bool判斷犀斋、
改善性能集中調(diào)用可以集中發(fā)現(xiàn)修改問題贝乎、
限制變化帶來的影響、子程序應(yīng)該提高內(nèi)聚性做好一個事兒例如求cos叽粹,避免干兩個事兒例如cos以及tan览效。高內(nèi)聚性的bug會更少。
子程序命名:
(1)有的時候會起解決的問題&副作用的名字例如calculateReportTotalsAndOpenOutputFile
虫几。這樣就太長了朽肥,可以只保留解決的問題即可
(2)避免無意義的動詞看不明白干了啥,例如handleOutput
可以改成formatAndPrintOutput
持钉,但有時候不好起名是這個子程序沒有很好地內(nèi)聚衡招,需要修改代碼
(3)避免僅數(shù)字形成不同名字,例如outputUser1
/outputUser2
(4)要對返回值有所描述每强,例如currentColor()
(5)面向?qū)ο笳Z言里面如果都document.print()
了就可以不用命名為printDocument
(6)對仗始腾,open/close、create/destory
(7)為常見操作統(tǒng)一命名空执。例如項目里面好幾個類都有獲取id的方法浪箭,那么就盡量不要一個類叫getId()
,另一個類叫id()
辨绊,還要一個類叫id().get()
參數(shù)順序奶栖,可以先是input不修改值的、最后是output會改變值的。(類似swift)
用assert對參數(shù)說明宣鄙,例如數(shù)值范圍袍镀、枚舉、數(shù)量的參數(shù)的單位
參數(shù)限制在7個以內(nèi)冻晤;
如果一個有10個參數(shù)外露的對象苇羡,子程序之需要3個,那么是傳入對象還是3個參數(shù)呢鼻弧?看子程序需要的抽象究竟是什么设江,避免裝包拆包也就是避免創(chuàng)建一個新的對象給子程序,然后子程序再拿自己要的三個屬性攘轩,如果是這樣就直接給三個屬性即可叉存。如果經(jīng)常需要修改子程序參數(shù),每次都是加這個對象的其他參數(shù)度帮,那么就可以傳入對象歼捏。
顯式聲明參數(shù)名,防止錯位
返回值應(yīng)該是函數(shù)名所指定的够傍,如果函數(shù)名沒有指定甫菠,則不應(yīng)該有返回值挠铲。不要為了方便隨意加返回值給外面暗示成功or失敗哦冕屯。
define宏注意用括號 & 花括號包起來避免執(zhí)行錯誤:例如
Cube(a) ((a) * (a) * (a))
,這樣就避免了傳入的是 x + 1 這種拂苹。所以盡量不要用宏安聘,可以用inline、template瓢棒、typedef來替代斷言是保證代碼正確性的浴韭,只有debug環(huán)境監(jiān)測,避免絕不應(yīng)出現(xiàn)的問題脯宿;錯誤處理是處理可能出現(xiàn)的問題念颈,讓程序不崩潰;異常是嚴(yán)重問題连霉,外面拿到異常如果不catch就crash了
-
不要拋出低層級exception暴露內(nèi)部實現(xiàn):
錯誤的異常
可以先寫偽代碼榴芳,從高到低,直到你覺得已經(jīng)沒有意義了跺撼,然后作為注釋窟感,填充代碼。(注意去除冗余的注釋歉井,雖然其實現(xiàn)在的互聯(lián)網(wǎng)沒有給寫偽代碼的時間..)
變量存活時間是從最后一次使用到第一次聲明的行數(shù)柿祈;平均跨度是相鄰使用之間的行數(shù)距離的平均值。我們追求的應(yīng)該是短存活時間 + 短跨度,可以讓讀者更好的理解代碼躏嚎,并且減少中間使用的可能性來減少錯誤蜜自。其實也就是讓你思考和控制的范圍變小,就更不容易出錯紧索。(所以應(yīng)該避免全局變量)
把計算的量放在名字最后的這條規(guī)則也有例外袁辈,那就是Num限定詞的位置已經(jīng)是約定俗成的。Num放在變量的開始位置代表一個總數(shù):numCustomers表示的是員工的總數(shù)珠漂。Num放在變量名的結(jié)束位置代表一個下標(biāo):customerNum表示的是當(dāng)前員工的序號晚缩。通過numCustomers最后代表復(fù)數(shù)的s也能夠看出這兩種應(yīng)用之間的區(qū)別。
然而媳危,由于這樣使用Num常常會帶來麻煩荞彼,因此可能最好的辦法是避開這些問題,用Count或者Total來代表員工的總數(shù)待笑,用Index來指代某個特定的員工鸣皂。這樣,customerCount就代表員工的總數(shù)暮蹂,customerIndex代表某個特定的員工寞缝。循環(huán)中可以用i,j,k命名,但是如果是循環(huán)嵌套仰泻,其實這樣容易出問題荆陆,或者這個變量不僅僅在循環(huán)內(nèi)用,這些情況最好起一個有意義的名字類似
score[teamIndex][eventIndex]
BOOL的命名可以用
found
集侯、success
等被啼,自己就包含了真假的含義。如果有的人習(xí)慣在前面加一個is
例如isFound
也可以棠枉,可以避免非bool含義的變量被如此命名例如isStatus
就很奇怪浓体。但是if(found)
就比if(isFound)
好很多。如果是反義的可以取名notFound
辈讶,如果經(jīng)常判斷的是if(!notFound)
就應(yīng)該替換為if(found)
啦在同一段中不要出現(xiàn)含義類似的兩個變量命浴,很難理解。也不要那種拼寫類似的哦贱除,容易看混生闲。
在精度要求很高的環(huán)境下,如果你不確定自己要用double還是float勘伺,其實可以typedef一個新的類型跪腹,然后計算時候都用自己定義的type,如果需要改精度飞醉,只要改這個type的定義就可以改掉所有地方比較方便~ 例如
typedef Coordinate double
冲茸,而且這樣可以對使用者隱藏細(xì)節(jié)冯丙。內(nèi)存中的一個位置就是一個地址灼狰,常用16進(jìn)制形式表示。在32bit的處理器上面,地址用32bit表示互捌,指針本身只包含地址铝耻。
-
如何解釋內(nèi)存中某個位置的內(nèi)容慈格,是由指針的base type決定的仍秤,如果某個指針指向整數(shù),那么意味著編譯器會把指向的內(nèi)存位置的數(shù)據(jù)解釋為整數(shù)彼宠。所以內(nèi)存并沒有綁定一個指定的type鳄虱,是你用的指針才指定了解析內(nèi)存的方式。你可以讓一個整數(shù)凭峡、字符串拙已、浮點數(shù)都指向同一塊內(nèi)存,同樣的內(nèi)存可以解釋為不同的內(nèi)容摧冀,只是取決于指針的base type倍踪,例如:
指針type 如果子程序傳入?yún)?shù)是個指針,需要
*parameter = some_value
才是改了指針指向的內(nèi)容哦索昂。
但是注意int *p; *p = 1
不能用建车,因為int *p;
定義了一個指針p,然而p并沒有指向任何地址椒惨,所以當(dāng)使用*p
時是沒有任何地址空間對應(yīng)的缤至,所以*p=1
就會導(dǎo)致,不知道把這個1賦值給哪個地址空間了框产。
int *p; p = 1;
能用是因為int *p;
定義了一個指針p凄杯,p = 1;
意思是將一個內(nèi)存地址為1的地址賦值給p错洁,所以這個是可行的秉宿。但是這個操作是不安全的。即使全局變量屯碴,也不要讓程序里各個地方直接使用和修改描睦,需要創(chuàng)建setter和getter控制訪問。這樣比較方便之后拓展和調(diào)試导而。
- 雖然應(yīng)該避免全局變量忱叭,但如果真的需要那么就光明正大的用,命名盡量和別的區(qū)分開今艺,不要搞一個大的雜亂無章的對象來回傳遞反而不好韵丑。(類應(yīng)該具有抽象一致性,而非單純?yōu)榱藴p少參數(shù)傳遞個數(shù)之類的虚缎,除非本身就是一致的)
- 如何標(biāo)識一系列需要順序執(zhí)行的子程序:
通常我們對寫程序?qū)?shù)據(jù)操作都是取數(shù)撵彻,對數(shù)據(jù)進(jìn)行計算,再打印。一段java演示代碼如下:
data = ReadData();
results = CalculateResultsFromData( data );
PrintResults( results );
這三個步驟思路非常清晰陌僵,說明了這幾個函數(shù)之間的耦合程度轴合,和相互依賴性。除非什么特殊情況發(fā)生碗短,否則都會按照這個順序來執(zhí)行受葛。尤其在ABAP中,似乎都是這樣的順序偎谁。
再看一個例子:
revenue.ComputeMonthly();
revenue.ComputeQuarterly();
revenue.ComputeAnnual();
在以上代碼中先計算的是月份总滩,接著是季度,最后是年份巡雨。這是一個常識咳秉,但是光從代碼中我們無法得出他們是否有耦合,是否應(yīng)該按照這個特定順序執(zhí)行鸯隅。
再看一個VB例子:
ComputeMarketingExpense
ComputeSalesExpense
ComputeTravelExpense
ComputePersonnelExpense
DisplayExpenseSummary
從以上例子中也看不處這段代碼的執(zhí)行順序有什么特殊之處澜建,也不知道如果改變語句的執(zhí)行順序會怎樣。沒有任何的注釋蝌以,程序也沒有參數(shù)炕舵,可能都是直接對全局變量進(jìn)行操作。所以這段代碼寫的并不好跟畅。
=> Solution 1: 組織代碼讓依賴關(guān)系更明顯
在上面的VB代碼中咽筋,應(yīng)該有一個初始化函數(shù),如InitializeExpenseData()
徊件。寫這個函數(shù)的目的是程序的結(jié)構(gòu)更清晰奸攻,讓讀代碼的人知道了一個隱含的信息,在調(diào)用其它函數(shù)之前虱痕,必須調(diào)用初始化函數(shù)睹耐。
=> Solution 2: 使子程序名凸顯依賴關(guān)系
如果ComputeMarketingExpense
必須最先執(zhí)行,它做的不僅僅是現(xiàn)在名字里面的事情部翘,還InitializeMemberData
它應(yīng)該命名為ComputeMarketingExpenseAnd InitializeMemberData
=> Solution 3: 利用子程序參數(shù)明確顯示依賴關(guān)系
如果子程序沒有傳參硝训,你就看不出來哪些子程序依賴了一樣的數(shù)據(jù),通過顯式寫參數(shù)可以暗示使用者順序是很重要的新思。
ComputeMarketingExpense( marketingData )
ComputeSalesExpense( salesData )
ComputeTravelExpense( travelData )
ComputePersonnelExpense( personnelData )
DisplayExpenseSummary( marketingData, salesData, travelData, personnelData )
有一種更明確依賴關(guān)系的方式就是加入輸入和輸出
expenseData = InitializeExpenseData( expenseData )
expenseData = ComputeMarketingExpense( expenseData )
expenseData = ComputeSalesExpense( expenseData )
expenseData = ComputeTravelExpense( expenseData )
expenseData = ComputePersonnelExpense( expenseData )
DisplayExpenseSummary( expenseData )
這樣你是不是就可以明確的感受到窖梁,后面的結(jié)果依賴于前面的步驟了
=> Solution 4: 注釋
=> Solution 5: 斷言
如果這段非常重要,可以在前置步驟里面加一個bool isXXXExceuted
夹囚,然后再后面的步驟判斷之前的isXXXExceuted
都是true
才執(zhí)行后面的纵刘,否則就assert。但這樣增加了變量哦荸哟,所以要權(quán)衡利弊啦是不是足夠重要假哎。
順序無關(guān)的語句也要遵循就近原則蛔翅,相關(guān)的放在一起,降低讀代碼的時候的復(fù)雜度位谋。
if-else山析、switch-case的時候,先寫正常的再寫異常情況掏父。這個也類似likely可以提高執(zhí)行效率笋轨。
如果condition過于復(fù)雜,可以抽出函數(shù)來得到BOOL值赊淑,不要把
if(xxx)
寫的過長很難理解爵政。簡化case里面要做的事兒,如果很長就提成函數(shù)陶缺。盡量避免case里面不break越到下個case钾挟,如果真的要這么做就注釋一下,但也盡量不要這么做饱岸。
遞歸要注意有可能內(nèi)存溢出掺出,可以加入安全計數(shù)器限制執(zhí)行次數(shù)。(不要用遞歸計算階乘 & 斐波那契苫费,明明循環(huán)更可讀并性能好)
-
各種方式找代碼問題的有效率:
缺陷檢測率
所以沒有任一種是可以直接把問題都找出來的汤锨,需要混合各種方法來提高檢測率。代碼復(fù)查的找到錯誤的效率要高于測試的百框,所以要多review吖還可以順便改bug
測試先行闲礼。先寫測試用例,能明確需求也能提高開發(fā)效率铐维。
一個不能經(jīng)驗論的例子:性能優(yōu)化的時候我們可能為了性能而寫了晦澀的高級代碼柬泽,例如二維數(shù)組求和,為了避免用循環(huán)以及下標(biāo)計算嫁蛇,可以用指針遞減的方式做锨并。但是當(dāng)你真的測試性能就會發(fā)現(xiàn)沒有區(qū)別兩者,因為編譯器早就做了優(yōu)化棠众,循環(huán)的實現(xiàn)就是指針遞減琳疏。所以真的有性能問題的時候要找到影響性能的20%代碼然后優(yōu)化有决,不要再細(xì)枝末節(jié)上面糾結(jié)太多闸拿。
C#中switch-case的效率要高于if-else,然而java中相反书幕。不用語言是不一樣的哈~ 只能通過測試確定這個事情新荤。
-
用表查詢替代邏輯判斷:
image1.png
當(dāng)性能和可讀性矛盾的時候需要看場合,不是一定為了性能犧牲可讀性台汇。例如循環(huán)展開求和比for快苛骨,但很難理解篱瞎。但我們應(yīng)該盡量簡化for循環(huán)里面做的事情
-
把最忙的循環(huán)放在內(nèi)層:
把最忙的循環(huán)放在內(nèi)層 -
削弱運算強度來提高性能:
運算強度 -
用低級語言重寫代碼:
image -
性能改善:
代碼調(diào)整方法
項目越大,寫代碼的時間占比會越小痒芝,架構(gòu)俐筋、需求、測試的b比例會更大严衬。并且項目的大小成倍增加澄者,總的時間可能不止一倍增加,因為相應(yīng)其他的部分是非線性增長的请琳。
-
編譯器與鏈接器:
編譯器與鏈接器
根據(jù)C++標(biāo)準(zhǔn)粱挡,一個編譯單元(Translation Unit)是指一個.cpp文件以及這所include的所有.h文件,.h文件里面的代碼將會被擴(kuò)展到包含它的.cpp文件里俄精,然后編譯器編譯該.cpp文件為一個.obj文件询筏,后者擁有PE(Portable Executable,即Windows可執(zhí)行文件)文件格式竖慧,并且本身包含的就是二進(jìn)制代碼嫌套,但是不一定能執(zhí)行,因為并不能保證其中一定有main函數(shù)圾旨。當(dāng)編譯器將一個工程里的所有.cpp文件以分離的方式編譯完畢后灌危,再由鏈接器進(jìn)行鏈接成為一個.exe或.dll文件。
舉個例子碳胳,編譯器負(fù)責(zé)把每一頁或節(jié)或章翻譯成等價的中文勇蝙,鏈接器負(fù)責(zé)把翻譯好的章節(jié)整理成完整說明書。
Finally挨约,這本書真的好長味混。。涉及的很廣還會有一些管理心理學(xué)之類的诫惭,雖然年代久遠(yuǎn)但也值得一看吧翁锡。