淺談前端/軟件工程師的代碼素養

「程序是寫給人讀的,只是偶爾讓計算機執行一下。」 ——Donald Ervin Knuth(高德納)javascript

關於代碼素養

咱們經常談到「素養」一詞,是指我的在專業領域內實踐訓練而成的一種修養,在不一樣的領域中有不一樣的體現,如在音樂領域中,「音樂素養」是指我的對於音樂的感受程度,對音高節奏的把控,對不一樣流派音樂的鑑賞能力等,而在編程領域,也有不一樣的素養,反映出對基本功、代碼整潔度、專業態度等等方面,所謂「代碼素養」,簡單來講,就是指代碼寫的是否優雅美觀可維護。html

絕對完美的代碼是不存在的,代碼素養並非指完美主義。在翻譯領域有「信,達,雅」的標準,「雅」之因此放在最後,是由於要達到它,須要有比較高的水準和經驗積累。類比到編程領域,咱們在編程時,第一時間想到的是如何將業務邏輯實現出來,而不是如何把代碼優雅地寫出來,因此寫代碼沒有所謂的絕對優雅。可是,做爲一名專業的前端工程師,確切的說,應該是專業的軟件工程師,編寫優雅的代碼應當是時刻保持的追求,它更像是一個準繩,如同每一個人知道本身該作什麼,不應作什麼,所謂原則,所謂底線,體現出所謂的「代碼素養」前端

破窗理論

破窗理論,原義指窗戶破損了,建築無人照管,人們聽任窗戶繼續破損,最終本身也參與破壞活動,在外牆上塗鴉,任垃圾堆積,最後走向傾頹。java

破窗理論在實際中很是容易出現,每每第一我的的代碼寫的很差,第二我的就會有相似「反正他已經寫成這樣了,那我也只能這樣了」的思想,致使代碼越維護越冗雜,最後一刻轟然坍塌,變成無人想去維護的垃圾。android

整潔的代碼

整潔的代碼如同優美的散文,試想讀過的一本好書,可以隨着做者的筆鋒跌宕起伏,充滿了畫面感,調動了本身的喜怒哀樂。代碼雖然沒有那樣的高潮迭起,但整潔的代碼應當充滿張力,可以在某一時刻利用這種張力將情節推向高潮。ios

我更喜歡把寫代碼類比於寫文章講故事,寫代碼是創做的過程,做者須要將本身想表達的東西經過代碼的形式展示出來,而整潔的代碼如同講故事通常,娓娓道來,引人入勝,很差的代碼則讓人感受毫無頭緒,通篇不知道在講什麼。程序員

整潔代碼原則

在現代化的前端開發中,有不少自動化工具能夠幫助咱們寫出規範的代碼,如eslinttslint等各類輔助校驗工具,知名的規範如google規範airbnb規範等等也從各個細節方面約束,幫助咱們造成合理規範的代碼風格。編程

本小節再也不重複語言層面的代碼風格,根據實際重構項目,總結出一系列開發過程當中須要時刻注意的原則,按照重要程度優先級排列。設計模式

