文章出處

 

說明:作者也沒寫過什么框架,只是分享一些自己的理解,拋磚引玉罷了。如果你寫過一些框架可能會產生一些共鳴歡迎討論,如果你正在寫或正打算寫一個框架可能會給你一些啟發。本文以為較長可能會分多個篇博客來寫,現在能想到的是主要分為步驟、模式兩部分。如果你覺得好,按一個推薦舉手之勞讓更多的人可以看到。

 

步驟

 

定位

 

所謂定位就是回答幾個問題,我出于什么目的要寫一個框架,我的這個框架是干什么的,有什么特性適用于什么場景,我的這個框架的用戶對象是誰,他們會怎么使用,框架由誰維護將來怎么發展等等。

  1. 如果你打算寫框架,那么肯定心里已經有一個初步的定位,比如它是一個緩存框架、Web MVC框架、IOC框架、ORM/數據訪問框架、RPC框架或是一個用于Web開發的全棧式框架。
  2. 是否要重復造輪子?除非是練手項目,一般我們是有了解決不了問題的時候才會考慮不使用既有的成熟的框架而重復造輪子的,這個時候需要列出新框架主要希望解決什么問題。有關是否應該重復造輪子的話題討論了很多,我的建議是在把問題列清后進行簡單的研究看看是否可以通過擴展現有的框架來解決這個問題。一般而言大部分成熟的框架都有一定的擴展和內部組件的替換能力,可以解決大部分技術問題,但在如下情況下我們可能不得不自己去寫一個框架,比如即使通過擴展也無法滿足技術需求、安全原因、需要更高的生產力、需要讓框架和公司內部的流程更好地進行適配、開源的普適框架無法滿足性能需求、二次開發的成本高于重新開發的成本等等。
  3. 主打輕量級?輕量級是很多人打算自己寫一個新框架的原因,但我們要明白,大部分項目在一開始的時候其實都是輕量級的,隨著框架的用戶越來越多,它必定需要滿足各種奇怪的需求,在經過了無數次迭代之后,框架的主線流程就會多很多擴展點、檢測點,這樣框架勢必變得越來越重(從框架的入口到框架的工作結束的方法調用層次越來越多,勢必框架也就越來越慢),如果你打算把框架定位于一個輕量級的框架的話,那么在今后的迭代過程中需要進行一些權衡,在心中有堅定的輕量級的理念的同時不斷做性能測試來確保框架的輕量,否則隨著時間的發展框架可能會越來越重進而偏離了開始的定位。
  4. 特性?如果你打算寫一個框架,并且只有輕量級這一個理由的話,你或許應該再為自己的框架想一些新特性,就像做一個產品一樣,如果找不出兩個以上的亮點,那么這個產品不太可能成功,比如你的新框架可以是一個零配置的框架,可以是一個前端開發也能用的后端框架。
  5. 其它?一般來說框架是給程序員使用的,我們要考慮框架使用的頻度是怎么樣的,這可能決定的框架的性能需求和穩定性需求。還有,需要考慮框架將來怎么發展,是希望走開源路線還是商業路線。當然,這些問題也可以留到框架有一個大致的結構后再去考慮。

我們來為本文模擬一個場景,假設我們覺得現有的Spring MVC等框架開發起來效率有點低,打算重復造輪子,對于新框架的定位是一個給Java程序員使用的輕量級的、零配置的、易用的、易擴展的Web MVC框架。

 

調研

 

雖然到這里你已經決定去寫一個框架了,但是在著手寫之前還是至少建議評估一下市面上的類似(成熟)框架。需要做的是通讀這些框架的文檔以及閱讀一些源碼,這么做有幾個目的:

  1. 通過分析現有框架的功能,可以制定出一個新框架要實現的功能列表。
  2. 通過分析現有框架的問題,總結出新框架需要避免的東西和改善的地方。
  3. 通過閱讀現有框架的源碼,幫助自己理清框架的主線流程為總體設計做鋪墊(后面總體設計部分會更多談到)。
  4. 如果能充分理解現有的框架,那么你就是站在巨人的肩膀上寫框架,否則很可能就是在井底造輪子。

新開發一個框架的好處是沒有兼容歷史版本的包袱,但是責任也同樣重大,因為如果對于一開始的定位或設計工作沒有做好的話,將來如果要對格局進行改變就會有巨大的向前兼容的包袱(除非你的框架沒有在任何正式項目中使用),兼容意味著框架可能會越來越重,可能會越來越難看,閱讀至少一到兩個開源實現,做好充分的調研工作可以使你避免犯大錯。

