iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Software Development

程式設計沒有告訴你的事:30 天破解每一個 Why系列 第 25 篇

Framework 的核心零件都有了,最後到底該怎麼把它們組起來?

  • 分享至 

  • xImage
  •  

走到今天,前面幾天研究過的東西已經很多。

我們從最底層開始:

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?


上一篇
每個 Request 都要做 Logging 和驗證嗎?Middleware 到底是什麼?
下一篇
開始組裝 Mini Web Framework:第一步,先把巨大 Program.cs 拆掉
系列文
程式設計沒有告訴你的事:30 天破解每一個 Why 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言