1. DRY(Don't Repeat Yourself)

相信做爲一名軟件工程師,你們都據說過最基本的DRY原則,不少設計模式,包括面向對象自己,都是在這條原則上作努力。api

DRY顧名思義,是指「不要重複本身」,它實際上強調了一個抽象性原則,若是一樣或相似的代碼片斷出現了兩次以上,那麼應該將它抽象成一個通用方法或文件,在須要使用的地方去依賴引入,確保在改動的時候,只需調整一處,全部的地方都改變過來,而不是到每一個地方去找到相應的代碼來修改。

在實際工做中,我見過兩種在這條原則上各自走向極端的代碼:

  • 一種是徹底沒有抽象概念,重複的代碼散落在各處,更奇葩的是,有一部分的抽象,但更多的是重複,好比在common下抽取了一個data.js的數據處理文件,部分頁面中引用了該文件,而更多頁面徹底拷貝了該文件中的幾個不一樣方法代碼。而做者的意圖則是使人哭笑不得——只用到小部分代碼,不必引入那麼整個文件。且不論現代化的前端構建層面能夠解決這個問題,即便是引入了整個大文件,這部分多餘的代碼在gzip以後也不會損失多少性能,但這種處處copy的行爲帶來後續的維護成本是翻倍的。
  • 對於這種行爲還遇到另一個理由,就是工期時間短,改不動以前的代碼,怕形成外網問題,那就拷貝一份相同的邏輯來修改。好比支付邏輯,原有的邏輯爲單獨的UI浮層+單個支付購買,如今產品提出「打包購買」需求,原有的代碼邏輯又比較複雜,出現了「改不動」的現象,因而把UI層和購買邏輯的幾個文件整個拷貝過來,修改幾下,造成了新的「打包購買」模塊,後來產品又提出「按條購買」,按照上述」改不動「原則,又拷貝了一份「按條購買」的模塊。這樣一來調用處的邏輯就會冗餘重複,須要根據不一樣的購買方式引入不一樣UI組件和支付邏輯,另外若是新添需求,如支持「分期付款」,那麼將改動的是很是多的文件,最可悲的是,最後想要把代碼重構爲一處統一調用的人,將會面對三份「改不動」的壓力,須要衆多邏輯中對比分析提取相同之處,工做量已經不能用翻倍來衡量,而這種工做量每每沒法獲得產品的認同和理解。
  • 另外一種極端是過分設計,在寫每一個邏輯的時候都去抽象,讓代碼的可讀性大大降低,一個簡單的for循環都要複用,甚至變量定義,這種代碼維護起來也是比較有成本的,還有將迥然不一樣的邏輯過分抽象,使得抽象方法變得很是複雜,常常「牽一髮而動全身」,這種行爲也是不可取的。

這也是將該原則排在首位的緣由,這種行爲致使的重構工做量是最大的,保持良好的代碼維護性是一種素養,更是一種責任,若是本身在這方面逃避或偷懶,將把這塊工做量翻倍地加在未來別人或本身的身上。

2. SRP(Single Responsibility Principle)

SRP也是一個比較著名的設計原則——單一職責,在面向對象的編程中,認爲類應該具備單一職責,一個類的改變更機應當只有一個。

對於前端開發來講,最須要貫徹的思想是函數應當保持單一職責,一個函數應當只作一件事,這樣一來是保證函數的可複用性,更單一的函數有更強的複用性,二來可讓總體的代碼框架更加清晰,細節都封裝在一個個小函數中。另一點也和單一職責有關,就是無反作用的函數,也稱純函數,咱們應當儘可能保證純函數的數量,非純函數是不可避免的,但應當儘可能減小它。

把SRP原則排在第二位,由於它很是的重要,沒有人願意看一團亂麻的邏輯,在維護代碼時,若是沒有一個清晰的邏輯結構,全部的數據定義、數據處理、DOM操做等等一系列細節的代碼所有放在一個函數中,致使這個函數很是的冗長,讓人本能地產生心理排斥,不肯去查看內部的邏輯。

全部的複雜邏輯放在一個函數中,相信你們看到這樣的代碼都會眉頭一皺:

show: function(a, b) {
	if (!isInit) {
            init();
            isInit = true;
        }

        // reset
        this.balance = 0;
        this.isAllBalance = false;

        var shouldShowLayer = true,
            preSelectedTermId = 0,
            needAddress = course.address_state, 
            showTerms,
            termsObj;
        var hasPunish = false;

        this.course = course = course || {};
        opt = opt || {};
        opt.showMax = opt.showMax || 6;
        
    (isIosApp || b.isIAP) && (usekedian = !0, priceSymbol = '<i class="icon-font i-kedian"></i>'), 
    f.splice(b.showMax), layer.show({
        $container:b.$container,
        content:termSelectorTpl({
            terms:f,
            curTermId:b.curTermId || d,
            name:a.name,
            hasPunish:h,
            userInfo:j
        }, {
            renderTime:T.render.time.renderCourseTime,
            renderCourseTime:renderCourseTime,
            hideUserInfo:b.hideUserInfo,
            hideTitle:b.hideTitle,
            hidePayPrice:b.hidePayPrice,
            confirmText:b.confirmText,
            sys_time:a.sys_time
        }),
        cls:"term-select-new",
        allowMove:function(a) {
             return opt.allowMove || ($target.closest('.select-content').length &&
                        $('.term-select-new .select-time').height() +
                        $('.term-select-new .select-address').height() +
                        $('.term-select-new .select-discounts').height() > (winWidth > 360 ? 190 : winWidth > 320 ? 175 : 150));
        },
        afterInit:function(c) {
           if (needAddress) {
                        that.loadAddress();

                        // 若是須要地址,且是 app 的話,屏幕可見性切換時須要更新下地址
                        if (isApp) {
                            $(document).on(visibilityChange, function (e) {
                                // console.log('visibilityChange',document[hidden]);
                                if (!document[hidden]) {
                                    // true 參數表示必須刷新
                                    that.loadAddress(true);
                                }
                            });
                        }
                    }
                    that.afterTermSelect();

                    $dom.on('click', '.layer-close', function() {
                        setTimeout(function() {
                            !opt.noAutoHide && layer.hide();
                        }, 100);

                        opt.onCancel && opt.onCancel();
                    });

                    $dom.on('click', '.term', function(e) {
                        var $this = $(this);
                        var $terms = $('.term');

                        if (!$this.hasClass('disabled')) {
                            $terms.removeClass('selected');
                            $this.addClass('selected');
                        }

                        that.afterTermSelect();
                    });

                    $dom.on('click', '.layer-comfirm', function(e) {
                        var $this = $(this);
                        var termId = $dom.find('.term.selected').data('term-id');
                        var termName = $dom.find('.term.selected').find('.term-title').html();
                        var discountId = $dom.find('.discounts-list_item.selected').data('discount-id');
                        var couId = $dom.find('.discounts-list_item.selected .discounts-coupon').data('cou-id');
                        var directPay = false; 
                        // ios 手Q IAP
                        if (that.toRecharge) {
                            // 須要充值的金額數目
                            var toRechargePrice = that.curPrice - that.balance;
                            if (isIosApp) {
                                require.async('api', function (api) {
                                    api.invoke('api', 'balanceRecharge', {
                                        amount: toRechargePrice
                                    });

                                    // 充值完成設置回調
                                    api.addEventListener('balanceRechargeCallBack', function(data) {
                                        // 支付成功的話
                                        // code=0爲成功,其餘表示失敗
                                        // mode=1表示走充值檔位回調,2表示直接充值回調,若是ios 直接充值成功則直接支付
                                        var directPay = data.code === 0 && data.mode === 2;

                                        // 執行回調刷新數據
                                        that.toGetBalance(that.course, termId, function() {
                                            directPay && $this.trigger('click');
                                        });
                                    });
                                });
                            } else {
                                var toRechargePrice = that.curPrice - that.balance;
                                if (that.rechargeMap &&
                                    Object.keys(that.rechargeMap).indexOf("" + toRechargePrice) > -1) {
                                    that.opt.onComfirmClick && that.opt.onComfirmClick(1);
                                    iosPay.iosRecharge({
                                        productId: that.rechargeMap[toRechargePrice],
                                        count: toRechargePrice,
                                        succ: function() {
                                            that.toGetBalance(that.course, $('.term.selected').data('term-id'));
                                        }
                                    });
                                } else {
                                    that.opt.onComfirmClick && that.opt.onComfirmClick(2);
                                    // T.jump('/iosRecharge.html?_bid=167&_wv=2147483651');
                                    that.jumpPage('/iosRecharge.html?_bid=167&_wv=2147483651');
                                }
                            }
                            return;
                        }

                        if (!termId) {
                            require.async(['modules/tip/tip'], function(Tip) {
                                Tip.show(opt.dialogTitle);
                            });
                            return true;
                        }

                        // check address
                        if (needAddress && !that.addressid) {
                            if (course.must_fill_mailing || !$dom.find('.select-address').hasClass('z-no')) {
                                // 沒填地址的話地址框要標紅,而後須要滑到視窗讓用戶看到
                                var $cnt = $dom.find('.select-content');
                                var $addressWrap = $dom.find('.select-address_wrapper').addClass('z-err');
                                var cntRect = $cnt[0].getBoundingClientRect();
                                var addressBoxRect = $addressWrap[0].getBoundingClientRect();
                                // console.log('>>>>> ', cntRect, addressBoxRect);
                                if (addressBoxRect.bottom > cntRect.bottom) {
                                    $cnt.scrollTop($cnt.scrollTop() + (addressBoxRect.bottom - cntRect.bottom));
                                }
                                return;
                            }
                        }

                        if (that.isAllBalance && that.opt.onComfirmClick) {
                            that.opt.onComfirmClick(3);
                        }
                        opt.cb && opt.cb(termId, discountId, couId, termName, that.isAllBalance, that.payBalance, that.addressid);

                        setTimeout(function() {
                            !opt.noAutoHide && layer.hide();
                        }, 300);
                    });

                    $dom.on('click', '.discounts-list_item', function(e) {
                        var $this = $(this);
                        var $discounts = $('.discounts-list_item');
                        var isSelected = $this.hasClass('selected');

                        if (!$this.hasClass('disabled')) {
                            $discounts.removeClass('selected');
                            $this.addClass(isSelected ? '' : 'selected');

                            that.setPayPrice();
                        }
                    });

                    $dom.on('click', '.address-person .i-edit2, .address-add', function() {
                        var termId = $dom.find('.term.selected').data('term-id');
                        var courseId = that.course.cid;
                        var src = '/addrEdit.html?_bid=167&_wv=2147483649&ns=1&fr=' + (location.pathname.indexOf('allCourse.html') > -1 ?
                            4 : location.pathname.indexOf('courseDetail.html') > -1 ? 2 : 3) + '&course_id=' + courseId + '&term_id=' + termId;
                        // T.jump(src);
                        that.jumpPage(src);
                    }).on('click', '.select-address_title .i-right-light', function(e) {
                        var $addressDom = $dom.find('.select-address');
                        var isOpen = !$addressDom.hasClass('z-no');

                        if (isOpen) {
                            $addressDom.addClass('z-no');
                            that.theAddressid = that.addressid;
                            that.addressid = undefined;
                        } else {
                            $addressDom.removeClass('z-no');
                            that.addressid = that.theAddressid;
                        }
                    });
                }
            });

        } else {
            opt.cb && opt.cb(opt.curTermId || preSelectedTermId);
        }
}
複製代碼