假設我們評估了一些主流框架后已經很明確,我們的MVC框架是一個Java平臺的、基于Servlet的輕量級的Web MVC框架,主要的理念是約定優于配置,高內聚大于低耦合,提供主流Web MVC框架的大部分功能,并且易用方面有所創新,新特性體包括:

  1. 起手零配置,總體上約定由于配置,即使需要擴展配置也支持通過代碼和配置文件兩種方式進行配置。
  2. 除了Servlet之外不依賴其它類庫,支持通過插件方式和諸如Spring等框架進行整合。
  3. 更優化的項目結構,不需要按照傳統的Java Web項目結構那樣來分離代碼和WEB-INF,視圖可以和代碼在一起,閱讀代碼更便利。
  4. 攔截器和框架本身更緊密,提供Action、Controller和Global三個級別的"攔截器"(或者說過濾器)。
  5. 豐富的Action的返回值,返回的可以是視圖、可以是重定向、可以是文件、可以是字符串、可以是Json數據,可以是Javascript代碼等等。
  6. 支持針對測試環境自動生成測試的視圖模型數據,以便前端和后端可以同時開發項目。
  7. 支持在開發的時候自動生成路由信息、模型綁定、異常處理等配置的信息頁面和調試頁面,方便開發和調試。
  8. 提供一套通用的控件模版,使得,并且支持多種模版引擎,比如Jsp、Velocity、Freemarker、Mustache等等。

嗯,看上去挺誘人的,這是一個不錯的開端,如果你要寫的框架自己都不覺得想用的話,那么別人就更不會有興趣來嘗試使用你的框架了。

 

解決難點

 

之所以把解決難點放在開搞之前是因為,如果實現這個框架的某些特性,甚至說實現這個框架的主流程有一些核心問題難以解決,那么就要考慮對框架的特性進行調整,甚至取消框架的開發計劃了。有的時候我們在用A平臺的時候發現一個很好用的框架,希望把這個框架移植到B平臺,這個想法是好的,但之所以在這以前這么多年沒有人這么干過是因為這個平臺的限制壓根不可能實現這樣的東西。比如我們要實現一個MVC框架,勢必需要依賴平臺提供的反射特性,如果你的語言平臺壓根就沒有運行時反射這個功能,那么這就是一個非常難以解決的難點。又比如我們在某個平臺實現一個類似于.NET平臺Linq2Sql的數據訪問框架,但如果這個目標平臺的開發語言并不像C#那樣提供了類型推斷、匿名類型、Lambda表達式、擴展方法的話那么由于語法的限制你寫出來的框架在使用的時候是無法像.NET平臺Linq2Sql那樣優雅的,這就違背了實現框架的主要目的,實現新的框架也就變得意義不大了。

對于我們要實現的MVC框架貌似不存在什么根本性的無法解決的問題,畢竟在Java平臺已經有很多可以參考的例子了。如果框架的實現總體上沒什么問題的話,就需要逐一評估框架的這些新特性是否可以解決。建議對于每一個難點特性做一個原型項目來證明可行,以免在框架實現到一半的時候發現有無法解決的問題就比較尷尬了。

分析一下,貌似我們要實現的這8大特性只有第1點要研究一下,看看如何免配置通過讓代碼方式讓我們的Web MVC框架可以和Servlet進行整合,如果無法實現的話,我們可能就需要把第1點特性從零配置改為一分鐘快速配置了。

 

開搞

 

  1. 首先需要給自己框架取一個名字,取名要考慮到易讀、易寫、易記,也需要盡量避免和市面上其它產品的名字重復,還有就是最好不要起一個侮辱其它同類框架的名字以免引起公憤。
  2. 如果將來打算把項目搞大的話,可以提前注冊一下項目的相關域名,畢竟現在域名也便宜,避免到時候項目名和域名差距很大,或項目的.com或.org域名對應了一個什么不太和諧的網站這就尷尬了。
  3. 然后就是找一個地方來托管自己的代碼,如果一開始不希望公開代碼的話,最好除了本地源代碼倉庫還有一個異地的倉庫以免磁盤損壞導致抱憾終身,當然如果不怕出丑的話也可以在起步的時候就使用Github等網站來托管自己的代碼。

 

