最近給團隊新人講了一些設計上的常識,可能會對其它的新人也有些幫助, 把暫時想到的幾條,先記在這裡。
1、API與SPI分離
框架或元件通常有兩類客戶,一個是使用者,一個是擴充套件者。 API(Application Programming Interface)是給使用者用的, 而SPI(Service Provide Interface)是給擴充套件者用的。 在設計時,儘量把它們隔離開,而不要混在一起, 也就是說,使用者是看不到擴充套件者寫的實現的。
比如:
- 一個Web框架,它有一個API介面叫Action, 裡面有個execute()方法,是給使用者用來寫業務邏輯的。然後,Web框架有一個SPI介面給擴充套件者控制輸出方式。
- velocity模板輸出還是用json輸出等, 如果這個Web框架使用一個都繼承Action的VelocityAction和一個JsonAction做為擴充套件方式, 要用velocity模板輸出的就繼承VelocityAction,要用json輸出的就繼承JsonAction, 這就是API和SPI沒有分離的反面例子。
SPI介面混在了API介面中,合理的方式是,有一個單獨的Renderer介面,有VelocityRenderer和JsonRenderer實現, Web框架將Action的輸出轉交給Renderer介面做渲染輸出。
反正例子:
正確例子:
2、服務域/實體域/會話域分離
任何框架或元件,總會有核心領域模型,比如:
實體域:像Spring的Bean,Struts的Action,Dubbo的Service,Napoli的Queue等等 。這個核心領域模型及其組成部分稱為實體域,它代表著我們要操作的目標本身, 實體域通常是執行緒安全的,不管是通過不變類,同步狀態,或複製的方式。
服務域:也就是行為域,它是元件的功能集,同時也負責實體域和會話域的生命週期管理。比如Spring的ApplicationContext,Dubbo的ServiceManager等, 服務域的物件通常會比較重,而且是執行緒安全的,並以單一例項服務於所有呼叫。
會話域:就是一次互動過程, 會話中重要的概念是上下文,什麼是上下文? 比如我們說:“老地方見”,這裡的“老地方”就是上下文資訊, 為什麼說“老地方”對方會知道,因為我們前面定義了“老地方”的具體內容, 所以說,上下文通常持有互動過程中的狀態變數等, 會話物件通常較輕,每次請求都重新建立例項,請求結束後銷燬。
簡而言之: 把元資訊交由實體域持有, 把一次請求中的臨時狀態由會話域持有, 由服務域貫穿整個過程。
例項一
例項二
兩 種 例 子
複製程式碼
3、在重要的過程上設定攔截介面
1.如果你要寫個遠端呼叫框架,那遠端呼叫的過程應該有一個統一的攔截介面;
2.如果你要寫一個ORM框架,那至少SQL的執行過程,Mapping過程要有攔截介面;
3.如果你要寫一個Web框架,那請求的執行過程應該要有攔截介面;
複製程式碼
等等,就可以自行完成,而不用侵入框架內部。攔截介面,通常是把過程本身用一個物件封裝起來,傳給攔截器鏈。
比如:遠端呼叫主過程為invoke(),那攔截器介面通常為invoke(Invocation),Invocation物件封裝了本來要執行過程的上下文,並且Invocation裡有一個invoke()方法, 由攔截器決定什麼時候執行。同時,Invocation也代表攔截器行為本身, 這樣上一攔截器的Invocation其實是包裝的下一攔截器的過程, 直到最後一個攔截器的Invocation是包裝的最終的invoke()過程, 同理,SQL主過程為execute(),那攔截器介面通常為execute(Execution),原理一樣, 當然,實現方式可以任意,上面只是舉例。
4、重要的狀態的變更傳送事件並留出監聽介面
這裡先要講一個事件和上面攔截器的區別:
攔截器:是干預過程的,它是過程的一部分,是基於過程行為的。 事件:是基於狀態資料的,任何行為改變的相同狀態,對事件應該是一致的,事件通常是事後通知,是一個Callback介面,方法名通常是過去式的,比如onChanged()。
比如遠端呼叫框架,當網路斷開或連上應該發出一個事件,當出現錯誤也可以考慮發出一個事件, 這樣外圍應用就有可能觀察到框架內部的變化,做相應適應。
5、擴充套件介面職責儘可能單一,具有可組合性
比如,遠端呼叫框架它的協議是可以替換的, 如果只提供一個總的擴充套件介面,當然可以做到切換協議, 但協議支援是可以細分為底層通訊,序列化,動態代理方式等等, 如果將介面拆細,正交分解,會更便於擴充套件者複用已有邏輯,而只是替換某部分實現策略, 當然這個分解的粒度需要把握好。
6、微核外掛式,平等對待第三方
大凡發展的比較好的框架,都遵守微核的理念
Eclipse的微核是OSGi, Spring的微核是BeanFactory,Maven的微核是Plexus。
通常核心是不應該帶有功能性的,而是一個生命週期和整合容器, 這樣各功能可以通過相同的方式互動及擴充套件,並且任何功能都可以被替換, 如果做不到微核,至少要平等對待第三方, 即原作者能實現的功能,擴充套件者應該可以通過擴充套件的方式全部做到, 原作者要把自己也當作擴充套件者,這樣才能保證框架的可持續性及由內向外的穩定性。
7、不要控制外部物件的生命週期
比如上面說的Action使用介面和Renderer擴充套件介面, 框架如果讓使用者或擴充套件者把Action或Renderer實現類的類名或類元資訊報上來。然後在內部通過反射newInstance()建立一個例項, 這樣框架就控制了Action或Renderer實現類的生命週期, Action或Renderer的生老病死,框架都自己做了,外部擴充套件或整合都無能為力。
好的辦法是讓使用者或擴充套件者把Action或Renderer實現類的例項報上來, 框架只是使用這些例項,這些物件是怎麼建立的,怎麼銷燬的,都和框架無關, 框架最多提供工具類輔助管理,而不是絕對控制。
8、可配置一定可程式設計,並保持友好的CoC約定
因為使用環境的不確定因素很多,框架總會有一些配置, 一般都會到classpath直掃某個指定名稱的配置,或者啟動時允許指定配置路徑, 做為一個通用框架,應該做到凡是能配置檔案做的一定要能通過程式設計方式進行, 否則當使用者需要將你的框架與另一個框架整合時就會帶來很多不必要的麻煩。
另外,儘可能做一個標準約定,如果使用者按某種約定做事時,就不需要該配置項。 比如:配置模板位置,你可以約定,如果放在templates目錄下就不用配了, 如果你想換個目錄,就配置下。
9、區分命令與查詢,明確前置條件與後置條件
這個是契約式設計的一部分,儘量遵守有返回值的方法是查詢方法,void返回的方法是命令, 查詢方法通常是冪等性的,無副作用的,也就是不改變任何狀態,調n次結果都是一樣的。比如get某個屬性值,或查詢一條資料庫記錄。
命令是指有副作用的,也就是會修改狀態,比如set某個值,或update某條資料庫記錄, 如果你的方法即做了修改狀態的操作,又做了查詢返回,如果可能,將其拆成寫讀分離的兩個方法。
比如:
- User deleteUser(id),刪除使用者並返回被刪除的使用者,考慮改為getUser()和void1的deleteUser()。
- 另外,每個方法都儘量前置斷言傳入引數的合法性,後置斷言返回結果的合法性,並文件化。
10、增量式擴充套件,而不要擴充原始核心概念
我們平臺的產品越來越多,產品的功能也越來越多, 平臺的產品為了適應各BU和部門以及產品線的需求。勢必會將很多不相干的功能湊在一起,客戶可以選擇性的使用, 為了相容更多的需求,每個產品,每個框架,都在不停的擴充套件, 而我們經常會選擇一些擴充套件的擴充套件方式,也就是將新舊功能擴充套件成一個通用實現。
我想討論是,有些情況下也可以考慮增量式的擴充套件方式,也就是保留原功能的簡單性,新功能獨立實現。我最近一直做分散式服務框架的開發,就拿我們專案中的問題開涮吧。
比如:遠端呼叫框架,肯定少不了序列化功能,功能很簡單,就是把流轉成物件,物件轉成流, 但因有些地方可能會使用osgi,這樣序列化時,IO所在的ClassLoader可能和業務方的ClassLoader是隔離的, 需要將流轉換成byte[]陣列,然後傳給業務方的ClassLoader進行序列化。
為了適應osgi需求,把原來非osgi與osgi的場景擴充套件了一下, 這樣,不管是不是osgi環境,都先將流轉成byte[]陣列,拷貝一次。然而,大部分場景都用不上osgi,卻為osgi付出了代價, 而如果採用增量式擴充套件方式,非osgi的程式碼原封不動, 再加一個osgi的實現,要用osgi的時候,直接依賴osgi實現即可。
再比如:最開始,遠端服務都是基於介面方法,進行透明化呼叫的, 這樣,擴充套件介面就是,invoke(Method method, Object[] args), 後來,有了無介面呼叫的需求,就是沒有介面方法也能呼叫,並將POJO物件都轉換成Map表示, 因為Method物件是不能直接new出來的,我們不自覺選了一個擴充套件式擴充套件, 把擴充套件介面改成了invoke(String methodName, String[] parameterTypes, String returnTypes, Object[] args), 導致不管是不是無介面呼叫,都得把parameterTypes從Class[]轉成String[]。
如果選用增量式擴充套件,應該是保持原有介面不變, 增加一個GeneralService介面,裡面有一個通用的invoke()方法, 和其它正常業務上的介面一樣的呼叫方式,擴充套件介面也不用變, 只是GeneralServiceImpl的invoke()實現會將收到的呼叫轉給目標介面, 這樣就能將新功能增量到舊功能上,並保持原來結構的簡單性。
再再比如:無狀態訊息傳送,很簡單,序列化一個物件發過去就行, 後來有了同步訊息傳送需求,需要一個Request/Response進行配對, 採用擴充套件式擴充套件,自然想到,無狀態訊息其實是一個沒有Response的Request, 所以在Request里加一個boolean狀態,表示要不要返回Response, 如果再來一個會話訊息傳送需求,那就再加一個Session互動。然後發現,原來同步訊息傳送是會話訊息的一種特殊情況, 所有場景都傳Session,不需要Session的地方無視即可。 如果採用增量式擴充套件,無狀態訊息傳送原封不動。
同步訊息傳送,在無狀態訊息基礎上加一個Request/Response處理, 會話訊息傳送,再加一個SessionRequest/SessionResponse處理。
為什麼某些人會一直比你優秀,是因為他本身就很優秀還一直在持續努力變得更優秀,而你是不是還在滿足於現狀內心在竊喜!
我本人邀約各大BATJ架構大牛共創Java架構師社群群,(群號:673043639)致力於免費提供Java架構行業交流平臺,通過這個平臺讓大家相互學習成長,提高技術,讓自己的水平進階一個檔次,成功通往Java架構技術大牛或架構師發展
合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
To-陌霖Java架構
複製程式碼
分享網際網路最新文章 關注網際網路最新發展