今天談下在微服務架構下,接口設計和開發方面的思考。
對於微服務架構,SOA和Http Rest API接口設計,在我前面的頭條文章中均有專門的說明,因此對於基礎方面的解釋在本文不再重複。對於今天要寫的內容,先總結一句話再展開說明。
在SOA和微服務架構思想下,除了常說的面向對象,領域驅動,SOA等架構思想外。還需要增加基於API接口驅動進行的設計和開發工作。API接口的識別,定義,設計和開發過程貫徹了整個微服務開發的生命周期。
對於軟體開發過程或生命周期,常會說到瀑布模型,增量和迭代模型,敏捷方法論等。即使到了迭代開發,對於全新驅動仍然強調一個關鍵點,即:
軟體開發中應以架構為核心,架構應確保高度的概念一致性和完整性。
也就是說對於全新系統研發,即使要拆分和並行,也需要在架構設計完成後再進行,否則很難真正保證架構一致性和整個領域模型的完整性等。
從模塊拆分到前後端分離
對於傳統業務系統的開發,架構做了一個重要工作,即將大的業務系統拆分為多個子系統或者模塊,同時定義清楚各個模塊之間的接口。在這個工作完成後,後續各個模塊的設計開發工作才能夠真正並行起來。也就是說:
只要各個模塊是按照預先定義好的接口進行設計開發的,那麼最終各個模塊就一定能夠集成起來。架構師不單是做了分而治之的分解工作,而是通過接口解決了分解後的集成問題。
即使到了現在微服務架構模式下,對於究竟拆分為多少個模塊,每個模塊應該暴露哪些API接口服務應該是自頂向下方式,由架構師統一進行規劃和設計。
微服務架構開發實際上重點解決兩個階段的關鍵問題
其次,在微服務開發中,引入了另外一個關鍵即前後端分離。
前後端分離實際和SOA思想裡面的服務分層思路相對一致,至少SOA跨系統間的服務分層,服務組織思想進入到單個微服務內部的功能實現機制上。
前後端分離實際上當前是包括了技術和團隊組織兩個方面
在分離後開發分工越來越細,各自都可以專注自己的內容做到更加高效,一個前端往往也可以對應多個後端開發。而前端和後端協同的關鍵就變成了Rest API接口服務調用。
接口先行和驅動的開發
那麼當前微服務開發,有多少團隊是真正將接口定義和設計清楚後,各自基於嚴格約定的接口契約進行並行開發的?
實際上很多團隊並沒有嚴格這樣做。
前端說需要一個接口了,後端臨時再做一個,或者後端因為另外一個功能實現影響將接口調整了也不通知前方等,這些都是實際經常出現的情況。
即接口本身嚴肅性不夠,導致接口新增和變更都相對隨意,也導致了接口很難處於一個穩定的狀態。在接口無法穩定的情況下,導致團隊各個角色的開發都受到影響。
接口驅動開發的核心思路就是在微服務開發過程中優先定義和設計接口,確定API接口模型和接口契約,然後後端,前端,基於同樣的接口標準並行開展工作。
接口定義清楚後,可以生成相應的代碼框架或開發框架,前端和後端基於同樣的開發框架進行接口開發工作。對於後端人員應該優先實現接口,再實現非接口部分內容,後端人員實現的接口可以自己進行單元測試。而對於前端在進行開發的時候,由於有了接口契約,可以通過Mock的方式提供模擬接口數據,方便進行前端功能開發和測試。
只有這樣,才能夠真正做到全並行開發和開發後的集成工作。
為什麼接口驅動很難?
實際在實踐的過程中你會感覺到很難,就是前期定義接口往往在後期仍然大量變動和增加,但是這不是不用接口驅動的理由。就如我們不能因為需求會經常變更,而不先進行需求分析和開發一樣的道理。
接口頻繁變動實際本身包括兩個方面的原因。
其一是需求本身就不清晰,往往是功能做到哪裡想到哪裡,這樣自然會導致接口不斷地增加或者變更,導致大量重複工作。其二是在進行接口定義設計的時候,沒有考慮接口應該是提供領域服務能力,而不是提供大量的資料庫表的CRUD能力,沒有考慮很多邏輯應該是內聚在後端完成聚合和組裝,而跑到前端進行組裝。
以上兩個就是接口頻繁變動的關鍵原因。因此要推進接口驅動,首先要加強需求輸出的嚴謹性,其次要加強領域設計能力,做好接口的設計和抽象。
在這裡先看下關於面向接口編程的一些說明。
我們在一般實現一個系統的時候,通常是將定義與實現合為一體,不加分離的,我認為最為理想的系統設計規範應是所有的定義與實現分離,儘管這可能對系統中的某些情況有點麻煩。
在一個面向對象的系統中,系統的各種功能是由許許多多的不同對象協作完成的。在這種情況下,各個對象內部是如何實現自己的,對系統設計人員來講就不那麼重要了;而各個對象之間的協作關係則成為系統設計的關鍵。小到不同類之間的通信,大到各模塊之間的交互,在系統設計之初都是要著重考慮的,這也是系統設計的主要工作內容。
面向接口編程就是指按照這種思想來編程。
對接口的理解
接口從更深層次的理解,應是定義(規範,約束)與實現(名實分離的原則)的分離。接口的本身反映了系統設計人員對系統的抽象理解。
在設計模式裡面經常會提到面向接口而設計,同時強調儘量要少用繼承而多用組合。面向接口設計一方面是更加方便的應對和適配底層變化,比如底層實現的變化;另外一個方面就是接口定義清楚後對於接口依賴端可以並行開展工作。
同時還可以看到通過面向接口編程方式,可以最大限度地對外部屏蔽接口內部實現細節,一個模塊對外開放能力只需要開放接口定義格式和描述,而不用開放具體的實現細節。
定義和實現分離,達到了進一步的安全方面要求。
接口在架構設計中起到關鍵作用
在前面已經談到接口定義和設計在架構設計裡面起到關鍵作用,這種接口的定義除了上述的作用外,更加重要的是實現了系統分解後,各個子系統和模塊只要遵守共同的接口定義契約,就可以開始並行開發和實現。
這也是我們在實施大型SOA集成項目經常談到,接口定義和設計先行,通過統一標準的接口契約來實現接口開發和實現的並行。如下圖:
接口先行的目的就是大家遵循同樣的標準,那麼後續各個組件就能夠無縫地集成到一起。否則接口實現不一致,那麼後續就無法進行集成,導致功能和接口變更。
基於接口驅動來實現完整的產品開發和集成
在微服務開發過程中,整個微服務劃分和微服務間的接口設計仍然需要保持高度的架構完整性和概念一致性。即首先通過架構人員進行微服務拆分,關鍵接口設計,其次才是進行各個微服務模塊的開發,在開發完成後進行集成工作。
如上圖,大家遵循同樣的接口契約,那麼後端開發,前端開發和測試人員可以並行開始各自的工作。對於前端優先進行接口開發和實現,前端則通過接口契約產生Mock模擬,通過接口模擬實現來進行前端功能的開發。在前後端開發過程中,測試人員也可以根據接口定義進行測試設計工作,同時進行相關的測試腳本設計或錄製工作。
接口開發完成後,前端和後端首先各自進行單元測試,在單元測試完成後進行前後端的集成測試和驗證。同時測試人員可以啟動相應的接口自動化測試工作。
前後端分離後的問題
當我今天寫這篇文章的時候,進一步思考了下前後端分離開發的一些問題。
比如在傳統開發模式下,一個功能實現都由張三負責,那麼前端和後端都是張三來做,張三對整個功能業務和邏輯都了解,因此可以既完成接口層的單元測試,也可以完成前端頁面黑盒驅動的功能自測試工作。
當功能進行前後端分離開發後,張三僅負責後端,前端由小敏負責。
而這個時候張三往往對整個前端功能實現和頁面都不關心,僅僅關注自己邏輯實現和接口提供,關注自己的單元測試驗證。而小敏負責前端,實際上小敏對整個功能的業務邏輯和場景往往也並不清楚,僅僅只能夠測試數據的錄入,查詢裝載等正常。
即這個時候張三和小敏都無法完整地進行前端功能頁面的測試,這個時候導致很多測試工作都轉到測試階段後由測試人員才能夠測試和發現。
這本身也就導致了功能實現問題的進一步朝後面洩露。
如何解決這個問題?
第一個方法就是負責後端的張三必須做完整的單元測試,而這個工作量極大,大部分開發人員實際都無法做完整的單元測試。第二個方法就是該工作還是由小敏負責,那麼小敏就必須參與前期需求和接口評審,熟悉需求和業務場景,才能夠展開。
規模不大的項目沒必要前後端分離
這個也是我在前面強調過的要給觀點,儘管前後端分離可以進一步實現微服務間的解耦,但是也增加了單個功能實現多個角色之間溝通的協同量。
項目規模再小,你還得找到要給後端角色或前端開發兩個角色,或者你需要找到一個很厲害的都懂的全棧人員。而實際上前後端分離本身帶來大量的集成工作量,同時後端分離的API接口本身並沒有體現出應有的粗粒度和復用價值。
本來一個簡單項目,前端用easyui就能搞定,結果用微服務和前後端分離後卻越搞越複雜。
對於HTTP Rest接口的設計,網上已經有很多文章都有詳細的闡述,今天再重新整理下這裡面的一些重點,大家都清楚Rest接口是面向資源的接口設計方法,而且基於原生的Http協議,因此裡面就有兩個最關鍵的點,一個就是對資源的理解,一個就是對操作的理解。
圖片來源網絡
什麼是RESTful架構,重要的幾點如下:
對資源的理解
就是我們平常上網訪問的一張圖片、一個文檔、一個視頻等。這些資源我們通過URI來定位,也就是一個URI表示一個資源。資源是做一個具體的實體信息,它可以有多種的展現方式。而把實體展現出來就是表現層,例如一個txt文本信息,它可以輸出成html、json、xml等格式,一個圖片他可以jpg、png等方式展現,這個就是表現層的意思。
URI確定一個資源,但是如何確定它的具體表現形式呢?應該在HTTP請求的頭信息中用Accept和Content-Type欄位指定,這兩個欄位才是對」表現層」的描述。
對操作的理解
客戶端能通知伺服器端的手段,只能是HTTP協議。
具體來說,就是HTTP協議裡面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT用來更新資源,DELETE用來刪除資源。
對於資源的任何操作,都應該映射到HTTP的幾個有限的方法(常用的有GET/POST/PUT/DELETE 四個方法,還有不常用的PATCH/HEAD/OPTIONS方法)上面。所以RESTful API建模的過程,可以看作是具有統一接口約束的面向對象建模過程。
按照HTTP協議的規定,GET方法是安全且冪等的,POST方法是既不安全也不冪等的(可以用來作為所有寫操作的通配方法),PUT、 DELETE方法都是不安全但冪等的。將對資源的操作合理映射到這四個方法上面,既不過度使用某個方法(例如過度使用GET方法或POST方法),也不添 加過多的操作以至於HTTP的四個方法不夠用。
是否全用POST方法實現接口?
在接口設計裡面經常會遇到一個問題,即是否全部用POST方法來實現接口,如果圖簡單省事,那麼全部用POST方法是最省事的方式。
對於Get方法相對Post方法來說究竟有哪些優勢?網上一段話可以參考如下: GET 的URL可以人肉手工在地址欄輸入啊。。。你在地址欄打個POST給我看看。本質上面, GET 的所有信息都在URL, 所以可以很方便的記錄下來重複使用。
也就是說通過Get方法可以更好地方便測試,驗證,緩存等。
如果不考慮這點,確實可以完全用Post方法來實現所有Rest API接口。但是我們仍然建議對於簡單的對象獲取,根據Key值獲取等仍然可以實現為Get方法。
其次對於比較複雜的模糊查詢,還涉及到排序,多條件過濾的,雖然網上也有Rest API規範來強調用Get方法如何來定義,但是我們仍然推薦還是採用POST方法通過Body傳遞更合適。
反之,涉及到數據新增修改等操作,再簡單也不建議通過Get方法進行。
API接口的版本問題
對於API接口的版本定義,仍然推薦在IP位址或域名後先定義版本,再定義詳細的對象集合和ID信息。具體如下:
GET https://{serviceRoot}/{collection}/{id}
GET https://www.aa.com/v1.0/user/123/
當然還有一種方法是在後面添加版本,比如:
https://api.contoso.com/user/123?version=v1
這種方式為何不好?
即我們很難通過前面的接口定義和URL信息明確地知道究竟是消費哪個版本,而具體接口的版本信息必須動態在調用的時候參數化傳遞。
那麼我們就很難將顯性化的細粒度控制到具體的版本。特別是在接口啟用大小版本的情況下,實際上大小版本已經是兩個輸入輸出有明顯差異的服務,必須單獨控制。
包括用這種動態方式,API接口不同版本要註冊接入到API網關本身也相當麻煩難以實現。當然如果你全部用POST方式實現接口,那麼版本信息定義在路徑末尾也是可以的,比如:
https://api.contoso.com/GetPeopleByID/V1
具體更加詳細的說明可以參考微軟的Rest API接口定義規範。
主流的Swagger設計工具
對於設計工具,這裡只談下Swagger設計工具,這個工具最大的好處就是只需要通過編輯器進行Rest接口設計文件的定義,然後可以自動生成測試框架,自動生成客戶端和服務端多種語言下的開發框架,生成API接口設計文檔,這個工具本身設計完成內容還可以導出為YAML文件或Json文件,也支撐文件導入。
在採用Swagger工具的時候,我們希望的步驟為:
首先是通過Swagger Editor進行接口文件的定義,對於接口文件本身的定義建議仍然進行分域而不是全部都定義到一個文件裡面。
其次基於接口定義,通過Swagger提供的CodeGen功能來生成RestClient,或者整個SpingBoot項目。這個生成既需要包括客戶端消費框架,也需要包括服務提供端框架。類似原來基於WSDL文件,用CXF框架來生成代碼框架一樣。
注意網上也有Spingcloud框架來集成Swagger的文章,更多的則是對已經定義好的接口來生成API接口文檔,這並不是完整的接口驅動開發。
再次,通過接口定義來自動生成Mock數據,可以通過Swagger Json文件定義格式在前端通過JS來產生mock數據,也可以搭建一個完整的Mock Server產生數據,在這個步驟完成後,前端開發人員可以開始並行開發工作。
最後,API接口文檔生成,Swagger在完成了接口定義後本身就已經形成了完整的接口定義,這個定義本身可以導出為完整的Html文檔,也可以自己寫代碼進一步自動轉化為Word文檔。
即基於Swagger這類工具,再加上少量的定製,基本就可以實現一個完整的接口驅動開發中所需要的所有文檔,代碼開發框架等。