軟件設計雜談

軟件設計雜談javascript

2015-04-17 程序人生 程序人生
程序人生

功能介紹 十年漫漫程序人生,打過各類雜,也作過讓我驕傲的軟件;管理過十多人的團隊,還帶領一班兄弟姐妹創過業。關注程序人生,瞭解程序猿,學作程序猿,讓咱們的人生再也不屌絲化。java

disclaimer: 本文所講的設計,非UI/UE的設計,單單指軟件代碼/功能自己在技術上的設計。UI/UE的主題請出門右轉找特贊(Tezign)。:)nginx

在現在這個Lean/Agile橫掃一切的年代,設計彷佛有了被邊緣化的傾向,作事的週期如此之快,彷佛已容不下人們更多的思考。MVP(Minimal Viable Produce)在不少團隊裏演化成一個形而上的圖騰,因而工程師們找到了一個完美的藉口:我先作個MVP,設計的事,之後再說。web

若是純屬我的玩票,有個點子,hack out還說得過去;但要嚴肅作一個項目,仍是要下工夫設計一番,不然,沒完沒了的返工會讓你無語淚千行。安全

設計首先得搞懂要解決的問題

工程師大多都是很聰明的人,聰明人有個最大的問題就是自負。不少人拿到一個需求,還沒太搞明白其外延和內涵,代碼就已經在腦殼裏流轉。這樣作出來的系統,縱使再精妙,也免不了承受因需求理解不明確而致使的返工之苦。websocket

搞懂需求這事,提及來簡單,作起來難。需求有正確的但表達錯誤的需求,有正確的但沒表達出來的需求,還有過分表達的需求。因此,拿到需求後,先不忙尋找解決方案,多問問本身,工做夥伴,客戶follow up questions來澄清需求模糊不清之處。網絡

搞懂需求,還須要瞭解需求對應的產品,公司,以及(潛在)競爭對手的現狀,需求的上下文,以及需求的約束條件。人有二知二不知:架構

  1. I know that I knowapp

  2. I know that I don’t knowdom

  3. I don’t know that I know

  4. I don’t know that I don’t know

澄清需求的過程,就是不斷驅逐無知,掌握現狀,上下文和約束條件的過程。

這個主題講起來很大,且很是重要,但畢竟不是本文的重點,因此就此帶過。

尋找(多個)解決方案

若是對問題已經有不錯的把握,接下來就是解決方案的發現之旅。這是個考察big picture的活計。一樣是知足孩子想要個汽車的願望,你能夠:

  1. 去玩具店裏買一個現成的

  2. 買樂高積木,而後組裝

  3. 用紙糊一個,或者找塊木頭,刻一個

這對應軟件工程問題的幾種解決之道:

  1. 購買現成軟件(acuquire or licensing),二次開發之(若是須要)

  2. 尋找building blocks,組裝之(glue)

  3. 本身開發(build from scratch, or DIY)

大部分時候,若是a或b的TCO [1] 合理,那就不要選擇c。作一個產品的目的是爲客戶提供某種服務,而不是證實本身能一行行碼出出來這個產品。

a是個很重要的點,惋惜大部分工程師腦殼裏沒有錢的概念,或者出於job security的私心,而忽略了。工程師如今愈來愈貴,能用合理的價格搞定的功能,就不應僱人去打理(本身打臉)。一個產品,最核心的部分不超過整個系統的20%,把人力資源鋪在覈心的部分,纔是軟件設計之道。

b咱們稍後再講。

對工程師而言,DIY出一個功能是個極大的誘惑。一種DIY是源自工程師的不滿。任何開源軟件,在處理某種特定業務邏輯的時候總會有一些不足,眼裏若是把這些不足放在,卻忽略了人家的好處,是大大的不妥。前兩天我聽到有人說 "consul sucks, …, I’ll build our own service discovery framework…",我就苦笑。我相信他能作出來一個簡單的service discovery tool,這不是件特別困難的事情。問題是值不值得去作。若是連處於consul這個層次的基礎組件都要本身去作,那要麼是心太大,要麼是沒有定義好本身的軟件系統的核心價值(除非系統的核心價值就在於此)。代碼一旦寫出來,不管是5000行仍是50行,都是須要有人去維護的,在系統的生命週期裏,每一行本身寫的代碼都是一筆債務,須要按期不按期地償還利息。