單一職責並不必定要經過不少函數來完成,也能夠用分段達到目的,如同這樣:

show(data) {
    data && this.setData(data);
    const renderData = {
        data: this.data,
        courseData: this.data.courseData,
        termList: this.termList,
        userInfo: this.userInfo,
        addrList: this.addrList,
        isIAP: this.isIAP,
        balance: betterDisplayNum(this.balance),
        curPrice: betterDisplayNum(this.curPrice),
        curTermId: this.curTermId,
        discountList: this.discountList,
        curDisId: this.curDisId,
        jdSelectId: this.jdSelectId,
        curAddrId: this.curAddrId
    };
    const formatters = {
        // formatters
        termFormatter,
        priceFormatter,
        okBtnFormatter,
        balanceFormatter,
        priceFormatterWithDiscount
    };
    console.log('[render data]: ', renderData);
    const html = payLayerTpl(renderData, formatters);

    // 記錄滾動條位置
    this._setScrollTop();

    // 防止重複append
    if (this.$view) {
        this.$view.replaceWith(html);
    } else {
        this.$container.append(html);
    }

    afterUIRender(() => {
        this.$view = $('.' + COMPONENT_NAME).show();        
        this._setContentHeight();   // 動態設置滾動區域的高度
        this._restoreScrollTop(); // 恢復滾動位置
        this._initEvent();   
        this._initCountDown();  // 限時折扣倒計時         
    });
}
複製代碼

