1. 程式人生 > >怎麼去寫好一段優雅的程式

怎麼去寫好一段優雅的程式

此文已由作者吳維偉授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。

寫好一段優雅程式的必要條件是良好的設計。

寫程式就像在走一個迷宮。編寫之初,有若干個可能的解決方案縈繞在我們的腦海。我們選擇一個繼續深入,可能達到終點——實現了功能需求,但更大的可能是進入了一個死衚衕或者一個新的岔路口,需要重新進行抉擇,如此反覆。

想起一年前的自己,僅憑著生物的本能去寫著程式碼:我依照著以往的經驗,先寫了一段。然後重新整理一下頁面,檢視是否離實現需求更近了一步。幻想著程式可以完美執行的我看到最多的是JavaScript報錯和意料之外的執行結果,那種被QA們稱作BUG的東西。於是,我又憑著本能做出了修改……恍恍惚惚,不知經過了多久,程式終於執行在一個貌似正確的邏輯軌道上了。嗯?你問我程式裡會不會有什麼bug?這個,我還真不敢確定呢。

我需要一份迷宮的地圖,避開所有的死衚衕,找到一條最優的路徑到達出口,這就是設計。

我們設計一段程式與PM規劃一個產品的過程有些類似——首先對需求進行收集和整理,然後明確需要實現的N條功能,最後依次進行實現。不同的是,我們的使用者就是我們自己,所以我們更具優勢,更容易設計出一段易於使用的程式。

設計程式的第一步是明確程式中需要實現的功能點。許多的功能點羅列在面前,是把它們實現在一個模組裡呢,還是分多個模組去實現?如果分多個模組,每個模組都要實現哪些功能點呢?這些問題當然不能冒然的拍腦門決定,需要考慮可複用性和維護性。

想象一個登陸功能,需求是這樣的:我們需要把使用者資訊傳送給後端進行驗證,如果成功則重新整理頁面。功能很簡單,很容易把程式碼寫了出來:

//模組邏輯class Login {    login () {        this.verify(function () {            window.location.reload();        });    }        /**     *  @description 驗證使用者資訊。     *  @param callback {Function} 驗證通過後執行的回撥函式     */    verify (callback) {        //do something    }}//模組呼叫new Login().login();

瞬間搞定,So Easy!

弄完沒多久,來了新需求。假設剛剛是頁面A,現在要實現的頁面B中的登陸邏輯與A有了一些不同:登陸成功後不再進行頁面重新整理,而是直接更新頁面內關於使用者資訊的顯示。

現在要怎麼實現呢?從零開始重新實現一個頁面B的登陸邏輯?首先排除這種做法,畢竟驗證使用者資訊這部分邏輯並沒有發生改變,可以複用。想了想,寫下了這樣的程式碼:

//模組邏輯class Login {    /**     *  @param state {Number} 1 登陸後重新整理  2 登陸後更新使用者資料     */    login (state) {        this.verify(function () {            if(state === 1)                window.location.reload();            else if(state === 2)                doSomethingWithUserInfo();        });    }        /**     *  @description 驗證使用者資訊。     *  @param callback {Function} 驗證通過後執行的回撥函式     */    verify (callback) {        //do something    }}//模組呼叫//登陸後重新整理new Login().login(1);//登陸後更新使用者資料new Login().login(2);

嗯,很好地滿足了需求。但是這種實現方式過於僵硬,不太靈活。假設有頁面C,頁面D,其登陸邏輯中登陸成功後執行的操作又有不同。此時需要再次修改`Login.prototype.login`方法中的實現,那麼以前能夠穩定執行的邏輯就會有被改壞的可能。好的程式結構應該對擴充套件開放,對修改關閉。就是說,期望中,無論又增加哪些登陸成功後執行的操作,我們都不需要修改原來的程式碼。

所以,重構了下:

//模組邏輯class Login {        login () {        this.verify(function () {            this.doAfterLogin();        }.bind(this));    }        /**     *  @description 驗證使用者資訊。     *  @param callback {Function} 驗證通過後執行的回撥函式     */    verify (callback) {        //do something    }        /**     *  @abstract     */    doAfterLogin () {        //子類實現具體邏輯    }}class LoginA extends Login {    doAfterLogin () {        window.location.reload();    }}class LoginB extends Login {    doAfterLogin () {        doSomethingWithUserInfo();    }}//模組呼叫//登陸後重新整理new LoginA().login();//登陸後更新使用者資料new LoginB().login();

將一些公用的邏輯提取到父類`Login`中。登陸成功後的操作每一次變化,只需要繼承`Login`類,在新的子類中實現具體的邏輯。這樣對已有功能不會產生任何影響。簡直完美!

沾沾自喜中,又來了新需求。假設現在又要實現頁面E,頁面F。頁面E中登陸後的操作是重新整理頁面,與A相同。頁面F中登陸後的操作是更新使用者資訊展示,與B相同。但是它們不再通過自己的後端來驗證使用者資訊,而是通過URS和VRS(不要問我VRS是什麼鬼……)。現在需要複用的部分不僅僅是對使用者資訊進行驗證的功能,還有登陸成功後執行的操作。面對這樣的需求,僅僅通過繼承是不能將已有功能最大化複用的,需要將登陸驗證和登陸後執行的操作這2個功能點劃分到不同的模組中。於是,可以這樣實現:

//模組邏輯class Verify {    /**     *  @abstract     *  @description 驗證使用者資訊。     *  @param callback {Function} 驗證通過後執行的回撥函式     */    verify (callback) {        //子類實現具體邏輯    }}class VerifyNormal extends Verify {    verify (callback) {       //通過自己後臺進行驗證    }}class VerifyURS extends Verify {    verify (callback) {        //通過URS進行驗證    }}class VerifyVRS extends Verify {    verify (callback) {        //通過VRS進行驗證    }}class Login {        /**     *  @param verify {Verify}     */    login (verify) {        verify.verify(function () {            this.doAfterLogin();        }.bind(this));    }        /**     *  @abstract     */    doAfterLogin () {        //子類實現具體邏輯    }}class LoginA extends Login {    doAfterLogin () {        window.location.reload();    }}class LoginB extends Login {    doAfterLogin () {        doSomethingWithUserInfo();    }}//模組呼叫//普通登陸,登陸後重新整理頁面new LoginA().login(new VerifyNormal());//普通登陸,登陸後更新使用者資訊顯示new LoginB().login(new VerifyNormal());//URS登陸,登陸後重新整理頁面new LoginA().login(new VerifyURS());//URS登陸,登陸後更新使用者資訊顯示new LoginB().login(new VerifyURS());//VRS登陸,登陸後重新整理頁面new LoginA().login(new VerifyVRS());//VRS登陸,登陸後更新使用者資訊顯示new LoginB().login(new VerifyVRS());

總結一下:如果一個模組只有一個變化的原因(只有登陸後的操作會變化時),可以通過繼承來滿足開閉原則(對擴充套件開放,對修改關閉)。但是如果一個模組有多個變化的原因(如登陸後的操作和登陸驗證流程都會發生變化),我們就需要把其中一個變化原因劃分到另外一個模組中。一個模組只能有一個變化的原因(單一職責原則)。將功能點友好地劃分到每一個模組,那麼一段好程式的雛形也就被塑造出來了,剩下的就是往裡面狠狠的填充。

網易雲免費體驗館,0成本體驗20+款雲產品! 

更多網易技術、產品、運營經驗分享請點選