總體設計

 

對于總體設計我的建議是一開始不一定需要寫什么設計文檔畫什么類圖,因為可能一開始的時候無法形成這么具體的概念,我們可以直接從代碼開始做第一步。框架的使用者一般而言還是開發人員,拋開框架的內在的實現不說,框架的API設計的好壞取決于兩個方面。對于普通開發人員而言就是使用層面的API是否易于使用,拿我們的MVC框架舉例來說:

  1. 最基本的,搭建一個HelloWorld項目,聲明一個Controller和Action,配置一個路由規則讓Get方法的請求可以解析到這個Action,可以輸出HelloWorld文字,怎么實現?
  2. 如果要實現從Cookie以及表單中獲取相關數據綁定到Action的參數里面,怎么實現?
  3. 如果要配置一個Action在調用前需要判斷權限,在調用后需要記錄日志,怎么實現?

我們這里說的API,它不一定全都是方法調用的API,廣義上來說我們認為框架提供的接入層的使用都可以認為是API,所以上面的一些功能都可以認為是MVC框架的API。

框架除了提供基本的功能,還要提供一定程度的擴展功能,使得一些復雜的項目能夠在某些方面對框架進行增強以適應各種需求,比如:

  1. 我的Action是否可以返回圖片驗證碼?
  2. 我的Action的參數綁定是否可以從Memcached中獲取數據?
  3. 如果出現異常,能否在開發的時候顯示具體的錯誤信息,在正式環境顯示友好的錯誤頁面并且記錄錯誤信息到數據庫?

一般而言如果要實現這樣的功能就需要自己實現框架公開的一些類或接口,然后把自己的實現"注冊"到框架中,讓框架可以在某個時候去使用這些新的實現。這就需要框架的設計者來考慮應該以怎么樣的友好形式公開出去哪些內容,使得以后的擴展實現在自由度以及最少實現上的平衡,同時要兼顧外來的實現不破壞框架已有的結構。

要想清楚這些不是一件容易的事情,所以在框架的設計階段完全可以使用從上到下的方式進行設計。也就是不去考慮框架怎么實現,而是以一個使用者的身份來寫一個框架的示例網站,API怎么簡單怎么舒服就怎么設計,只從使用者的角度來考慮問題。對于相關用到的類,直接寫一個空的類(能用接口的盡量用接口,你的目的只是通過編譯而不是能運行起來),讓程序可以通過編譯就可以了。你可以從框架的普通使用開始寫這樣一個示例網站,然后再寫各種擴展應用,在此期間你可能會用到框架內部的20個類,這些類就是框架的接入類,在你的示例網站通過編譯的那剎那,其實你已經實現了框架的接入層的設計。

這里值得一說的是API的設計蘊含了非常多的學問以及經驗,要在目標平臺設計一套合理易用的API首先需要對目標平臺足夠了解,每一個平臺都有一些約定俗成的規范,如果設計的API能符合這些規范那么開發人員會更容易接受這個框架,此外還有一些建議:

  1. 之所以我們把API的設計先行,而不是讓框架的設計先行是因為這樣我們更容易設計出好用的API,作為框架的實現者,我們往往會進行一些妥協,我們可能會為了在框架內部DRY而設計出一套丑陋的API讓框架的使用者去做一些重復的工作;我們也可能會因為想讓框架變得更松耦合強迫框架的使用者去使用到框架的一些內部API去初始化框架的組件。如果框架不是易用的,那么框架的內部設計的再合理又有什么意義?
  2. 盡量少暴露一些框架內部的類名吧,對于框架的使用者來說,你的框架對他一點都不熟悉,如果要上手你的框架需要學習一到兩個類尚可接受,如果要使用到十幾個類會頭暈腦脹的,即使你的框架有非常多的功能以及配置,可以考慮提供一個入口類,比如創建一個ConfigCenter類作為入口,讓使用者可以僅僅探索這個類便可對框架進行所有的配置。
  3. 一個好的框架是可以讓使用者少犯錯誤的,框架的設計者務必要考慮到,框架的使用者沒有這個業務來按照框架的最佳實踐來做,所以在設計API的時候,如果你希望API的使用者一定要按照某個方式來做的話,可以考慮設置一個簡便的重載來加載默認的最合理的使用方式而不是要求使用者來為你的方法初始一些什么依賴,同時也可以在API內部做一些檢測,如果發現開發人員可能會犯錯進行一些提示或拋出異常。好的框架無需過多的文檔,它可以在開發人員用的時候告知它哪里錯了,最佳實踐是什么,即便他們真的錯了也能以默認的更合理的方式來彌補這個錯誤。
  4. 建議所有的API都有一套統一的規范,比如入口都叫XXXCenter或XXXManager,而不是叫XXXCenter、YYYManager和ZZZService。API往往需要進行迭代和改良的,在首個版本中把好名字用掉也不一定是一個好辦法,最好還是給自己的框架各種API的名字留一點余地,這樣以后萬一需要升級換代不至于太牽強。