雖然這個函數也沒有維持單一職責,但經過「分段」的形式清晰的代表了內部的流程邏輯,這樣的代碼看起來就會比全部細節揉在一個函數中好不少。

對於單一職責來講,保持起來仍是比較困難的,主要在於職責的拆分,有時過於細緻的職責拆分也會給閱讀帶來比較大的困難,對於這種狀況,仍是拿寫做來對比,單一職責至關於文章的一個「段落」,對於文章來講,每一個段落都有它的中心思想,能夠用一句話描述出來,若是你發現函數的中心思想很模糊,或者須要不少語言去描述它,那也許它已經有不少個職責該拆分了。

3. LKP(Least Knowledge Principle)

LKP原則是最小知識原則,又稱「迪米特」法則,也就是說,一個對象應該對另外一個對象有最少的瞭解,你內部如何複雜都不要緊,我只關心調用的地方。

保持暴露接口的簡介易用性也是API設計的通用規則,在實際中發現了這樣的一個UI組件:

module.exports = {
	show: function(course, opt) {
		// 此處省略一堆邏輯
	},
	jumpPage: function(url) {
		// 此處省略一堆邏輯
	},
	afterTermSelect: function() {
		// 此處省略一堆邏輯
	},
	setPrice: function() {
		// 此處省略一堆邏輯
	},
	setBalance: function() {
		// 此處省略一堆邏輯
	},
	toGetBalance: function(course, curTermId, cb) {
		// 此處省略一堆邏輯
	},
	setDiscounts: function(course, curTermId, curPrice) {
		// 此處省略一堆邏輯
	},
	filterDiscounts: function(discounts, curPrice) {
		// 此處省略一堆邏輯
	},
	isSuitCoupon: function(cou, curPrice) {
		// 此處省略一堆邏輯
	},
	setPayPrice: function() {
		// 此處省略一堆邏輯
	},
	setTermTips: function(wording) {
		// 此處省略一堆邏輯
	},
	loadAddress: function(needUpdate) {
		// 此處省略一堆邏輯
	},
	setAddress: function(addressid) {
		// 此處省略一堆邏輯
	}
}
複製代碼