另一種DIY是出於工程師的無知。「無知者無畏」在某些場合的效果是正向的,有利於打破陳規。但在軟件開發上,仍是知識和眼界越豐富越開闊越好。一個無知的工程師在面對某個問題時(好比說service discovery),若是不知道這問題也許有現成的解決方案(consul),本身鉚足了勁寫一個,大半會有失偏頗(好比說沒作上游服務的health check,或者本身自己的high availability),結果bug不斷,辛辛苦苦一個個都啃下來,才發現,本身走了不少彎路,費了大半天勁,作了某個開源軟件的功能的子集。固然,對工程師而言,這個練手的價值仍是很大的,但對公司來講,這是一筆沉重的無心義的支出。

眼界定義了一我的的高度,若是你天天見同類的人,看同質的書籍/視頻,(讀)寫隸屬同一domain的代碼,那多半眼界不夠開闊。互聯網的發展一日千里,變化太快,若是把本身禁錮在一方小天地裏,很容易成爲陶淵明筆下的桃花源中人:乃不知有漢,不管魏晉。

構建靈活且有韌性的系統

若是說以前說的都是廢話,那麼接下來的和真正的軟件設計能扯上些關係。

分解和組合

軟件設計是一個把大的問題不斷分解,直至原子級的小問題,而後再不斷組合的過程。這一點能夠類比生物學:原子(keyword/macro)組合成分子(function),分子組合成細胞(module/class),細胞組合成組織(micro service),組織組合成器官(service),進而組合成生物(system)。

一個如此組合而成系統,是知足關注點分離(Separation of Concerns)的。大到一個器官,小到一個細胞,都各司其職,把本身要作的事情作到極致。心臟沒必要關心腎臟會幹什麼,它只須要作好本身的事情:把新鮮血液經過動脈排出,再把各個器官用過的血液從靜脈回收。

分解和組合在軟件設計中的做用如此重要,以致於一個系統若是合理分解,那麼往後維護的代價就要小得多。一樣講關注點分離,不一樣的工程師,分離的方式可能徹底不一樣。但究其根本,還有有一些規律可循。

總線(System Bus)

首先咱們要把系統的總線定義出來。人體的總線,大的有幾條:血管(動脈,靜脈),神經網絡,氣管,輸尿管。它們有的徹底負責與外界的交互(氣管,輸尿管),有的徹底是內部的信息中樞(血管),有的內外兼修(神經網絡)。

總線把生產者和消費者分離,讓彼此互不依賴。心臟往外供血時,把血壓入動脈血管就是了。它並不須要知道誰是接收者。

一樣的,回到咱們熟悉的計算機系統,CPU訪問內存也是如此:它發送一條消息給總線,總線通知RAM讀取數據,而後RAM把數據返回給總線,CPU再獲取之。整個過程當中CPU只知道一個內存地址,毋須知道訪問的具體是哪一個內存槽的哪塊內存 —— 總線將兩者屏蔽開。

學過計算機系統的同窗應該都知道,經典的PC結構有幾種總線:數據總線,地址總線,控制總線,擴展總線等;作過網絡設備的同窗也都知道,一個經典的網絡設備,其軟件系統的總線分爲:control plane和data plane。

路由(routing)

有了總線的概念,接下來必然要有路由。咱們看人體的血管:

每一處分叉,就涉及到一次路由。

路由分爲外部路由和內部路由。外部路由處理輸入,把不一樣的輸入dispatch到系統裏不一樣的組件。作web app的,可能沒有意識到,但其實每一個web framework,最關鍵的組件之一就是url dispatch。HTTP的偉大之處就是每一個request,都能經過url被dispatch到不一樣的handler處理。而url是目錄式的,能夠層層演進 —— 就像分形幾何,一個大的系統,經過不斷重複的模式,組合起來 —— 很是利於系統的擴展。遺憾的是,咱們本身作系統,對於輸入既沒有總線的考量,又無路由的概念,if-else下去,長此以往,代碼便繞成了意大利麪條。

再舉一例:DOM中的event bubble,在javascript處理起來已然隱含着路由的概念。你只需定義當某個事件(如onclick)發生時的callback函數就好,至於這事件怎麼經過eventloop抵達回調函數,無需關心。好的路由系統剝繭抽絲,把繁雜的信息流正確送處處理者手中。

外部路由總還有「底層」爲咱們完成,內部路由則需工程師考慮。service級別的路由(數據流由哪一個service處理)能夠用consul等service discovery組件,service內部的路由(數據流到達後怎麼處理)則須要本身完成。路由的具體方式有不少種,pattern matching最爲常見。