下一步工作就是把項目中那些空的類按照功能進行劃分。目的很簡單,就是讓你的框架的100個類或接口能夠按照功能進行拆分和歸類,這樣別人一打開你的框架就可以馬上知道你的框架分為哪幾個主要部分,而不是在100個類中暈眩;還有因為一旦在你的框架有使用者后你再要為API相關的那些類調整包就比困難了,即使你在創建框架的時候覺得我的框架就那么十幾個類無需進行過多的分類,但是在將來框架變大又發現當初設計的不合理,無法進行結構調整就會變得很痛苦。因此這個工作還是相當重要的,對于大多數框架來說,可以有幾種切蛋糕的方式:

  1. 分層。我覺得框架和應用程序一樣,也需要進行分層。傳統的應用程序我們分為表現層、邏輯層和數據訪問層,類似的對于很多框架也可以進行橫向的層次劃分。要分層的原因是我們的框架要處理的問題是基于多層抽象的,就像如果沒有OSI七層模型,要讓一個HTTP應用去直接處理網絡信號是不合理的也是不利于重用的。舉一個例子,如果我們要寫一個基于Socket的RPC的框架,我們需要處理方法的代理以及序列化,以及序列化數據的傳輸,這完全是兩個層面的問題,前者偏向于應用層,后者偏向于網絡層,我們完全有理由把我們的框架分為兩個層面的項目(至少是兩個包),rpc.core和rpc.socket,前者不關心網絡實現來處理所有RPC的功能,后者不關心RPC來處理所有的Socket功能,在將來即使我們要淘汰我們的RPC的協議了,我們也可以重用rpc.socket項目,因為它和RPC的實現沒有任何關系,它關注的只是socket層面的東西。
  2. 橫切。剛才說的分層是橫向的分割,橫切是縱向的分割(橫切是跨多個模塊的意思,不是橫向來切的意思)。其實橫切關注點就是諸如日志、配置、緩存、AOP、IOC等通用的功能,對于這部分功能,我們不應該把他們和真正的業務邏輯混淆在一起。對于應用類項目是這樣,對于框架類項目也是這樣,如果某一部分的代碼量非常大,完全有理由為它分出一個單獨的包。對于RPC項目,我們可能就會把客戶端和服務端通訊的消息放在common包內,把配置的處理單獨放在config包內。
  3. 功能。也就是要實現一個框架主要解決的問題點,比如對于上面提到的RPC框架的core部分,可以想到的是我們主要解決是客戶端如何找到服務端,如何把進行方法調用以及把方法的調用信息傳給目標服務端,服務端如何接受到這樣的信息根據配置在本地實例化對象調用方法后把結果返回客戶端三大問題,那么我們可能會把項目分為routing、client、server等幾個包。

如果是一個RPC框架,大概是這樣的結構:

對于我們的Web MVC框架,舉例如下:

  1. 我們可以有一個mvc.core項目,細分如下的包:
    1. common:公共的一組件,下面的各模塊都會用到
    2. config:配置模塊,解決框架的配置問題
    3. startup:啟動模塊,解決框架和Servlet如何進行整合的問題
    4. plugin:插件模塊,插件機制的實現,提供IPlugin的抽象實現
    5. routing:路由模塊,解決請求路徑的解析問題,提供了IRoute的抽象實現和基本實現
    6. controller:控制器模塊,解決的是如何產生控制器
    7. model:視圖模型模塊,解決的是如何綁定方法的參數
    8. action:action模塊,解決的是如何調用方法以及方法返回的結果,提供了IActionResult的抽象實現和基本實現
    9. view:視圖模塊,解決的是各種視圖引擎和框架的適配
    10. filter:過濾器模塊,解決是執行Action,返回IActionResult前后的AOP功能,提供了IFilter的抽象實現以及基本實現
  2. 我們可以再創建一個mvc.extension項目,細分如下的包:
    1. filters:一些IFilter的實現
    2. results:一些IActionResult的實現
    3. routes:一些IRoute的實現
    4. plugins:一些IPlugin的實現