這個UI組件暴露了很是多的方法,有業務邏輯,有視圖邏輯,還有工具方法,這時會給維護者帶來比較大的困擾,本能的覺得這些暴露出去的方法都在被使用,因此想重構其中某些方法都有些很差下手,而實際上,外部調用的方法僅僅是show而已。

一個好的封裝,不管內部多麼複雜,它暴露出來的必定是最簡潔實用的接口,而內部邏輯是獨立維護的,如上述代碼,做爲一個UI組件來講,提供最基本的show/hide方法便可,有必要時可加入update方法自更新,而無需暴露衆多細節,形成調用者和維護者的困擾。

4. 可讀性基本定理

可讀性基本定理——「代碼的寫法應當使別人理解它所需的時間最小化」

代碼風格和原則不是一律而論的,咱們常常須要對一些編碼原則和方案進行取捨,例如對於三元表達式的取捨,當咱們以爲兩種方案都佔理時,那麼惟一的評判標準就是可讀性基本定理,不管寫法多麼的高超炫技,最好的代碼依舊是讓人第一時間可以理解的代碼。

5. 有意義的名稱

代碼的可讀性絕大部分依賴於變量和函數的命名,一個好的名稱可以一針見血地幫助維護者理解邏輯,如同寫文章中的「文筆」,文筆優異者總能將故事娓娓道來,引人入勝。

不過要起好名稱仍是很難的,尤爲是咱們不是以英語爲母語,更是添加了一層障礙,有些人認爲糾結在名稱上會致使效率變低,開發第一時間應該完成需求的開發。這樣說並無錯,咱們在開發過程當中應當專一於功能邏輯,但不要徹底忽視命名,所謂「文筆」是須要鍛鍊的,思考的越多,命名就會越發的水到渠成,到後來也就不太會影響工做效率了。

在這裏推薦鮑勃大叔提到的童子軍規,每一次看本身的代碼,都進行一次重構,最簡單的重構即是更名,也許一開始以爲命名還比較貼合,但邏輯越寫越不符合初始的命名了,當回顧代碼時,咱們能夠順手對變量和方法進行從新命名,現代編輯工具也很容易作到這一點。

文不對題的命名是最可怕的,如:

