背景
在過去對框架的設計中,我收到過的最有用的建議是:“不要一開始就根據現有的技術去整合和改進。而是先搞清楚你覺得最理想的框架應該是怎樣的,再根據現在的技術去評估,的確實現不了時再妥協。這樣才能做出真正有意義的框架。”
在這篇文章裡,就讓我們按照這樣一條建議來探索一下現在的 web 框架最終可以進化成的樣子,你絕對會被驚豔到。
前端,還是從前端說起。前端目前的現狀是,隨著早期的 Backbone,近期的 Angular、React 等框架的興起,前端在 模組化、元件化 兩個方向上已經形成了一定的行業共識。在此基礎上,React 的 FLUX、Relay 則是進一步的對前端應用架構的探索。這些技術在目前國內的大公司、大團隊內部實際上都落地得非常好,因為很容易和公司內部已有的後端技術棧結合。而且這些純前端框架的配套技術方案一般比較成熟,例如在支付寶確定使用 React,其實有一部分原因是它相容 IE8,並且有伺服器端渲染方案來加速首屏。
相比之下,像 Meteor 這類從前到後包辦的框架就較難落地。雖然能極大地提高開發效率,整體架構非常先進,但架構的每一個層級往往不容易達到行業內的頂尖標準。特別是在伺服器端,對大公司來說,通常都有適合自己業務的伺服器叢集、資料庫方案,並且經受過考驗。因此當一個團隊一上手就要做面向十萬級、甚至百萬級使用者的產品時,是不太願意冒風險去嘗試的。反而是個人開發者、創業型的團隊會願意去用,因為確實能在短時間內高效地開發出可用的產品出來。包括像 Leancloud 提出的這型別的服務,也是非常受歡迎的。
這種現狀,就是理想和現實的一個爭論。Meteor 的方式能滿足我對開發效率的理想,而團隊已有的技術方案能保障穩定。能否整合其中的優勢,不妨讓我們進一步來細化一下對框架的希望:
– 有強大的前後端一致的資料模型層
– 程式碼可以可以複用。例如我有一個 User 模型,當我建立一個新的 user 時,user 上的欄位驗證等方法是前後端通用的,由框架自動幫我區別前後端環境。
– 資料模型和前端框架沒有耦合,但可以輕鬆結合。這樣在前端渲染型的框架進一步升級時,不影響我的業務邏輯程式碼。
– 由資料模型層提供自動的資料更新機制。例如我在前端要獲取 id 為 1 的使用者,並且如果伺服器端資料有更新的話,就自動幫我更新,不需要我自己去實現輪詢。我希望的程式碼寫法是:
1 2 3 |
var user = new User({id:1}); user.pull(); user.watch(); |
實際上,Meteor已經能實現絕大部分上述功能。但這不是軟文。我要強調兩點我不希望的:
– 我不希望這個資料模型層去包含業務邏輯,也就是我建立的user物件,我不希望它提供 login、logout 等 api。
– 我也不希望資料模型層自動和任何ORM框架繫結,提供任何 SQL 或 NoSQL 的資料支援。
看到這兩點你可能心中大打問號,這兩點不正是高效的精髓嗎?前後端邏輯複用,遮蔽資料庫細節。別急,讓我們重新用“理想的方式”來思考一下“邏輯”和“資料持久化”這兩件事。
資料與邏輯
我們以這樣一個問題開頭:任何一個應用,我們的程式碼最少能少到什麼程度?
這算半個哲學問題。任何人想一想都會得到同一個答案:最少也就少到和應用本身的描述一一對應而已了。什麼是應用描述?或者說什麼是應用?我們會這樣描述一個部落格:“使用者可以登入、退出。使用者登入後可以發表文章。發表文章時可以新增相應的標籤。”
抽象一下描述,答案很簡單:資料,和邏輯。
如果你在一個流程要求嚴格的公司,應用描述就是prd或系分文件。應用的資料就是資料字典,應用的邏輯就是流程圖的總和:
流程圖
那麼程式碼最少能怎麼寫呢?資料很簡單,參照資料字典,我們來用一種即使是產品經理都能掌握的偽碼來寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//描述欄位 User : { name : string } Post : { title : string, content : text } Tag : { name : string } //描述關係 User -[created]-> Post Post -[has]-> Tag |
這裡為了進一步幫助讀者從已有的技術思維中跳出來,我想指出這段偽碼和資料庫欄位描述有一個很大的區別,那就是:我不關心 User 和 Post 中間的關聯關係到底是在兩者的欄位中都建立一個欄位來儲存對方的id,還是建立一箇中間表。我只關心我描述它時的邏輯就夠了。資料描述的程式碼,最簡也就簡單到這個程度了。
那麼邏輯呢?我們先用按常規方式試試?
1 2 3 4 5 6 7 |
class User{ createPost( content, tags=[] ){ var post = new Post({content:content}) post.setTags( tags.map(tagName=>{ return new Tag(tagName)} ) ) return post } } |
好像還不錯,如果今天產品經理說我們增加一個 @ 功能,如果文章裡 @ 某個使用者,那麼我們就發個站內信給他。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class User{ createPost( content, tags=[] ){ var post = new Post({content:content}) post.setTags( tags.map(tagName=>{ return new Tag(tagName)} ) ) if( at.scan(content) ){ at.getUser(content).forEach( atUser =>{ system.mail( atUser ) }) } return post } } |
你應該意識到我要說什麼了,像網際網路這種可以快到一天一個迭代的開發速度,如果沒有一個好的模式,可能用不了多久,新加的功能就把你的 createPost 搞成了800行。當然,我也並不是要講設計模式。程式碼中的設計模式,完全依賴於程式設計師本人,我們要思考的是從框架層面提供最簡單的寫法。
讓我們再回到哲學角度去分析一下業務邏輯。
我們所謂的邏輯,其實就是對一個 具體過程的描述 。在上面這個例子裡,過程無非就是新增標籤,全文掃描。描述一個過程,有兩個必備點:
– 幹什麼
– 順序
順序為什麼是必備的?某天上面發了檔案說標題裡帶 XXX 的文章都不能發,於是你不得不在函式一開始時就進行檢測,這時就必須指定順序。
如果我們用左右表示會互相影響的順序,從上下表示互不相干的順序,把上面的最初的流程圖重畫一下:
這是一棵樹。如果我們再加個功能,新增的標籤如果是某個熱門標籤,那麼我們就把這篇文章放到網站的熱門推薦裡。這棵樹會變成什麼樣子呢:
是的,事實上人類思維中的任何過程,都可以畫成一棵樹。有條件的迴圈可以拆解成遞迴,最終也是一棵樹。但重點並不是樹本身,重點是上面這個例子演化的過程,從一開始最簡單的需求,到加上一點新功能,再到加上一些噁心的特殊情況,這恰恰就是真實世界中 web 開發的縮影。真實世界中的變化更加頻繁可怕。其中最可怕的是,很多時候我們的程式結構、用到的設計模式,都是適用於當前的業務模型的。而某天業務模型變化了,程式碼質量又不夠好的話,就可能遇到牽一髮動全身,大廈將傾的噩夢。幾乎每個大公司都有一個“執行時間長,維護的工程師換了一批又一批”的專案。Amazon曾經有個工程師描述維護這種專案的感覺:“climb the shit mountain”。
回到之前的話題,在邏輯處理上,我們的理想是寫出的程式碼即短,又具有極高的可維護性和可擴充套件性。
更具體一點,可維護性,就是程式碼和程式碼結構,能最大程度地反映業務邏輯。最好我的程式碼結構在某種程度上看來和我們流程圖中的樹一樣。這樣我讀程式碼,就幾乎能理解業務邏輯。而可擴充套件性,就是當出現變化時,我能在完成變化時,能儘量少地去修改之前的程式碼。同樣的,如果我們能保障程式碼和程式碼結構能和流程圖儘量一致,那麼在修改時,圖上怎麼改,我們程式碼就怎麼改。這也就是理論上能達到的最小修改度了。綜上,我們用什麼樣的系統模型能把程式碼變得像樹形結構一樣?
很簡單,事件系統就可以做到。我們把都一個業務邏輯當做事件來觸發,而具體需要執行的操作單做監聽器,那麼上面的程式碼就可以寫成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// emitter 是事件中心 emitter.on("post.create", function savePost(){...}) emitter.on("post.create", function createTags(){...}, {before:"savePost"}) emitter.on("post.create", function scanSensitiveWords( post ){ if( system.scanSensitiveWords( post ) ){ return new Error("you have sensitive words in post.") } }, {block:all}) emitter.on("post.create", function scanPopTags(){...}) |
1 2 |
//執行建立文章操作 emitter.fire("post.create", {...args}) |
這樣看來,每個操作的程式碼變得職責單一,整體結構也非常工整。值得注意的是,在這段偽碼裡,我們用了 {before:"savePost"}
這樣的引數來表示操作的順序,看起來也和邏輯本身的描述一致。
讓我們回到可維護性和可擴充套件性來檢查這種寫法。首先在可維護性上,程式碼職責變得很清晰,並且與流程描述一致。不過也有一個問題,就是操作的執行順序已經無法給人巨集觀上的印象,必須把每個監聽器的順序引數拼起來,才能得到整體的順序。
在可擴充套件性上,無路是新增還是刪除操作,對應到程式碼上都是刪除或新增相應的一段,不會影響到其他操作程式碼。我們甚至可以把這些程式碼拆分到不同的檔案中,當做不同的模組。這樣在增減功能時,就能通過增刪檔案來實現,這也為實現一個檔案級的模組管理器提供了基礎技術。
至此,除了無法在執行順序上有一個巨集觀印象這個問題,似乎我們得到了理想的描述邏輯的方式。那我們現在來攻克這最後一個問題。拿目前的這段偽碼和之前的比較,不難發現,之前程式碼需要被執行一遍才能較好地得到其中函式的執行順序,才能拿到一個呼叫棧。而現在的這段程式碼,我只要實現一個簡單的 emitter,將程式碼執行一遍,就已經能得到所有的監聽器資訊了。這樣我就能通過簡單的工具來得到這個巨集觀的執行順序,甚至以圖形化的方式展現出來。得到的這張圖,不就是我們一模一樣的流程圖嗎?!
不知道你有沒有意識到,我們已經開啟了一扇之前不能開啟的門!在之前的程式碼中,我們是通過函式間的呼叫來組織邏輯的,這和我們現在的方式有一個很大的區別,那就是:用來封裝業務邏輯的函式,和系統本身提供的其他函式,沒有任何可以很好利用的區別,即使我們能得到函式的呼叫棧,這個呼叫棧用圖形化的方式列印出來也沒有意義,因為其中會參雜太多的無用函式資訊,特別是當我們還用了一些第三方類庫時。列印的結果可能是這樣:
而現在,我們用來表述業務的某個邏輯,就是事件。而相應的操作,就是監聽器。監聽器無論是觸發還是註冊,都是通過 emitter 提供的函式,那麼我們只需要利用 emitter,就能列印出只有監聽器的呼叫棧。而監聽器的呼叫棧,就是我們的流程圖。
程式碼結構可圖形化,並且是有意義的可圖形化,這扇大門一旦開啟,門後的財富是取之不盡的。我們從 開發、測試、監控 三個方面來看我們能從中獲得什麼。
在開發階段,我們可以通過呼叫棧生成圖,那通過圖來生成程式碼還會難嗎?對於任何一份流程圖,我們都能輕易地直接生成程式碼。然後填空就夠了。在除錯時、我們可以製作工具實時地列印出呼叫棧,甚至可以將呼叫時儲存的傳入傳出值拿出來直接檢視。這樣一旦出現問題,你就可以直接根據當前儲存的呼叫棧資訊排查問題,而再無需去重現它。同理,繁瑣的斷點,四處列印的日誌都可以告別了。
測試階段,既然能生成程式碼,再自動生成測試用例也非常容易。我們可以通過工具直接檢測呼叫棧是否正確,也可以更細緻地給定輸入值,然後檢測各個監聽器的傳入傳出值是否正確。
同樣很容想到監控,我們可以預設將呼叫棧的資料建構作為日誌保留,再用系統的工具去掃描、對邊,就能自動實現對業務邏輯本身的監控。
總結一下上述,用事件系統去描述邏輯、流程,使得我們程式碼結構和邏輯,能達到一個非常理想的對應程度。這個對應程度使得程式碼裡的呼叫棧資訊就能表述邏輯。而這個呼叫棧所能產生的巨大價值,一方面在於可圖形化,另一方面則在於能實現測試、監控等一系列工程領域的自動化。
到這裡,我們已經得到了兩種理想的表達方式來分別表述資料和邏輯。下面真正激動人心的時刻到了,我們來關注現實中的技術,看是否真的能夠做出一個框架,讓我們能用一種革命性的方式來寫應用?
理想到現實
首先來看資料描述語言和和資料持久化。你可能早已一眼看出 User -[create]-> Post
這樣的偽碼是來自圖資料庫 Neo4j 的查詢語言 cypher 。在這裡我對不熟悉的讀者科普一下。Neo4j 是用 java 寫的開源圖資料庫。圖資料本身是以圖的方式去儲存資料。
例如同樣對於 User 這樣一個模型,在 關係型資料庫中就是一張表,每一行是一個 user 的資料。在圖資料庫中就是一堆節點,每個節點是一個 user。當我們又有了 Post 這個模型時,如果要表示使用者建立了 Post 這樣一個關係的話,在關係型資料庫裡通常會建立一箇中間表,存上相應 user 和 post 的 id。也或者直接在 user 或 post 表裡增加一個欄位,存上相應的id。不同的方案適用於不同的場景。而 在圖資料庫中要表達 user 和 post 的關係,就只有一種方式,那就是建立一個 user 到 post 的名為 CREATED 的 關係。這個關係還可以有屬性,比如 {createdAt:2016,client:”web”} 等。
你可以看出圖資料和關係型資料庫在使用上最大的區別是,它讓你完全根據真實的邏輯去關聯兩個資料。而關係型資料庫則通常在使用時就已經要根據使用場景、效能等因素做出不同的選擇。
我們再看查詢語言,在 SQL 中,我們是以SELECT ... FROM
這樣一種命令式地方式告訴資料怎樣給我我要的資料。語句的內容和存資料的表結構是耦合的。例如我要找出某個 user 建立的所有 post。表結構設計得不同,那麼查詢語句就不同。而在 Neo4js 的查詢語句 cypher 中,是以 (User) -[CREATED] ->(Post)
這樣的 模式匹配 的語句來進行查詢的。這意味著,只要你能以人類語言描述自己想要的資料,你就能自己翻譯成 cypher 進行查詢。
除此之外,圖資料當然還有很多高階特性。但對開發者來說,模式匹配式的查詢語句,才是真正革命性的技術。熟悉資料庫的讀者肯定有這樣的疑問:
其實很多 ORM 就能實現 cypher 現在這樣的表達形式,但在很多大公司裡,你會發現研發團隊仍然堅持手寫 SQL 語句,而堅決不用 ORM。理由是,手寫 SQL 無論在排查問題還是優化效能時,都是最快速的。特別是對於大產品來說,一個 SQL 就有可能節約或者損失鉅額資產。所以寧願用 “多人力、低效率” 去換 “效能和穩定”,也不考慮 ORM。那麼 cypher 如何面對這個問題?
確實,cypher 可以在某種程度上理解成資料庫自帶的 ORM。它很難通過優化查詢語句來提升效能,但可以通過其他方式。例如對耗時長的大查詢做資料快取。或者把儲存分層,圖資料庫變成最底層,中間針對某些應用場景來使用其他的資料庫做中間層。對有實力的團隊來說,這個中間層甚至可以用類似於智慧資料庫的方式來對線上查詢自動分析,自動實現中間層。事實上,這些中間技術早就已經成熟,結合上圖資料庫和cypher,是可以把傳統的“人力密集型開發”轉變為“技術密集型開發”的。
扯得略遠了,我們重新回到模式匹配型的查詢語句上,為什麼說它是革命性的,因為它剛好滿足了我們之前對資料描述的需求。任何一個開發者,只要把資料字典做出來。關於資料的工作就已經完成了。或者換個角度來說,在任何一個已有資料的系統中,只要我能在前端或者移動端中描述我想要的資料,就能開發出應用,不再需要寫任何伺服器端資料介面。Facebook 在 React Conf 上放出的前端 Relay 框架和 GraphQL 幾乎就已經是這樣的實現。
再來看邏輯部分,無論在瀏覽器端還是伺服器端,用什麼語言,實現一個事件系統都再簡單不過。這裡我們倒是可以進一步探索,除了之前所說的圖形介面除錯,測試、監控自動化,我們還能做什麼?對前端來說,如果前後端事件系統可以直接打通,並且出錯時通過圖形化的除錯工具能無需回滾直接排查,那就最好了。
例如:在建立 post 的前端元件中
1 2 3 4 5 6 7 |
//觸發前端的 post.create 事件 var post = {title: "test", content: "test"} emitter.fire("post.create").then(function(){ alert("建立成功") }).catch(function(){ alert("建立失敗") }) |
在處理邏輯的檔案中:
1 2 3 4 5 6 7 8 9 10 11 12 |
//可以增加前端專屬的邏輯 emitter.on("post.create", function checkTest(post){ if( post.title === "test"){ console.log("this is a test blog.") } }) //通過 server: 這樣的名稱空間來觸發伺服器端的事件 emitter.on("post.create", function communicateWithServer(post){ console.log("communicating with server") return emitter.fire("server:post.create", post) }) |
得到的事件棧
在瀏覽器端可以打通和伺服器端的事件系統,那麼在伺服器端呢?剛剛提到我們我們其實可以用任何自己熟悉的語言去實現事件系統,那是不是也意味著,只要事件呼叫棧的資料格式一致,我們就可以做一個跨語言的架構?
例如我們可以用nodejs的web框架作為伺服器端入口,然後用python,用go去寫子系統。只要約定好系統間通訊機制,以及事件呼叫棧的資料格式,那麼就能實現跨語言的事件系統融合。這意味你未來看到的呼叫棧圖可能是:
跨語言的實現,本身也是一筆巨大財富。例如當我們未來想要找人一起協同完成某一個web應用時,再也不必侷限於某一種語言的實現。甚至利用docker等容器技術,執行環境也不再是限制。再例如,當系統負載增大,逐漸出現瓶頸時。我們可以輕鬆地使用更高效的語言或者執行環境去替換掉某個業務邏輯的監聽器實現。
更多的例子,舉再多也舉不完。當你真正自己想清楚這套架構之後,你會發現未來已經在你眼前。
到這裡,對“理想”的想象和對實現技術的思考終於可以劃上句號了。對熟悉架構的人來說,其實已經圓滿了。但我也不想放棄來“求乾貨”的觀眾們。下面演示的,就是在框架原型下開發的簡單應用。這是一個多人的todo應用。
前端基於react,後端基於koa。
目錄結構
前端資料(todo 列表) /public/data/todos.js
前端邏輯(todo 基本邏輯) /public/events/todo.js
前端邏輯(輸入@時展示使用者列表) /public/events/mention.js
後端邏輯(通知被@使用者) /modules/mention.js
通過除錯工具得到的建立時的呼叫棧和輸入@符號時的呼叫棧
這只是一個引子,目的是為了讓你巨集觀的感受將應用拆解為“資料+邏輯”以後能有多簡單。目前這套框架已完成 50% ,實現了資料部分的設計、前後端事件融合,還有跨語言等方案正在開發中。未來將開源,期待讀者關注。
後記
終於寫完了。框架只是架構的實現。這套架構幾乎孕育了近兩年,這其中已經開發出一款實現了部分功能,基於nodejs的伺服器端原型框架。完整的框架開發目前也已經四個月了。雖然從它落地的這些前端技術、資料技術看起來,它其實是有技術基礎的,應該是積累的產物。但實際上,最早的關於資料和邏輯的思路,卻是在我讀研時對一個“很虛”的問題的思考:什麼樣的系統是最靈活的系統?在很長一段時間內,對各種架構的學習中我都沒有找到想要的答案,直到後來在學認知心理學和神經學的時候,我想到了人。人是目前可以理解的最具備適應性,最靈活的系統。人是怎麼運作的?生理基礎是什麼?
認知心理學裡提到曾經有一個學派認為人的任何行為都不過是對某種刺激的反射,這種刺激可以是來自內部也可以是外部。來自內部的刺激有兩個重要來源,一是生理上,例如飢餓,疲憊。二則是記憶。例如,你每天起床要去工作,是因為你的過去的記憶告訴你你需要錢,或者你喜歡工作的內容。這對人來說也是一種刺激,所以你產生了去工作的動機。外部刺激就更簡單,例如生理上的被火燙了,心理上被嘲諷、被表揚等等。而人的反應,就是對這些刺激而產生的多種反射的集合。例如早上起床,你的一部分反射是產生上班的動機,但是如果你生病了,你的身體和記憶就會刺激你去休息。最終你會在這兩種刺激下達到一個平衡,做出反應。值得注意的是,大部分時候,人在不同時間面臨相同的刺激,卻做出不同的反應。並不是因為後來某些反射被刪除了,而是因為後來形成了更強的反射區壓制住了之前的反射。它的生理基礎就是神經學中的神經遞質可以互相壓制。
如果我們把要打造的系統看做一個有機體,把迭代看做生長,把使用者的使用看做不斷的刺激。那我們是不是就能模擬人的反射過程來打造系統,從而期待系統得到像人一樣的適應力?而恰恰你會發現科幻作品中的人工智慧產品通常都以人的形態出現。因為我們希望我們所使用的產品,就像人一樣通情達理,具有人一樣的領悟能力。而要達到這樣的效果,或許就是不斷給給他新增人對刺激的反射規則。
思考到這一步的時候,我對應用架構的設計哲學已經基本定型。後來驗證出來的,這樣的系統能夠極大地提高研發效率,都只是這段哲學的附加價值。其實提高研發效率的原理很簡單,無論系統的需求再怎麼擴充套件、再怎麼變更,它也是遵循人本身的思維邏輯的。因此,你始終可以使用本身就模擬人類認知的系統去適應它。並且,它怎麼變化,你就怎麼變化。
架構這種東西,最終仍然關注在使用者身上的。所以與其和我討論確定的技術問題,不如討論這些更有意義。對思考架構的人來說,我認為眼界和哲學高度,最重要。
討論記錄
尤小右:感覺其實就是 flux 啊,但是 string-based global event bus 規模大了還是會有點坑爹的。一個事件觸發的後果遍及全棧,不好 track。
答:和flux的區別在於flux的資料物件本身和對資料的操作是合在store裡的。事件系統規模的問題通過兩個方式控制:一是名稱空間。二是事件只應用在業務邏輯個程度就夠了,像“存入資料庫”這種操作就不要再用事件觸發。這樣系統就不會亂掉,因為它只反映業務邏輯。
玉伯也叫黑俠:認識心理學那段很有趣。很關注如何讓業務程式碼隨著時間流逝不會腐化而會趨良?比如事件fire點,怎麼才能可控又夠用,而不會隨著業務複雜而爆發式增長?(簡單如seajs, 隨著外掛的多樣化事件點都經常不夠用)。還有如何讓事件間彼此解耦?經常一個需求要新增多個監聽,做得不好還可能影響其他功能點。
答:用事件去反映業務邏輯,而不是技術實現的邏輯”不只是這套架構對於防止事件濫用的一個建議,更是它的哲學理論的重要部分。遵守它,這套框架就能把高可擴充套件性和高可維護性發揮到極致。我們用一個常見的例子來說明這一點。有時候面臨需求變更,我們會覺得難搞,會對產品經理說:“你這個變更影響很大,因為我的程式碼中xxx不是這樣設計的”。而產品經理有可能不理解,因為對他來說,變更的需求可能只是一個很簡單的邏輯,加上一點特殊情況而已。產生這種矛盾的關鍵就在於,沒有找到一種能準確描述業務邏輯的方式去組織程式碼。如果組織程式碼的方式和描述業務邏輯的方式一致,那麼業務邏輯上覺得改動點很簡單,程式碼上就也會很簡單。這套架構中的事件系統、包括事件擁有的順序控制等特性,都是為了提供一種儘可能合適的方式去描述業務邏輯。只有這樣,才能實現程式碼最少、最可讀、最可擴充套件。它本身是為描述業務邏輯而不是技術實現邏輯而生。所以只有遵守這個規則,才能得到它帶來的財富。
玉伯也叫黑俠:嗯,看明白了。感覺是將程式碼階段的複雜性,前移到了業務系分階段,如果系分階段做得好,那麼程式碼就會很優雅。反之,則很難說。進一步提一個無恥要求:怎麼保證系分階段的良好性呢?不少時候,寫程式碼的過程,就是梳理業務邏輯的過程,寫完後,才明白某個需求真正該怎麼實現。
答:不太認同寫程式碼的過程是梳理業務邏輯的過程。可以說寫程式碼的過程是梳理具體技術實現的過程。如果一開始寫程式碼的人連業務邏輯都不清楚,再好的技術和框架也無法防止他寫出爛程式碼。基於事件的架構其實不是對系分的要求提高了,反而是降低了。因為只要求你理清楚邏輯,具體的實現寫得再爛,之後都可以依賴事件系統架構本身的靈活性去完善的。就例如“發表文章後給所有被@的人發站內信”這樣的邏輯,你可能一開始沒有考慮發站內信的時候最好用個佇列,防止請求被卡住。但只要你做到了最基礎的把“傳送站內”這個監聽器註冊到“發表文章”的事件上。未來就能在不影響任何其他程式碼的情況下去優化。實際上沒有任何框架能幫你寫好程式碼,即使DDD社群也是強調不斷重構,只可能“降低讓你寫好程式碼的門檻”。這套架構就是遮蔽很多技術上的概念,用事件的方式讓你只關注邏輯。
玉伯也叫黑俠:有沒有一種讓程式碼趨良的架構?可能剛開始寫得亂糟糟,但隨著做的需求越多,寫的程式碼越多,整體可維護性反而會變得越好?比如前後端分層,讓後端專注業務模型,一般來說,業務模型會逐步趨於完善和穩定,前端程式碼也會逐步變好。用一些約束,推動程式碼的良性迴圈。這些約束,是否就是理想應用架構的精髓?這些約束是什麼?可能是某種要求比如測試覆蓋率,也可能是某種強制約束比如必須通過資料改動來更新介面。roof的約束是用事件去反映業務邏輯,但這個約束更多是「道德」層面,而不是「法律」,比如如何防止「大事件」(一個事件裡,一坨技術實現的邏輯程式碼)?如何讓人羞於去寫出糟糕的程式碼?
答:即使前後端分離,業務模型趨於穩定,也是靠開發者自身不斷重構去實現的,要不然怎麼會“趨於”穩定呢。架構只可能讓人站到更好地平臺上,用更好地方式去寫好程式碼,不可能主動幫人把程式碼變好。文中架構就是通過遮蔽技術細節,讓你關注業務邏輯的方式,讓程式碼易理解,也讓你能不影響業務地去升級技術。這套架構因為有一個清晰的事件呼叫棧資料結構,所以能很容易地做出相應的測試、監控工具保障程式碼質量。但要實現“法律”是不可能的。即使是Java、即使是領域驅動程式設計,也可以在它好的架構下寫出各種糟糕的程式碼。畢竟程式設計仍然是一件需要創造力的工作。這就像硬幣的兩面,如果要實現法律,那工作本身必須是無需創造,完全可以按照流程由機器人生產。如果要創造力,就必然會有因人而異的品質差異。