這里我們以IXXX來描述一個抽象,可以是接口也可以是抽象類,在具體實現的時候根據需求再來確定。

這種結構的劃分方式完全吻合上面說的切蛋糕方式,可以看到除了橫切部分和分層部分,作為一個Web MVC框架,它核心的組件就是routing、model、view、controller、action(當然,對于有些MVC框架它沒有route部分,route部分是交由Web框架實現的)。

如果我們在這個時候還無法確定框架的模塊劃分的話,問題也不大,我們可以在后續的搭建龍骨的步驟中隨著更多的類的建立,繼續理清和確定模塊的劃分。

經過了設計的步驟,我們應該心里對下面的問題有一個初步的規劃了:

  1. 我們的框架以什么形式來提供如何優雅的API?
  2. 我們的框架包含哪些模塊,模塊大概的作用是什么?

 

搭建龍骨

 

在經過了初步的設計之后,我們可以考慮為框架搭建一套龍骨,一套抽象的層次關系。也就是用抽象類、接口或空的類實現框架,可以通過編譯,讓框架撐起來,就像造房子搭建房子的鋼筋混凝土結構(添磚加瓦是后面的事情,我們先要有一個結構)。對于開發應用程序來說,其實沒有什么撐起來一說,因為應用程序中很多模塊都是并行的,它可能并沒有一個主結構,主流程,而對于框架來說,它往往是一個高度面向對象的,高度抽象的一套程序,搭建龍骨也就是搭建一套抽象層。這么說可能有點抽象,我們還是來想一下如果要做一個Web MVC框架,需要怎么為上面說的幾個核心模塊進行抽象(我們也來體會一下框架中一些類的命名,這里我們為了更清晰,為所有接口都命名為IXXX,這點不太符合Java的命名規范):

  1. routing MVC的入口是路由
    1. 每一個路由都是IRoute代表了不同的路由實現,它也提供一個getRouteResult()方法來返回RouteResult對象
    2. 我們實現一個框架自帶的DefaultRoute,使得路由支持配置,支持默認值,支持正則表達式,支持約束等等
    3. 我們需要有一個Routes類來管理所有的路由IRoute,提供一個findRoute()方法來返回RouteResult對象,自然我們這邊調用的就是IRoute的getRouteResult()方法,返回能匹配到的結果
    4. RouteResult對象就是匹配的路由信息,包含了路由解析后的所有數據
  2. controller 路由下來是控制器
    1. 我們有IControllerFactory來創建Controller,提供createController()方法來返回IController
    2. IController代表控制器,提供一個execute()方法來執行控制器
    3. 我們實現一個框架自帶的DefaultControllerFactory來以約定由于配置的方式根據約定規則以及路由數據RouteResult來找到IController并創建它
    4. 我們為IController提供一個抽象實現,AbstractController,要求所有MVC框架的使用者創建的控制器需要繼承AbstractController,在這個抽象實現中我們可以編寫一些便捷的API以便開發人員使用,比如view()方法、file()方法、redirect()方法、json()方法、js()方法等等
  3. action 找到了控制器后就是來找要執行的方法了
    1. 我們有IActionResult來代表Action返回的結果,提供一個execute()方法來執行這個結果
    2. 我們的框架需要實現一些自帶的IActionResult,比如ContentResult、ViewResult、FileResult、JsonResult、RedirectResult來對應AbstractController的一些便捷方法
    3. 再來定義一個IActionInvoker來執行Action,提供一個invokeAction()方法
    4. 我們需要實現一個DefaultActionInvoker以默認的方式進行方法的調用,也就是找到方法的一些IFilter按照一定的順序執行他們,最后使用反射進行方法的調用得到上面說的IActionResult并執行它的execute()方法
  4. filter 我們的框架很重要的一點就是便捷的過濾器
    1. 剛才提到了IFilter,代表的是一個過濾器,我們提供IActionFilter對方法的執行前后進行過濾,提供IResultFilter對IActionResult執行前后進行過濾
    2. 我們的IActionInvoker怎么找到需要執行的IFilter呢,我們需要定義一個IFilterProvider來提供過濾器,它提供一個getFilters()方法來提供所有的IFilter的實例
    3. 我們的框架可以實現一些自帶的IFilterProvider,比如AnnotationFilterProvider通過掃描Action或Controller上的注解來獲取需要執行的過濾器信息;比如我們還可以實現GlobalFilterProvider,開發人員可以直接通過配置或代碼方式告知框架應用于全局的IFilter
    4. 既然我們實現了多個IFilterProvider,我們自然需要有一個類來管理這些IFilterProvider,我們實現一個FilterProviders類并提供getFilters()方法(這和我們的Routes類來管理IRoute是類似的,命名統一)
  5. view 各種IActionResult中最特殊最復雜的就是ViewResult,我們需要有一個單獨的包來處理ViewResult的邏輯
    1. 我們需要有IViewEngine來代表一個模版引擎,提供一個getViewEngineResult()方法返回ViewEngineResult
    2. ViewEngineResult包含視圖引擎尋找視圖的結果信息,里面包含IView和尋找的一些路徑等
    3. IView自然代表的是一個視圖,提供render()方法(或者為了統一也可以叫做execute)來渲染視圖
    4. 我們的框架可以實現常見的一些模版引擎,比如FreemarkerViewEngine、VelocityViewEngine等,VelocityViewEngine返回的ViewEngineResult自然包含的是一個實現IView的VelocityView,不會返回其它引擎的IView
    5. 同樣的,我們是不是需要一個ViewEngines來管理所有的IViewEngine呢,同樣也是實現findViewEngine()方法
  6. common 這里可以放一些項目中各個模塊都要用到的一些東西
    1. 比如各種context,context代表的是執行某個任務需要的環境信息,這里我們可以定義HttpContext、ControllerContext、ActionContext和ViewContext,后者繼承前者,隨著MVC處理流程的進行,View執行時的上下文相比Action執行時的上下文信息肯定是多了視圖的信息,其它同理,之所以把這個信息放在common里面而不是放在各個模塊自己的包內是因為這樣更清晰,可以一目了然各種對象的執行上下文有一個立體的概念
    2. 比如各種helper或utility