function checkTimeConflict(opts) {
    if (opts.param.passcard || (T.bom.get('autopay') && T.bom.get('term_id'))) { 
        selectToPay({
            result: {}
        }, opts);
    } else {
        DB.checkTimeConflict({
            param: {
                course_id: opts.param.courseId,
                term_id: opts.param.termId
            },
            succ: function(data) {
                selectToPay(data, opts);
            },
            err: function(data) {
                dealErr(opts, data);
            }
        });
    }
}
複製代碼

這個函數被命名爲check*開頭的,本意是檢測課程時間是否衝突,但內部邏輯卻包含了支付整個流程,此時對於調用者來講,若是不去細看內部邏輯,頗有可能就會錯誤的認爲check函數沒有反作用致使事故發生。

6. 適當的註釋維護

註釋是一個比較有爭議性的話題,有人認爲可讀的函數變量就很清晰,不須要額外的註釋,且註釋有不可維護性,如:

// 1-PC, 2-android手QH5, 3-android APP, 4-ios&非手QH5, 5-IOS APP
var platform = isAndroidApp ? 3 : isIosApp ? 5 : 4;
複製代碼

實際上,這個字段的含義早已發生了改變,但因爲修改者只修改了邏輯,並無注意到這一行註釋,致使這個老註釋提供了錯誤信息,此時的註釋不只變成了無效註釋,甚至會致使維護人的誤解,形成bug的產生。

對於這種狀況,要麼維護註釋,要麼在註釋裏面註明接口文檔,維護文檔,在其餘狀況下,適當的註釋是有必要的,對於複雜的邏輯,若是有一個簡練的註釋,對於代碼可讀性的幫助是極大的,但有些沒必要要的註釋能夠去掉,註釋的取捨關鍵在於可讀性基本定理,如:

const filterFn = (term) => {
    if (rule.hideEndTerms && term.is_end) {
        return false;   // 隱藏已結束的期
    }
    if (rule.hideSignEndTerms && term.is_out_of_date) {
        return false;   // 隱藏已結束報名的期
    }
    if (rule.hideAppliedTerms && courseUtil.isTermApplied(term)) {
        return false;   // 隱藏已報名的期
    }
    if (rule.hideZeroAllowedTerms && courseUtil.isTermZero(term)) {
        return false;   // 隱藏名額已滿的期
    }
    if (rule.productType === productType.PACKAGE) {
        return false;   // 隱藏課程包的班級
    }

    return true;
};
複製代碼

對於上述邏輯來講,雖然經過變量能夠大體猜出功能含義,但一眼看上去就能清晰掌握邏輯結構,歸功於註釋的簡明與清晰。

小結

本文提到的6個代碼編寫的原則,前三個偏向於代碼維護性,後三個偏向於代碼可讀性,整個可維護性和可讀性構成了代碼的基本素養。做爲一名前端開發工程師,想要擁有良好的代碼素養,首先要讓本身的代碼可維護,不給別人的維護帶來巨大的成本和工做量,其次儘可能保證代碼的美觀可讀,整潔的代碼人見人愛,如同閱讀一本好書,使人心情愉悅。

」代碼素養「是一種態度,真正熱愛編程的程序員必定不會缺失「代碼素養」。咱們一般稱「寫代碼」爲「程序設計」,而不是「程序編寫」,「設計」一詞體現出了咱們的代碼是一件做品,也許不如「藝術品」那麼精緻,但也不是什麼粗麻爛布,若是在寫代碼時天馬行空,得過且過,抱着只要能實現功能的思想,那這部「做品「是不具備觀賞價值的,這不只僅體現出代碼編寫者的」不專業」,更是反映出對待編程這件事的態度,代碼的整潔程度、可維護性取決於你是否真正「在乎」你的代碼,每一個程序員不必定熱愛編程,但請你必定要以「認真」的態度對待本身的專業。

"clean code"的做者鮑勃大叔提到,有人曾送給他一條腕帶,上面寫着「Test Obsessed」,他發覺本身帶上後再也沒法取下了,不只是由於腕帶很緊,更是由於它也是一條精神上的緊箍咒。在編程時,咱們下意識的看下本身的手腕,是否能發現一條隱形的腕帶呢?

相關文章
相關標籤/搜索