文章出處

 

模式

 

接下來去聊一聊框架設計中的一些常見設計模式,這和傳統的一些設計模式不同(之前寫過 無廢話C#設計模式系列文章,有興趣的讀者可以去看一下),這里聊的一些設計模式是比較高層的粗粒度的架構設計模式,主要是用于之前說的構建框架的龍骨,使得框架中的幾百個類型可以有結構有條理組織在一起,在這些模式之中你完全可以再去使用各種Gof設計模式,這是不沖突的。

 

分層

 

在之前的文章中其實已經強調過這個概念了。即使我們寫的是同一個框架,對于框架中的所有類型可能也會有多層的結構:

  1. 純抽象類型(接口),用于定義框架的骨骼,比如IController
  2. 半抽象類型(抽象類或帶默認實現的接口),用于定義框架的骨骼并且還是共有邏輯在載體,比如AbstractController
  3. 具體的實現類型,在骨骼的基礎上的不同的實現,比如VelocityViewEngine

在通常情況下是可以把這些類型都放到一個模塊內方便閱讀和管理,在一些特殊的情況下我們可以把框架直接在層次上拆分成多層多個框架(這些父子框架之間可以繼承關系),比如:

  1. 如果有太多具體類型是過于具體的和業務邏輯相關的實現,而且實現的數量非常龐大
  2. 框架雖然整體解決的是一個問題,但針對不同的環境(比如網站、桌面和移動)在實現上有比較大的差異,那么我們可以把共同的部分放到父框架中,為不同環境提供單獨的擴展框架
  3. 如果框架會和某個業務邏輯進行銜接,如果業務邏輯的接口不穩定的話,完全可以把這種不穩定的因素隔離出一個單獨的子框架,保持父框架的問題

總之,OO的概念不一定可以應用于類型之間的關系,還可以應用到框架本身的層次關系上。

 

處理器和過濾器

 

框架就像是空氣凈化器的主框架它會確保空氣吸入并走過所有的過濾器,過濾器就像是空氣凈化器的多層過濾各司其職,處理器負責把新鮮的空氣排出。這是一個非常非常常見的框架的設計模式,幾乎所有的面向數據流的框架,比如Web框架、Web MVC框架(ASP.NET MVC、Spring MVC等)、網絡框架(Netty、Mina等)都會采用這種模式。因為這種模式:

  1. 由處理器來定義框架的主要的數據處理模型,由過濾器定義數據的加工模型,非常適合于基于數據處理的框架。
  2. 可以讓框架有極大的擴展性,由框架來搭建主線流程,提供一個抽象模型,由開發人員來真正實現數據的處理業務邏輯。
  3. 非常符合職責分離的原則,而且非常靈活,有的框架甚至提供了運行時根據實際情況臨時構建各種過濾器和處理器的棧。

這種模式有非常多的變種,而且過濾器和處理器之間往往在概念上也會有一些融合:

  1. 框架會暴露出一些事件由過濾器來處理,有的時候這些事件僅僅是發生在處理器之前(如第一個圖)
  2. 有的時候在處理器執行前和執行后都會有一些事件,甚至過濾器是可以改變已處理的結果的(如第二個圖)
  3. 過濾器只是訂閱了所有或部分事件進行處理的一些類,只要過濾器注冊到了框架那么框架就會按照一定的順序去執行過濾器,往往過濾器是會有多個,處理器只會有一個,如果有多個的話也是最終選擇一個合適的處理器來處理
  4. 有框架處理器也是職責鏈式的(如第三個圖),允許多個處理器對數據或部分數據進行處理,這個時候處理器又有一點像過濾器了(也有一些框架沒有過濾器,只有處理器鏈)
  5. 有一些框架的過濾器和處理器是各自獨立的,它們只是管線的一部分,無法感知其它過濾器或處理器
  6. 而有一些框架的過濾器可以在過濾的過程中直接告知框架跳過后續的過濾器,或告知框架執行哪個處理器,甚至是告知框架跳過處理器直接提供結果
  7. 類似的,如果框架允許多個處理器,有的框架是允許處理器告知框架是否要執行后續的處理器的
  8. 大多數框架的過濾器管線在處理器之外,而有一些框架在處理器的內部安排了一組過濾器,也就是下圖的整個圖就是一個處理器,其中的處理器變為了執行器
  9. 有的框架的內部有多條過濾處理的管線,甚至是結合了分發器一起使用,我們回想一下我們的Web MVC框架是不是就是由分發器->處理器(處理器本身由前置過濾->處理->后置過濾+另外一組前置過濾->處理->后置過濾構成)的?
  10. 有框架沒有使用過濾器這個名詞而是叫攔截器,本質上差異不大,如果攔截器和處理器并存那么需要辨別其差異

總之,掌握了這個重要的模式不但對自己寫框架有幫助,對使用其它框架也非常有幫助。

 

依賴注入

 

首先想說一個比較偏激的觀點,有很多框架對于其內部的組件采用了全面的外部依賴注入,框架整體的靈活性很高耦合很低,但是這樣就會導致框架本身不是那么高內聚而且會造成框架使用者一些困擾,我是不太喜歡這么做的,我框架內部的組件可以是由依賴注入的,但是最好提供一個配置中心,提供統一的注入點而不是由使用者在用的時候來初始化這個框架的結構。所以對于框架提供的依賴注入,我更覺得它應該為各種由框架使用者提供的代碼(比如插件、過濾器、處理器)進行依賴的解析。

對于很多框架如果它本身就是會和業務邏輯進行綁定的,那么提供一個依賴注入的入口可能必不可少(不是提供依賴解析的功能),我們可以提供類似一個IDependencyResolver/Adapter的接口,由框架的使用者去選擇合適的IOC框架或類庫進行實現,使得框架創建的外部對象都能夠得到依賴的解析。

對于一些全棧框架,甚至可以在框架內部自帶一個默認的IOC實現,使用和框架更為契合的API為使用者進行自動的依賴解析,在大多數時候我們真的不需要一個復雜且完整的東西,我們只需要一個簡單且高效的東西。

 

各種器/者(Er/Or)

 

如下列出了各種ErOr,其中粗粒度的一些類型在之前的一些模式中有提到過部分,其余部分也是很多框架的老面孔了:

 

粗粒度/高抽象層的:

  • Dispatcher
  • Consumer
  • Producer
  • Publisher
  • Subscriber
  • Handler
  • Filter
  • Interceptor
  • Provider
  • Container
  • ……

 

中粒度/中抽象層的:

  • Locator
  • Creator
  • Initializer
  • Reader
  • Writer
  • Activator
  • Finder
  • Builder
  • Selector
  • Visitor
  • Loader
  • Descriptor
  • Generator
  • Adapter
  • Listener
  • Wrapper
  • Mapper
  • Binder
  • Invoker
  • Executor
  • Detector
  • Tracer
  • Decorator
  • Mapper
  • Resolver
  • Processor
  • Advisor
  • ……

 

細粒度/底層的:

  • Locker
  • Iterator
  • Extractor
  • Accessor
  • Validator
  • Formatter
  • Converter
  • Replacer
  • Comparer
  • Manager
  • Combiner
  • Parser
  • Encoder
  • Decoder
  • Importer
  • Exporter
  • Editor
  • Modifier
  • Evaluator
  • ……

這里列的這些都是類型(類或接口)而不是方法,它們的共性就是以動詞的名詞形式來命名類型,一般很多時候動詞對應的是方法或函數是某個操作,之所以很多框架把函數往上提成了方法的原因是:

  1. 框架的組件一般會有很多重用并且要支持組件擴展,把函數提取為類型才可以享受到抽象、繼承、多態。
  2. 很多高層次和中層次的這些ErOr往往對應的是一種設計模式,設計模式能讓復雜的代碼變得有調理和充實飽滿。
  3. 框架的代碼一般比較復雜,只有讓每一個組件都有很明確的職責,才能讓代碼清晰。當然,如果你的Replacer真的是一句replace(),你的Encoder()真是只是一句encode(),那是沒有必要搞這么復雜的。如果在重構的時候我們發現方法的邏輯有重復,并且邏輯涉及到多種實現,我們就可以考慮把方法提升到ErOr然后通過模版方法避免重復。

在這里無法一一對每一種類型進行詳細的闡述,如果大家有興趣可以去下載一些框架的源碼,進一步學習一下每種ErOr背后的模式和用法,很多名詞都已經成為了標準,所以如果你也采用同樣的名詞來命名相關類型的話,會助于閱讀你框架源碼的人理解。

 

發布訂閱和消息總線

 

對于分布式應用程序,我們經常會采用消息總線或發布訂閱隊列來解耦各種組件。

通過發布訂閱,我們可以解耦發布者和訂閱者:

  1. 讓關心某個數據的組件可以訂閱某個話題,且不用關心數據的來源
  2. 讓產生某個數據的組件可以談論某個話題,且不用關心是否誘人訂閱了我的話題
  3. 同時可以實現很多高級功能,比如按照模式來訂閱話題,消息的持久化等等

如果你的系統中有大量的異步事件,實現了發布訂閱模式,那么不管將來擴展了多少事件以及事件的處理變得多少復雜,我們都不用去改變既有的實現。

總線是一種特殊的發布訂閱模式,通過總線,我們解耦提供者和消費者:

  1. 讓所有服務的提供者自己注冊到總線上,告知總線提供某個服務
  2. 讓所有服務的消費者在無需知道下游提供者的情況下進行服務的調用
  3. 同時總線可以做很多額外的工作,比如負載、心跳、限流、短路等

如果你的系統中提供了非常多的服務,使用了總線,服務之間的調用就不會有任何具體實現的依賴。

其實我想畫一個圖的,但Visio總是把箭頭亂指,折騰了老半天箭頭永遠是斜著的,太難看了算了

你可能會問,用于分布式應用程序的模式和我們框架的設計有什么關聯?個人覺得如果你從事的是一個具有狀態的框架(比如IOC就是一種具有狀態的框架,但MVC不應該是,或應用程序,特別是桌面和移動應用程序)的開發,那么有些時候可能會使用到一下這兩種模式來進行大規模的解耦。怎么使用?用RabbitMQ、Kafka? 不是,可以自己嘗試實現:

  1. 一個基于內存的容器(作為Broker),由容器來管理話題(元數據)以及上下游對象,并且進行消息的路由(轉發),這就實現了發布訂閱
  2. 如果要實現總線,在這個基礎上擴展一下即可,可以想到如果有人訂閱Request且發布Response那么它是服務的提供者(不過Request只用推送給任意一個訂閱者即可),如果有人發布Request且訂閱Response那么它是服務的消費者
  3. 當然,作為框架內使用的總線,我們可能不需要去實現諸如負載、心跳、限流等功能,數據的流轉也可以是同步模式的

放到最后說這個模式是因為這其實是一個不錯的練習,如果你有興趣的話看了本文可以自己去嘗試使用本文介紹的一些步驟和模式實現一個基于內存的輕量級的RabbitMQ/Kafka作為練習,謝謝閱讀。另外推薦大家可以去看一下POSA面向模式的軟件架構的前兩卷。


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()