不管用何種方式路由,數據抵達總線前爲其定義Identity(ID)很是重要,你能夠管這個過程叫data normalization,data encapsulation等,總之,一個消息能被路由,須要有個用於路由的ID。這ID能夠是url,能夠是一個message header,也能夠是一個label(想象MPLS的狀況)。當咱們爲數據賦予一個個合理的ID後,如何路由便清晰可見。

隊列(Queue)

對於那些並不是須要當即處理的數據,可使用隊列。隊列也有把生產者和消費者分離的功效。隊列有:

  • single producer single consumer(SPSC)

  • single producer multiple consumers(SPMC)

  • multiple producers single consumer(MPSC)

  • multiple producers multiple consumers(MPMC)

仔細想一想,隊列其實就是總線+路由(可選)+存儲的一個特殊版本。通常而言,system bus之上是系統的各個service,每一個service再用service bus(或者queue)把micro service chain起來,而後每一個micro service內部的組件間,再用queue鏈接起來。

有了隊列,有利於提升流水線的效率。通常而言,流水線的處理速度取決於最慢的組件。隊列的存在,讓慢速組件有機會運行多份,來彌補生產者和消費者速度上的差距。

Pub/Sub

存儲在隊列中的數據,除路由外,還有一種處理方式:pub/sub。和路由類似,pub/sub將生產者和消費者分離;但兩者不一樣之處在於,路由的目的地由路由表中的表項控制,而pub/sub通常由publisher控制[2]:任何subscribe某個數據的consumer,都會到publisher處註冊,publisher由此能夠定向發送消息。

協議(protocol)

一旦咱們把系統分解成一個個service,service再分解成micro service,彼此之間互不依賴,僅僅經過總線或者隊列來通信,那麼,咱們就須要協議來定義彼此的行爲。協議聽起來很高大上,其實否則。咱們寫下的每一個function(或者每一個class),其實就是在定義一個不成文的協議:function的arity是什麼,接受什麼參數,返回什麼結果。調用者需嚴格按照協議調用方能獲得正確的結果。

service級別的協議是一份SLA:服務的endpoint是什麼,版本是什麼,接收什麼格式的消息,返回什麼格式的消息,消息在何種網絡協議上承載,須要什麼樣的authorization,能夠正常服務的最大吞吐量(throughput)是什麼,在什麼狀況下會觸發throttling等等。

頭腦中有了總線,路由,隊列,協議等這些在computer science 101中介紹的基礎概念,系統的分解便有跡可尋:面對一個系統的設計,你要作的再也不是一道做文題,而是一道填空題:在若干條system bus裏填上其名稱和流進流出的數據,在system bus之上的一個個方框裏填上服務的名稱和服務的功能。而後,每一個服務再以此類推,直到感受毋須再細化爲止。

組成系統的必要服務

有些管理性質的服務,儘管和業務邏輯直接關係不大,但不管是任何系統,都須要考慮構建,這裏羅列一二。

代謝(sweeping)

一個活着的生物時時刻刻都進行着新陳代謝:每時每刻新的細胞取代老的細胞,同時身體中的「垃圾」經過排泄系統排出體外。一個運轉有序的城市也有新陳代謝:下水道,垃圾場,污水處理等維持城市的正常功能。沒有了代謝功能,生物會凋零,城市會荒蕪。

軟件系統也是如此。日誌會把硬盤寫滿,軟件會失常,硬件會失效,網絡會擁塞等等。一個好的軟件系統須要一個好的代謝系統:出現異常的服務會被關閉,一樣的服務會被從新啓動,恢復運行。

代謝系統能夠參考erlang的supervisor/child process結構,以及supervision tree。不少軟件,都運行在簡單的supervision tree模式下,如nginx。

高可用性(HA)

每一個人都有兩個腎。爲了apple watch賣掉一個腎,另外一個還能保證人體的正常工做。固然,人的兩個腎是Active-Active工做模式,內部的腎元(micro service)是 N(active)+M(backup) clustering 工做的(看看人家這service的作的),少了一個,performance會一點點有折扣,但能夠忽略不計。

大部分軟件系統裏的各類服務也須要高可用性:除非徹底無狀態的服務,且服務重啓時間在ms級。服務的高可用性和路由是息息相關的:高可用性每每意味着同一服務的冗餘,同時也意味着負載分擔。好的路由系統(如consul)可以對路由至同一服務的數據在多個冗餘服務間進行負載分擔,同時在檢測出某個失效服務後,將數據路只由至正常運做的服務。

