怎麼去寫好一段優雅的程式
此文已由作者吳維偉授權網易雲社群釋出。
歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。
寫好一段優雅程式的必要條件是良好的設計。
寫程式就像在走一個迷宮。編寫之初,有若干個可能的解決方案縈繞在我們的腦海。我們選擇一個繼續深入,可能達到終點——實現了功能需求,但更大的可能是進入了一個死衚衕或者一個新的岔路口,需要重新進行抉擇,如此反覆。
想起一年前的自己,僅憑著生物的本能去寫著程式碼:我依照著以往的經驗,先寫了一段。然後重新整理一下頁面,檢視是否離實現需求更近了一步。幻想著程式可以完美執行的我看到最多的是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+款雲產品!
更多網易技術、產品、運營經驗分享請點選。