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

怎麽去寫好一段優雅的程序

結構 == 代碼規範 必要條件 需要 程序 維護 本質 .com

此文已由作者吳維偉授權網易雲社區發布。

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


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


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


想起一年前的自己,僅憑著生物的本能去寫著代碼:我依照著以往的經驗,先寫了一段。然後刷新一下頁面,查看是否離實現需求更近了一步。幻想著程序可以完美運行的我看到最多的是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+款雲產品!

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


相關文章:
【推薦】 一份ECMAScript2015的代碼規範(下)
【推薦】 理解DDoS防護本質:基於資源較量和規則過濾的智能化系統

怎麽去寫好一段優雅的程序