走到今天,前面幾天研究過的東西已經很多。
我們從最底層開始:
TCP
↓
Socket
↓
NetworkStream
↓
HTTP Request
接著慢慢加入:
HttpRequest
Routing
Headers
Query
Body
JSON
Reflection
Parameter Binding
Handler
Result Conversion
HttpResponse
Middleware
單獨看,每一個概念都開始有一點理解了。
但真正把它們全部攤開後,我突然發現一個新的問題。
這些東西到底應該放在哪裡?
如果最後全部還是塞在 Program.cs:
讀 Request
解析 Request
找 Route
Binding
執行 Handler
處理 Result
組 Response
跑 Middleware
那即使每一個功能都做出來了,整個程式還是會很亂。
所以今天想理解的問題是:
一個 Framework 到底該怎麼把這些核心元件組織起來?
💭 我原本以為……
一開始做這個 Mini Web Framework 時,幾乎所有程式都可以放在 Program.cs。
因為那時候只有:
TcpListener
AcceptTcpClient()
NetworkStream
Read()
幾十行程式就能理解整個流程。
但隨著功能越來越多,Program.cs 開始同時知道太多事情。
它知道 TCP。
它知道 HTTP 格式。
它知道 Request Line 怎麼切。
它知道 Routing。
它知道 Response 怎麼組。
如果再把 Headers、Query、Body、Reflection、Binding、Middleware 全部加進去,Program.cs 很快就會變成一個什麼都知道的巨大檔案。
功能可能可以執行。
但它已經不像 Framework。
反而更像:
所有功能全部黏在一起的一支大型程式。
所以 Framework 除了「有什麼功能」之外,好像還有另一個很重要的問題:
誰應該負責什麼?
為什麼一定要拆?
最直接的理由是:
複雜度會越來越高。
假設今天只有一個功能:
讀取 Request
那放在 Program.cs 完全沒問題。
但如果同時有:
讀取 TCP
Parse HTTP
Route Matching
Parameter Binding
JSON Serialization
Middleware
Error Handling
Response Sending
全部混在一起後,只要其中一個地方要修改,就很容易影響其他功能。
所以拆分並不是單純為了:
檔案比較多,看起來比較專業。
真正的目的是:
讓每一個部分只負責自己的事情。
這其實和前幾天一直看到的 Responsibility 有關。
HttpRequest 應該負責什麼?
HttpRequest 不應該負責:
找 Route
執行 Handler
送 Response
它比較適合負責:
描述這次 Request。
例如:
Method
Path
Version
Headers
Query
Body
甚至可以包含 Request Parsing 的相關邏輯。
所以:
Raw HTTP
↓
HttpRequest
↓
提供 Request 資料
它的責任就很清楚。
HttpResponse 又負責什麼?
Day15 已經建立過 HttpResponse。
它適合保存:
StatusCode
ContentType
Body
Headers
以及最後:
Send()
所以:
Handler Result
↓
HttpResponse
↓
真正送回 Client
這樣 Response 相關細節不需要散落到其他元件。
Router 應該負責什麼?
Router 的核心問題很單純:
這個 Request 應該交給誰?
例如:
GET /hello
↓
Hello Handler
GET /products
↓
Products Handler
所以 Router 不應該同時:
解析 JSON
寫 NetworkStream
處理 Logging
它只要專心:
Route Registration
Route Matching
找到 Handler
即可。
這就是把 Routing 從整個 Server 流程中獨立出來。
Parameter Binder 又放哪裡?
前面 Day22 已經知道,Parameter Binding 的工作是:
查看 Handler Parameter
↓
決定資料來源
↓
從 Request 取得資料
↓
Type Conversion
↓
建立 Arguments
所以這件事情也可以獨立成:
ParameterBinder
概念上:
Handler Metadata
+
HttpRequest
↓
ParameterBinder
↓
object[] arguments
然後 Handler Invoker 再拿這些 arguments 去執行 Handler。
這樣 Router 就不需要知道:
JSON 怎麼 Deserialize。
HttpRequest 也不需要知道:
Handler 需要幾個參數。
大家的責任開始真正分開。
Handler Invoker 是什麼?
這是目前可以想像再拆出來的一層。
Router 的工作:
找到 Handler。
Parameter Binder:
準備 Handler 需要的參數。
那最後誰真的執行 Handler?
可以有一個:
HandlerInvoker
負責:
找到 Method
↓
拿到 Arguments
↓
Invoke
↓
取得 Return Value
所以可以拆成:
Router
↓
找到 Handler
ParameterBinder
↓
準備 Arguments
HandlerInvoker
↓
執行 Handler
這樣每一層的問題都比較單純。
Result Converter 呢?
Day23 又知道:
Handler 執行完之後,可能回:
string
Object
HttpResponse
null
不同結果需要先統一處理。
所以可以再想像一個:
ResultConverter
它負責:
Handler Result
↓
判斷類型
↓
轉成 HttpResponse
例如:
string
↓
Text Response
Object
↓
JSON Response
HttpResponse
↓
直接使用
最後全部變成:
HttpResponse
這樣 Handler 和真正的 HTTP Response 就被隔開了。
Middleware 又放在哪裡?
Middleware 比較特別。
它不是一個單純的資料處理工具。
它控制的是:
整條 Request Pipeline。
例如:
Request
↓
Exception Middleware
↓
Logging Middleware
↓
Routing
↓
Binding
↓
Handler
↓
Result Conversion
↓
Response
所以 Framework 最後還需要一個地方:
把這些 Middleware 組成 Pipeline。
也就是:
Pipeline Builder
或某個 Application / Server 類別。
這開始有一點像我們平常看到的:
app.Use(...)
app.MapGet(...)
這類 API。
Program.cs 應該剩什麼?
這是今天我覺得最重要的一個問題。
如果 Framework 做得夠好,使用 Framework 的人理論上不應該每天都看到:
TcpListener
NetworkStream
Encoding
Split
Reflection
JsonSerializer
這些底層細節。
Program.cs 應該開始往:
描述「我要怎麼使用 Framework」
而不是:
描述「Framework 底層怎麼運作」
的方向前進。
例如概念上可以慢慢變成:
建立 App
註冊 Middleware
註冊 Route
Run
也就是:
var app = new MiniWebApplication();
app.Use(...);
app.MapGet("/hello", ...);
app.Run();
這種感覺。
真正的實作之後可以再決定。
但這個方向本身非常重要。
因為如果最後使用 Framework 還要自己寫:
AcceptTcpClient()
stream.Read()
Parse()
那我們其實只是建立一些 Helper Class。
還沒有真的做出一個方便使用的 Framework。
框架和 Library 有什麼不一樣?
做到這裡也開始碰到一個很有趣的差別。
Library 通常是:
我的程式
↓
主動呼叫 Library
例如:
JsonSerializer.Serialize(...)
是我決定什麼時候呼叫它。
Framework 則常常更像:
我先註冊:
Route
Handler
Middleware
接著 Framework 自己控制整個執行流程。
也就是:
Framework
↓
收到 Request
↓
決定何時呼叫我的 Handler
這種概念常常會被稱為:
Inversion of Control
也就是控制反轉。
以前:
我的程式是主角。
我想呼叫誰就呼叫誰。
Framework 的情況:
Framework 掌握主流程。
在適當時機回頭呼叫我提供的程式。
例如:
app.MapGet("/hello", HelloHandler)
我只是告訴 Framework:
如果遇到 /hello,請執行這個 Handler。
真正什麼時候執行,是 Framework 決定。
這就是 Framework 開始真正和普通工具函式不同的地方。
Mini Web Framework 最後的核心應該長什麼樣?
目前可以先把我們前面學過的元件整理成:
MiniWebFramework
Server
↓
負責 TCP Connection
HttpRequest
↓
描述 Request
Router
↓
找到 Handler
ParameterBinder
↓
準備 Handler Parameters
HandlerInvoker
↓
執行 Handler
ResultConverter
↓
將結果轉成 HttpResponse
HttpResponse
↓
回傳 Client
Middleware Pipeline
↓
控制整個 Request Flow
如果畫成完整流程:
Browser
↓
TCP Server
↓
HttpRequest
↓
Middleware
↓
Router
↓
ParameterBinder
↓
HandlerInvoker
↓
ResultConverter
↓
HttpResponse
↓
TCP
↓
Browser
到這裡,一個 Mini Web Framework 的主要骨架其實已經很明顯了。
是不是每一個都一定要做成 Class?
不一定。
這裡也要避免:
為了拆而拆。
我們現在是在學 Framework Architecture,所以把責任分開思考很重要。
但實際實作時,不代表:
每一個概念一定都要建立一個 Class。
有些很小的功能可以先:
Method
Delegate
Static Helper
甚至放在同一個 Class 裡。
重點不是檔案數量。
而是:
責任有沒有清楚。
例如 Mini Web Framework 第一版可能沒有專門的 HandlerInvoker Class。
也沒有完整的 ResultConverter 系統。
都沒有關係。
只要最終能清楚看到:
Request
Routing
Handler
Response
Pipeline
各自的角色,就已經達到這次專案的目的。
先做最小版本,而不是做 ASP.NET Core
這點對最後五天非常重要。
做到今天,會很容易開始想:
ASP.NET Core 有 Dependency Injection。
那我要不要做?
有完整 Middleware。
那我要不要做?
有 Endpoint Metadata。
要不要做?
有 Model Validation。
要不要做?
如果一直這樣加,30 天根本做不完。
所以最後成品的目標不是:
自己重做 ASP.NET Core。
而是:
做出一個足以證明自己真的理解 Web Framework 核心流程的 Mini Framework。
也就是:
可以啟動 Server
可以 Parse Request
可以 Routing
可以執行 Handler
可以產生 Response
最好可以有簡單 Middleware
官方怎麼說?
真正的 ASP.NET Core 本身就把不同 HTTP 功能拆成很多抽象。
例如:
HttpContext
HttpRequest
HttpResponse
Endpoint
Routing
Middleware
RequestDelegate
等等。
這些類型背後的實作當然比我們目前想像複雜非常多。
但它們反映了一個很重要的 Framework Architecture 想法:
不同責任由不同元件處理。
最後再透過 Request Pipeline 把它們組合起來。
所以我們 Mini Web Framework 的目的,不是複製官方架構。
而是透過最小化版本理解:
這些抽象為什麼會存在。
以前的我:
Framework 就是一大堆很厲害的功能。
現在的我:
Framework
↓
其實是很多責任不同的元件
↓
透過 Pipeline 組合
↓
最後提供簡單 API 給開發者使用
所以 Framework 的難點不只是:
功能能不能做。
還包括:
複雜度應該被放在哪裡?
使用者應該看到什麼?
底層細節又應該藏在哪裡?
這些其實也是 Framework Design 的一部分。
今天最大的發現是:
真正的 Framework 不只是把程式碼「拆成很多 Class」。
而是:
把複雜度藏到適合的地方。
例如:
開發者只想:
MapGet("/hello", handler)
Framework 底下卻可能處理:
TCP
HTTP Parsing
Routing
Reflection
Binding
Result Conversion
Serialization
Response
Middleware
但這些東西不應該全部暴露給 Framework 的使用者。
所以 Framework 的價值可以再重新理解成:
複雜度沒有消失。
只是 Framework 把它集中管理,再提供更容易使用的介面。
做到今天,我們終於可以先畫出最後成品的方向:
MiniWebFramework
├── Program.cs
├── HttpRequest.cs
├── HttpResponse.cs
├── Router.cs
├── Middleware
└── 其他必要元件
實際最後會有幾個檔案,可以在實作過程中再調整。
但最重要的是:
Program.cs 要開始變簡單。
理想方向是:
建立 Framework
註冊 Route
註冊 Middleware
啟動
而真正:
TCP 怎麼 Listen
Request 怎麼 Parse
Route 怎麼 Match
Response 怎麼送
應該逐步移到 Framework 內部。
今天暫時不實作
Day25 是正式進入最後成品前的整理日。
今天先把這 20 多天學過的核心概念重新排列好。
因為如果沒有先整理,就直接開始把所有程式碼塞進專案,很容易最後又變成一個巨大 Program.cs。
接下來五天的目標也不再是:
繼續增加更多新概念。
而是:
把我們已經理解過的東西真正接起來。
也就是從明天開始:
Day26~Day30
正式完成 Mini Web Framework。
結尾
從 Day1 開始,我一直在追問:
為什麼?
一開始是:
CPU 為什麼看不懂 C#?
Compiler 到底做了什麼?
Build 到底產生什麼?
後來一路走到:
Server 為什麼可以一直等?
Browser 到底傳什麼?
HTTP Request 長什麼樣?
Routing 為什麼可以找到 Handler?
Response 怎麼回到 Browser?
再走到:
Request Object
Response Object
Query
Body
JSON
Reflection
Parameter Binding
Middleware
做到今天,突然發現每一個 Why 最後都變成了 Framework 裡的一個小零件。
所以 Day25 對我來說不是再增加一個新功能。
而是第一次把前面的答案全部放回同一張圖裡。
Browser
↓
Request
↓
Framework
↓
Handler
↓
Framework
↓
Response
↓
Browser
真正的 Framework,就是把中間那些大量細節組織起來。
而接下來最後五天,就是把這張圖真正變成一個可以執行的程式。
理論已經差不多了。
從明天開始,不再只是問:
Framework 為什麼需要這些東西?
而是要開始面對:
我真的能不能把這些東西組成自己的 Framework?