接下去就不再詳細闡述model、plugin等模塊的內容了。

看到這里,我們來總結一下,我們的MVC框架在組織結構上有著高度的統一:

  • 如果xxx本身并無選擇策略,但xxx的創建過程也不是一個new這么簡單的,可以由xxxFactory類來提供一個xxx
  • 如果我們需要用到很多個yyy,那么我們會有各種yyyProvider(通過getyyy()方法)來提供這些yyy,并且我們需要有一個yyyProviders來管理這些yyyProvider
  • 如果zzz的選擇是有策略性的,會按照需要選擇zzz1或zzzN,那么我們可能會有一個zzzs來管理這些zzz并且(通過findzzz()方法)來提供合適的zzz

同時我們框架的相關類的命名也是非常統一的,可以一眼看出這是實現、還是抽象類還是接口;是提供程序,是執行結果還是上下文。當然,在將來的代碼實現過程中很可能會把很多接口變為抽象類提供一些默認的實現,這并不會影響項目的主結構。我們會在模式篇對框架常用的一些高層設計模式做更多的介紹。

到了這里,我們的項目里已經有幾十個空的(抽象)類、接口了,其中也定義了各種方法可以把各個模塊串起來(各種find()方法和execute()方法),可以說整個項目的龍骨已經建立起來了,這種感覺很好,因為我們心里很有底,我們只需要在接下去的工作中做兩個事情:

  1. 實現各種DefaultXXX來走通主流程
  2. 實現各種IyyyProvider和Izzz接口來完善支線流程

 

走通主線流程

 

所謂走通主線流程,就是讓這個框架可以以一個HelloWorld形式跑起來,這就需要把幾個核心類的核心方法使用最簡單的方式進行實現,還是拿我們的MVC框架來舉例子:

  1. 從startup開始,可能需要實現ServletContextListener來動態注冊我們框架的入口Servlet,暫且起名為DispatcherServlet吧,在這個類中我們需要走一下主線流程
    1. 調用Routes.findRoute()獲得IRoute
    2. 調用IRoute.getRouteResult()來獲得RouteResult
    3. 使用拿到的RouteResult作為參數調用DefaultControllerFactory.createController()獲得IController(其實也是AbstractController)
    4. 調用IController.execute()    
  2. 在config中創建一個IConfig作為一種配置方式,我們實現一個DefaultConfig,把各種默認實現注冊到框架中去,也就是DefaultRoute、DefaultControllerFactory、DefaultActionInvoker,然后把各種IViewEngine加入ViewEngines
  3. 然后需要完成相關默認類的實現:
    1. 實現Routes.findRoute()
    2. 實現DefaultRoute.getRouteResult()
    3. 實現DefaultControllerFactory.createController()
    4. 實現AbstractController.execute()
    5. 實現DefaultActionInvoker.invokeAction()
    6. 實現ViewResult.execute()
    7. 實現ViewEngines.findViewEngine()
    8. 實現VelocityViewEngine.getViewEngineResult()
    9. 實現VelocityView.render()