高可用性還意味着非關鍵服務,即使不可恢復,也只會致使系統降級,而不會讓整個系統沒法訪問。就像壁虎的尾巴斷了不妨礙壁虎逃命,人摔傷了手臂還能吃飯同樣,一個軟件系統裏統計模塊的異常不應讓用戶沒法訪問他的我的頁面。

安保(security)

安保服務分爲主動安全和被動安全。authentication/authorization + TLS + 敏感信息加密 + 最小化輸入輸出接口能夠算是主動安全,防火牆等安防系統則是被動安全。

繼續拿你的腎來比擬 —— 腎臟起碼有兩大安全系統:

  1. 輸入安全。腎器的厚厚的器官膜,保護器官的輸入輸出安全 —— 主要的輸入輸出只能是腎動脈,腎靜脈和輸尿管。

  2. 環境安全。腎器裏有大量脂肪填充,避免在撞擊時對核心功能的損傷。

除此以外,人體還提供了包括免疫系統,皮膚,骨骼,空腔等一系列安全系統,從各個維度最大程度保護一個器官的正常運做。若是咱們仔細研究生物,就會發現,安保是個一攬子解決方案:小到細胞,大到整我的體,都有各自的安全措施。一個軟件系統也需如此考慮系統中各個層次的安全。

透支保護(overdraft protection)

任何系統,任何服務都是有服務能力的 —— 當這能力被透支時,須要必定的應急計劃。若是使用擁有auto scaling的雲服務(如AWS),動態擴容是最好的解決之道,但受限於所用的解決方案,它並不是萬靈藥,AWS的auto scaling依賴於load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB對某些業務,如websocket,支持不佳;而第三方的load balancer,則須要考慮部署,與Amazon的auto scaling結合(須要寫點代碼),避免單點故障,保證自身的capacity等一堆頭疼事。

在沒法auto scaling的場景最通用的作法是back pressure,把壓力反饋到源頭。就好像你不斷熬夜,最後大腦受不了,逼着你睡覺同樣。還有一種作法是服務降級,停掉非核心的service/micro-service,如analytical service,ad service,保證核心功能正常。

把設計的成果講給別人聽

完成了分解和組合,也嚴肅對待了諸多與業務沒有直接關係,但又不得不作的必要功能後,接下來就是要把設計在白板上畫下來,講給任何一個利益相關者聽。聽他們的反饋。設計不是一個閉門造車的過程,全程都須要和各類利益相關者交流。然而,不少人都忽視了設計定型後,繼續和外界交流的必要性。不少人會認爲:個人軟件架構,設計結果和工程有關,爲什麼要講給工程師之外的人聽?他們懂麼?

其實pitch自己就是自我學習和自我修正的一部分。當着一我的或者幾我的的面,在白板上畫下腦海中的設計的那一刻,你就會有直覺哪一個地方彷佛有問題,這是很奇特的一種體驗:你本身畫給本身看並不會產生這種直覺。這大概是面對公衆的焦灼產生的腎上腺素的效果。:)

此外,從聽者的表情,或者他們提的聽起來很傻很天真的問題,你會進一步知道哪些地方你覺得你搞通了,其實本身是隻知其一;不知其二。太簡單,太基礎的問題,咱們take it for granted,不屑去問本身,非要有人點出,本身才發現:啊,原來這裏我也不懂哈。這就是破解 "you don’t know what you don’t know" 之法。

記得看過一個video,主講人大談企業文化,有個哥們傻乎乎發問:so what it culture literally? 主講人愣了一下,拖拖拉拉講了一堆本身都不能讓本身信服的廢話。估計回頭他就去查韋氏詞典了。

最後,總有人在某些領域的知識更豐富一些,他們會告訴你你一些你知道本身不懂的事情。填補了 "you know that you don’t know" 的空缺。

設計時的tradeoff

Rich hickey(clojure做者)在某個演講中說:

everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

因此,下回再腆着臉說:偶作了些tradeoff,先確保本身作足了功課再說。

設計的改變不可避免

設計不是一錘子買賣,改變不可避免。我以前的一個老闆,喜歡把:change is your friend 掛在口頭。軟件開發的整個生命週期,變動是屢見不鮮,以致於變動管理都生出一門學問。軟件的設計期更是如此。人總會犯錯,設計總有缺陷,需求總會變化,老闆總會指手畫腳,PM總有一天會亮出獠牙,再也不是貼心大哥,或者美萌小妹。。。因此,力排衆議,而後接受必要的改變便可。連凱恩斯他老人家都說:

What do you do, sir?

相關文章
相關標籤/搜索