提高代碼質(zhì)量:如何編寫函數(shù)
函數(shù)是實(shí)現(xiàn)程序功能的最基本單位,每一個(gè)程序都是由一個(gè)個(gè)最基本的函數(shù)構(gòu)成的。寫好一個(gè)函數(shù)是提高程序代碼質(zhì)量最關(guān)鍵的一步。本文就函數(shù)的編寫,從函數(shù)命名,代碼分布,技巧等方面入手,談?wù)勅绾螌懞靡粋€(gè)可讀性高、易維護(hù),易測(cè)試的函數(shù)。
命名
首先從命名說(shuō)起,命名是提高可讀性的***步。如何為變量和函數(shù)命名一直是開發(fā)者心中的痛點(diǎn)之一,對(duì)于母語(yǔ)非英語(yǔ)的我們來(lái)說(shuō),更是難上加難。下面我來(lái)說(shuō)說(shuō)如何為函數(shù)命名的一些想法和感受:
采用統(tǒng)一的命名規(guī)則
在談及如何為函數(shù)取一個(gè)準(zhǔn)確而優(yōu)雅的名字之前,首先最重要的是要有統(tǒng)一的命名規(guī)則。這是提高代碼可讀性的最基礎(chǔ)的準(zhǔn)則。
帕斯卡命名法和駝峰命名法是目前比較流行的兩種規(guī)則,不同語(yǔ)言采用的規(guī)則可能不一樣,但是要記住一點(diǎn):保持團(tuán)隊(duì)和個(gè)人風(fēng)格一致。
1、帕斯卡命名法
帕斯卡命名法簡(jiǎn)單地說(shuō)就是:多個(gè)單詞組成一個(gè)名稱時(shí),每個(gè)單詞的首字母大寫。比如:
- public void SendMessage ();
- public void CalculatePrice ();
在C#中,這種命名法常用于類、屬性,函數(shù)等等,在JS中,構(gòu)造函數(shù)也推薦采用這種方式命名。
2、駝峰命名法
駝峰命名法和帕斯卡命名法很類似,多個(gè)單詞組成一個(gè)名稱時(shí),***個(gè)單詞全部小寫,后面單詞首字母大寫。比如:
- var sendMessage = unction () {};
- var calculatePrice = function () {};
駝峰命名法一般用于字段、局部變量、函數(shù)參數(shù)等等。,在JS中,函數(shù)也常用此方法命名。
采用哪種命名規(guī)則并不絕對(duì),最重要的是要遵守團(tuán)隊(duì)約定,語(yǔ)言規(guī)范。
盡可能完整地描述函數(shù)所做的所有事情
有的開發(fā)者可能覺(jué)得相較于長(zhǎng)函數(shù)名來(lái)說(shuō),短函數(shù)名看起來(lái)可能更簡(jiǎn)潔,看起來(lái)也更舒服。但是通常來(lái)說(shuō),函數(shù)名稱越短其描述的意思越抽象。函數(shù)使用者對(duì) 函數(shù)的***印象就是函數(shù)名稱,進(jìn)而了解函數(shù)的功能,我們應(yīng)該盡可能地描述到函數(shù)所做的所有事情,防止使用者不知道或誤解造成潛在的錯(cuò)誤。
舉個(gè)例子,假設(shè)我們做一個(gè)添加評(píng)論的功能,添加完畢后并返回評(píng)論總數(shù)量,如何命名比較合適呢?
- // 描述不夠完整的函數(shù)名
- var count = function addComment() {}
- // 描述完整的函數(shù)名
- var count = function addCommentAndReturnCount() {};
這只是簡(jiǎn)單的一個(gè)例子,實(shí)際開發(fā)中可能會(huì)遇到得更多復(fù)雜的情況,單一職責(zé)原則是我們開發(fā)函數(shù)要遵守的準(zhǔn)則,但是有時(shí)候無(wú)法做到函數(shù)單一職責(zé)時(shí),請(qǐng)記 得函數(shù)名應(yīng)該盡可能地描述所有事情。當(dāng)你無(wú)法命名一個(gè)函數(shù)時(shí),應(yīng)該分析一下,這個(gè)函數(shù)的編寫是否科學(xué),有什么辦法可以去優(yōu)化它。
采用準(zhǔn)確的描述動(dòng)詞
這一點(diǎn)對(duì)母語(yǔ)非英語(yǔ)的開發(fā)者來(lái)說(shuō)應(yīng)該是比較難的一點(diǎn),想要提高這方面的能力,最主要的還是要提高詞匯量,多閱讀優(yōu)秀代碼積累經(jīng)驗(yàn)。
這里簡(jiǎn)單說(shuō)說(shuō)我自己的一些感想和看法:
1、不要采用太抽象廣泛的單詞
很多開發(fā)人員會(huì)采用一個(gè)比較寬泛的動(dòng)詞來(lái)為函數(shù)命名,最典型的一個(gè)例子就是get這個(gè)單詞。我們平時(shí)開發(fā)中經(jīng)常會(huì)通過(guò)各種不同的方式拿到數(shù)據(jù),但是每一種方式都用get就有點(diǎn)太抽象了。具體如何命名,要具體分析:
(1)簡(jiǎn)單的返回?cái)?shù)據(jù)
- Person.prototype.getFullName = function() {
- return this.firstName = this.lastName;
- }
(2)從遠(yuǎn)程獲取數(shù)據(jù)
- var fetchPersons = function () {
- ...
- $.ajax({
- })
- }
(3)從本地存儲(chǔ)加載數(shù)據(jù)
- var loadPersons = function () {};
(4)通過(guò)計(jì)算獲取數(shù)據(jù)
- var calculateTotal = function () {};
(5)從數(shù)組中查找數(shù)據(jù)
- var findSth = function (arr) {};
(6)從一些數(shù)據(jù)生成或得到
- var createSth = function (data) {};
- var buildSth = function (data) {};
- var parseSth = function(data) {};
這是一個(gè)簡(jiǎn)單的例子,我們平時(shí)開發(fā)中遇到的情況肯定會(huì)復(fù)雜得多,關(guān)鍵還是靠單詞的積累,多閱讀優(yōu)秀源碼
下面是整理的一些常用的對(duì)仗詞,大家可以參考使用
- add/remove increment/decrement open/close
- begin/end insert/delete show/hide
- create/destory lock/unlock source/target
- first/last min/max star/stop
- get/put next/previous up/down
- get/set old/new
根據(jù)不同項(xiàng)目和需求制定好命名規(guī)則
這一點(diǎn)也是很重要的,尤其是在團(tuán)隊(duì)合作中,不同的項(xiàng)目和需求可能導(dǎo)致的不同的命名規(guī)則。
比如我們通常采用的命名規(guī)則是動(dòng)賓結(jié)構(gòu),也就是動(dòng)詞在前,名詞災(zāi)后。但是有一些項(xiàng)目,比如數(shù)據(jù)接口等項(xiàng)目中,有的團(tuán)隊(duì)會(huì)采用名字在前,動(dòng)詞在后的形式,例如:
- public static Product[] ProductsGet(){};
- public static Product[] ProductsDel(){};
- public static Customer[] CustomerDel(){};
- public static Customer[] CustomerDel(){};
這種的好處是看到前面的名詞,比如ProductsGet,就能很快的知道這是產(chǎn)品相關(guān)的數(shù)據(jù)接口。
當(dāng)然這個(gè)并不是絕對(duì)的,關(guān)鍵還是要團(tuán)隊(duì)共同制定和遵守同一套命名規(guī)則。
函數(shù)參數(shù)
函數(shù)使用者在調(diào)用函數(shù)時(shí),必須嚴(yán)格遵守函數(shù)定義的參數(shù),這對(duì)函數(shù)的易用性,可測(cè)試性等方面都是至關(guān)重要的。下面我從幾個(gè)方面來(lái)談?wù)勱P(guān)于如何優(yōu)化好函數(shù)參數(shù)的一些想法。
參數(shù)數(shù)量
毫無(wú)疑問(wèn),函數(shù)參數(shù)越多,函數(shù)的易用性就越差,因?yàn)槭褂谜咝枰獓?yán)格眼中參數(shù)列表依次輸入?yún)?shù),如果某個(gè)參數(shù)輸錯(cuò),將導(dǎo)致不可意料的結(jié)果。
但是,函數(shù)參數(shù)就一定越少越好嗎?我們來(lái)看看下面的例子:
- var count = 0;
- var unitPrice = 1.5;
- ....
- ...
- var calculatePrice = function () {
- return count * unitPrice;
- }
在這個(gè)例子中,我們通過(guò)calculatePrice這個(gè)函數(shù)來(lái)計(jì)算價(jià)格,函數(shù)不接收任何參數(shù),直接通過(guò)兩個(gè)全局變量unitPrice和 count進(jìn)行計(jì)算。這種函數(shù)的定義對(duì)使用者來(lái)說(shuō)非常方便,直接調(diào)用即可,不用輸入任何參數(shù)。但是這里可能會(huì)有潛在的bug:全局變量可能在其他地方被修 改成其他值了,難以進(jìn)行單元測(cè)試等等問(wèn)題。所以,這個(gè)函數(shù)可以傳入數(shù)量和價(jià)格信息:
- var calculatePrice = function(count, unitPrice) {
- return count * unitPrice;
- }
這種方式下,函數(shù)使用者在使用時(shí),要傳入?yún)?shù)進(jìn)行調(diào)用,避免了全局變量可能存在的問(wèn)題。另外也降低了耦合,提高了可測(cè)試性,在測(cè)試的時(shí)候就不必依賴于全局變量。
當(dāng)然,在保證函數(shù)不依賴于全局變量和測(cè)試性的情況下,函數(shù)參數(shù)還是越少越好?!洞a大全》中提出將函數(shù)的參數(shù)限制在7個(gè)以內(nèi),這個(gè)可以作為我們的參考。
有的時(shí)候,我們不可避免地要使用超過(guò)10個(gè)以上函數(shù),在這中情況下,我們可以考慮將類似的參數(shù)構(gòu)造成一個(gè)類,我們來(lái)看看一個(gè)典型的例子。
我相信大家平時(shí)一定做過(guò)這樣的功能,列表篩選,其中涉及到各種條件的篩選,排序,分頁(yè)等等功能,如果將參數(shù)一個(gè)一個(gè)地列出來(lái)必定會(huì)很長(zhǎng),例如:
- var filterHotel = function (city, checkIn, checkOut, price, star, position, wifi, meal, sort, pageIndex) {}
這是一個(gè)篩選酒店的函數(shù),其中的參數(shù)分別是城市,入住和退房時(shí)間,價(jià)格,***,位置,是否有wifi,是否有早餐,排序,頁(yè)碼等等,實(shí)際的情況可能會(huì)更多。在這種參數(shù)特別多的情況下,我們可以考慮將一些相似的參數(shù)提取成類出來(lái):
- function DatePlace (city, checkIn, checkOut){
- this.city = city;
- this.checkIn = checkIn;
- this.checkOut = checkOut
- }
- function HotelFeature (price, star, position, wifi, meal){
- this.price = price;
- this.star = star;
- this.position = position;
- this.wifi = wifi;
- this.meal = meal;
- }
- var filterHotel = function (datePlce, hotelFeature, sort, pageIndex) {};
將多個(gè)參數(shù)提取成對(duì)象了,雖然對(duì)象數(shù)量增多了,但是函數(shù)參數(shù)更清晰了,調(diào)用起來(lái)也更方便了。
盡量不要使用bool類型作為參數(shù)
有的時(shí)候,我們會(huì)寫出使用bool作為參數(shù)的情況,比如:
- var getProduct = function(finished) {
- if(finished){
- }
- else{
- }
- }
- // 調(diào)用
- getProduct(true);
如果沒(méi)有注釋,使用者看到這樣的代碼:getProduct(true),他肯定搞不清楚true是代表什么意思,還要去查看函數(shù)定義才能明白這個(gè)函數(shù)是如何使用的。這就意味著這個(gè)函數(shù)不夠清晰,就應(yīng)該考慮去優(yōu)化它。通常有兩種方式去優(yōu)化它:
(1)將函數(shù)一分為二,分成兩個(gè)函數(shù)getFinishedProduct和getUnFinishedProduct
(2)將bool轉(zhuǎn)換成有意義的枚舉getProduct(ProductStatus)
不要修改輸入?yún)?shù)
如果輸入?yún)?shù)在函數(shù)內(nèi)被修改了,很有可能造成潛在的bug,而且使用者不知道調(diào)用函數(shù)后居然會(huì)修改函數(shù)參數(shù)。
正確使用輸入?yún)?shù)的做法應(yīng)該是只傳入?yún)?shù)用于函數(shù)調(diào)用。
如果不可避免地要修改,一定要在注釋中說(shuō)明。
盡量不要使用輸出參數(shù)
使用輸出參數(shù)說(shuō)明這個(gè)函數(shù)不只做了一件事情,而且使用者使用的時(shí)候可能還會(huì)感到困惑。正確的方式應(yīng)該是分解函數(shù),讓函數(shù)只做一件事。
編寫函數(shù)體
函數(shù)體就是實(shí)現(xiàn)函數(shù)功能的整個(gè)邏輯,是一個(gè)函數(shù)最關(guān)鍵的地方。下面我談?wù)勱P(guān)于函數(shù)代碼編寫的一些個(gè)人想法。
相關(guān)操作放在一起
有的時(shí)候,我們會(huì)在一個(gè)函數(shù)內(nèi)進(jìn)行一系列的操作來(lái)完成一個(gè)功能,比如:
- var calculateTotalPrice = function() {
- var roomCount = getRoomCount();
- var mealCount = getMealCount();
- var roomPrice = getRoomPrice(roomCount);
- var mealPrice = getMealPrice(mealCount);
- return roomPrice + mealPrice;
- }
這段代碼計(jì)算了房間價(jià)格和早餐價(jià)格,然后將兩者相加返回總價(jià)格。
這段代碼乍一看,沒(méi)有什么問(wèn)題,但是我們分析代碼,我們先是分別獲取了房間數(shù)量和早餐數(shù)量,然后再通過(guò)房間數(shù)量和早餐數(shù)量分別計(jì)算兩者的價(jià)格。這種 情況下,房間數(shù)量和計(jì)算房間價(jià)格的代碼分散在了兩個(gè)位置,早餐價(jià)格的計(jì)算也是分散到了兩個(gè)位置。也就是兩部分相關(guān)的代碼分散在了各處,這樣閱讀起代碼來(lái)邏 輯會(huì)略顯不通,代碼組織不夠好。我們應(yīng)該讓相關(guān)的語(yǔ)句和操作放在一起,也有利于重構(gòu)代碼。我們修改如下:
- var calculateTotalPrice = function() {
- var roomCount = getRoomCount();
- var roomPrice = getRoomPrice(roomCount);
- var mealCount = getMealCount();
- var mealPrice = getMealPrice(mealCount);
- return roomPrice + mealPrice;
- }
我們將相關(guān)的操作放在一起,這樣代碼看起來(lái)更清晰了,而且也更容易重構(gòu)了。
盡量減少代碼嵌套
我們平時(shí)寫if,switch或for語(yǔ)句是常有的事兒,也一定寫過(guò)多層if或for語(yǔ)句嵌套的情況,如果代碼里的嵌套超過(guò)3層,閱讀起來(lái)就會(huì)非常困難了。我們應(yīng)該盡量避免代碼嵌套多層,***不要超過(guò)2層。下面我來(lái)說(shuō)說(shuō)我平時(shí)一些減少嵌套的技巧或方法。
if語(yǔ)句嵌套的問(wèn)題
多層if語(yǔ)句嵌套是常有的事情,有什么好的方法可以減少嵌套呢?
1、盡早終止函數(shù)或返回?cái)?shù)據(jù)
如果符合某個(gè)條件下可以直接終止函數(shù),則應(yīng)該將這個(gè)條件放在***位。我們來(lái)看看下面的例子。
- if(condition1) {
- if(condition2){
- if(condition3){
- }
- else{
- return;
- }
- }
- else{
- return;
- }
- }
- else {
- return;
- }
這段代碼中if語(yǔ)句嵌套了3層,看起來(lái)已經(jīng)很復(fù)雜了,我們可以將***面的return提取到最前面去。
- if(!condition1){
- return;
- }
- if(!condition2){
- return;
- }
- if(!condition3){
- return;
- }
- //doSth
這段代碼中,我們把condition1等于false的語(yǔ)句提取到前面,直接終止函數(shù),將多層嵌套的if語(yǔ)句重構(gòu)成只有一層if語(yǔ)句,代碼也更清晰了。
注意:一般情況下,我們寫if語(yǔ)句會(huì)將條件為true的情況寫在前面,這也比較符合我們的思維習(xí)慣。如果是多層嵌套的情況,應(yīng)該優(yōu)先減少if語(yǔ)句的嵌套
2、不適用if語(yǔ)句或switch語(yǔ)句
條件語(yǔ)句一般來(lái)說(shuō)是不可避免的,有的時(shí)候,我們要判斷很多條件就會(huì)寫很多if-elseif語(yǔ)句,嵌套的話,就更加麻煩了。如果有一天增加了新需 求,我們就要去增加一個(gè)if分支語(yǔ)句,這樣不僅修改起來(lái)麻煩,而且容易出錯(cuò)。《代碼大全》提出的表驅(qū)動(dòng)法可以有效地解決if語(yǔ)句帶來(lái)的問(wèn)題。我們來(lái)看下面 這個(gè)例子:
- if(condition == “case1”){
- return 1;
- }
- elseif(condition == “case2”){
- return 2;
- }
- elseif(condition == “case3”){
- return 3;
- }
- elseif(condition == “case4”){
- return 4;
- }
這段代碼分別依次判斷了四種情況,如果再增加一種情況,我們就要再新增一個(gè)if分支,這樣就可能造成潛在的問(wèn)題,如何去優(yōu)化這段代碼呢?我們可以采用一個(gè)Map或Dictionary來(lái)將每一種情況和相應(yīng)值一一對(duì)應(yīng)。
- var map = {
- "case1":1,
- "case2":2,
- "case3":3,
- "case4":4
- }
- return map[condition];
通過(guò)map優(yōu)化后,整個(gè)代碼不僅更加簡(jiǎn)潔,修改起來(lái)也更方便而且不易出錯(cuò)了。
當(dāng)然,很多時(shí)候我們的條件判斷語(yǔ)句并不是這么簡(jiǎn)單的,可能會(huì)涉及到復(fù)雜的邏輯運(yùn)算,大家可以查看《代碼大全》第18章,其中有詳細(xì)的介紹。
3、提取內(nèi)層嵌套為一個(gè)函數(shù)進(jìn)行調(diào)用
多層嵌套的時(shí)候,我們還可以將內(nèi)層嵌套提取到一個(gè)新的函數(shù)中,然后調(diào)用該函數(shù),這樣代碼也就更清晰了。
for循環(huán)嵌套優(yōu)化
for循環(huán)嵌套相比于if嵌套來(lái)說(shuō)更加復(fù)雜,閱讀起來(lái)會(huì)更麻煩,下面說(shuō)說(shuō)幾點(diǎn)要注意的東西:
1、最多只能兩層for循環(huán)嵌套
2、提取內(nèi)層循環(huán)到新函數(shù)中
3、多層循環(huán)時(shí),不要簡(jiǎn)單地位索引變量命名為i,j,k等,容易造成混淆,要有具體的意思
提取復(fù)雜邏輯,語(yǔ)義化
有的時(shí)候,我們會(huì)寫出一些比較復(fù)雜的邏輯,閱讀代碼的人看到后可能搞不清楚要做什么,這個(gè)時(shí)候,就應(yīng)該提取出這段復(fù)雜的邏輯代碼。
- if (age > 18 && gender == "man") {
- //doSth
- }
這段代碼表示當(dāng)年齡大于18并且是男性的話,可以doSth,但是還是不夠清晰,可以將其提取出來(lái)
- var canDoSth = function (age, gender){
- return age > 18 && gender == "man";
- }
- ...
- ...
- ...
- if(canDoSth(age, gender)){
- //doSth
- }
雖說(shuō)多了一個(gè)函數(shù),但是代碼更加清晰和語(yǔ)義化了。
總結(jié)
本文從函數(shù)命名,函數(shù)參數(shù)和函數(shù)的代碼編寫三個(gè)方面談了關(guān)于如何編寫好一個(gè)函數(shù)的感受和想法。文中提到了很多具體的情況,當(dāng)然日常編碼中肯定會(huì)遇到更多復(fù)雜的情況可能我暫時(shí)沒(méi)有想到。我簡(jiǎn)單的歸納了幾點(diǎn):
1、準(zhǔn)確地對(duì)變量、函數(shù)命名
2、不要有重復(fù)邏輯的代碼
3、函數(shù)的行數(shù)不要超過(guò)20行,這里的20行只是個(gè)大概,并不一定是這個(gè)數(shù)字
4、減少嵌套
我相信大家一定會(huì)很多關(guān)于這方面的經(jīng)驗(yàn),歡迎進(jìn)行交流,共同提高代碼質(zhì)量。