在這一步,我們并不一定要去觸碰filter和model這部分的內容,我們的主線流程只是解析路由,獲得控制器,執行方法,找到視圖然后渲染視圖。過濾器和視圖模型的綁定屬于增強型的功能,屬于支線流程,不屬于主線流程。

雖然在這里我們說了一些MVC的實現,但本文的目的不在于教你實現一個MVC框架,所以不用深究每一個類的實現細節,這里想說的是,在前面的龍骨搭建完后,你會發現按照這個龍骨為它加一點肉上去實現主要的流程是順理成章的事情,毫無痛苦。在整個實現的過程中,你可以不斷完善common下的一些context,把方法的調用參數封裝到上下文對象中去,不但看起來清楚且符合開閉原則。到這里,我們應該可以跑起來在設計階段做的那個示例網站的HelloWorld功能了。

在這里還想說一點,有些人在實現框架的時候并沒有搭建龍骨的一步驟,直接以非OOP的方式實現了主線流程,這種方式有以下幾個缺點:

  1. 不容易做到SRP單一指責原則,你很容易把各種邏輯都集中寫在一起,比如大量的邏輯直接寫到了DispatcherServlet中,輔助一些Service或Helper,整個框架就肥瘦不勻,有些類特別龐大有些類特別小。
  2. 不容易做到OCP開閉原則,擴展起來不方便需要修改老的代碼,我們期望的擴展是實現新的類然后讓框架感知,而不是直接修改框架的某些代碼來增強功能。
  3. 很難實現DIP依賴倒置原則,即使你依賴的確實是IService但其實就沒意義,因為它只有一個實現,只是把他當作幫助類來用罷了。

 

實現各種支線流程

 

我們想一下,對于這個MVC框架有哪些沒有實現的支線流程?其實無需多思考,因為我們在搭建龍骨階段的設計已經給了我們明確的方向了,我們只需要把除了主線之外的那些龍骨上也填充一些實體即可,比如:

  1. 實現更多的IRoute,并注冊到Routes
  2. 實現更多的IViewEngine,并注冊到ViewEngines
  3. 實現必要的IFilterProvider以及FilterProviders,把IFilterProvider注冊到FilterProviders
  4. 增強DefaultActionInvoker.invokeAction()方法,在合適的時候調用這些IFilter
  5. 實現更多的IActionResult,并且為AbstractController實現更多的便捷方法來返回這些IActionResult
  6. ……實現更多model模塊的內容和plugin模塊的內容

實現了這一步后,你會發現整個框架飽滿起來了,每一個包中不再是僅有的那些接口和默認實現,而且會有一種OOP的爽快感,爽快感來源于幾個方面:

  1. 面對接口編程抽象和多態的放心安心的爽快感
  2. 為抽象類實現具體類享受到父類大量實現的滿足的爽快感
  3. 實現了大量的接口和抽象類后充實的爽快感

我們再來總結一下之前說的那些內容,實現一個框架的第一大步就是:

  1. 設計一套合理的接口
  2. 為框架進行模塊劃分
  3. 為框架搭建由抽象結構構成的骨架
  4. 在這個骨架的基礎上實現一個HelloWorld程序
  5. 為這個骨架的其它部分填充更多實現

經過這樣的一些步驟后可以發現這個框架是很穩固的,很平衡的,很易于擴展的。其實到這里很多人覺得框架已經完成了,有血有肉,其實個人覺得只能說開發工作實現了差不多30%,后文會繼續說,畢竟直接把這樣一個血肉之軀拿出去對外有點嚇人,我們需要為它進行很多包裝和完善。

 【原創】如何寫一個框架:步驟(下)


文章列表




Avast logo

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


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

    IT工程師數位筆記本

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