Community程式設計與開發github.com

ShortArrow/library-design

設計函式庫——一個已發布的套件,或從有第二個消費者開始的應用程式內部模組。實作可以永遠編輯,而公開 API、依賴集、效能特性和所有可觀察行為在首次被外部使用時凍結(Hyrum 法則)。對呼叫者的五項職責:不奪取控制權、不增加隱藏成本(零成本抽象為基準)、不持有隱藏狀態、不隱藏失敗、不阻擋逃脫路徑。適用於設計或審查公開 API、將共用程式碼提取為模組、被要求「讓這個可重複使用」、簽名相容的變更改變了可觀察行為,以及應用程式圍繞著框架的類型和生命週期塑造時。

library-design 是什麼?

這個技能定義了一個嚴格的函式庫設計合約,以應對 Hyrum 法則:一旦有足夠多的呼叫者,每一個可觀察的行為都將變得至關重要。它闡明了 API 表面、格式和效能特性在首次外部使用後即「凍結」的邊界,並且將公開 API 的宣告與 SemVer 的規則(MAJOR/MINOR/PATCH)結合起來。該技能列出了五項核心職責:不奪取控制權(分離機制與策略)、不增加隱藏成本(以零成本抽象為基準)、不持有隱藏狀態(狀態存在於由呼叫者建立的實例中)、不隱藏失敗(透過型別對錯誤進行建模),以及不阻擋逃脫路徑(提供高階與低階 API,並注入可觀察性)。它與「乾淨架構」的原則相對應,強調原始型別與可替換性,以保持呼叫者的選擇權。當模組只有一個消費者時(SemVer 0.y.z 階段),該技能建議僅保留低成本的原則,並在出現第二個消費者時再增加靈活性。支援平台不限;此為設計理念,可直接應用於任何函式庫的程式碼庫或設計審查中。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ShortArrow/skills/tree/HEAD/skills/library-design

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

函式庫合約

一個應用程式由使用者評斷; 一個函式庫由程式設計師評斷, 而且是處於一個特定的時刻——當他們想要作者未曾想像到的東西時。 一個好的函式庫,並非在其預期用途中方便使用。 而是在非預期用途中,不會被拋棄。

以下每一條規則都源自一個不對稱性。 在函式庫內部,你可以永遠更改任何東西。 在外部,呼叫者所能觀察到的任何事情,在有人第一次依賴它的那一刻就凍結了——而且當有足夠多的呼叫者時, 有人會依賴全部。

什麼凍結,以及何時凍結

Hyrum 法則是運作條件,而非奇聞軼事: 只要有足夠的使用者, 每一個可觀察的行為都會成為關鍵——例外訊息、 列舉順序、日誌輸出、時序, 甚至是你文件化為未定義的行為。 「合約裡從未包含這個」並不能讓那些已經據此發佈的呼叫者不受影響。

因此,凍結線位於可觀察性,而非 public 關鍵字, 並產生三個後果:

  • 表面積從小開始。 一個符號,一旦公開, 就是一個你可以拓寬但永遠無法悄然撤回的承諾。 「暫時公開」是毫無益處的負債;按需暴露。
  • 格式比程式碼長壽。 一種檔案格式、一個網路協定、 一個持久化結構,在圍繞它的函式庫每次重寫後依然存在。 將內部結構體序列化為原樣,會將已凍結的層次與可編輯的層次焊接在一起。
  • 應用程式內部模組在第二個消費者處跨越這條線, 而非在打包時。 當兩個呼叫者存在時, 其 API 具有與已發佈套件相同的凍結經濟學——只是爆炸半徑較小。

語義化版本控制是同一條線的公開拼寫, 並且從拒絕在沒有一個的情況下運作開始: 「使用語義化版本控制的軟體 MUST 宣告一個公開 API」(SemVer 2.0.0,第 1 條)。 一旦宣告,增量是決定而非感覺——對該 API 的非向後相容變更 MUST 提升 MAJOR (第 8 條), 新的向後相容功能 MUST 提升 MINOR (第 7 條), 向後相容的修復 MUST 提升 PATCH (第 6 條)。 Hyrum 法則使得宣告變得困難: 規範凍結了你所宣告的內容, 而呼叫者凍結了他們所觀察到的內容。

規範也為尚未跨越界線的模組提供了一個名稱。 「主要版本零 (0.y.z) 是為了初始開發。任何東西 MAY 隨時變更」(第 4 條)。 這是對於只有一個消費者的模組的誠實標籤, 而當它獲得第二個消費者的那一天,就是應該發布 1.0.0 的日子, 而不是它被打包的日子。

不奪取控制權

將機制與策略分離。 執行 HTTP 請求是機制; 用指數退避重試三次是策略, 而將策略內建於核心會將函式庫限制在與之共享策略的呼叫者。 提供日誌記錄、執行緒、重試、超時、 快取、序列化的便利預設值——並讓每一個都可替換。

應用程式的角色是單一的,而且不是你的。 退出程序、寫入 stdout、攔截 SIGINT、 安裝頂層例外處理器、調整執行緒池大小: 一個進程中恰好只能有一個元件執行這些操作, 而執行這些操作的函式庫會與有權執行的元件衝突。 讀取進程的輸入屬於同一個角色: 環境變數、 配置檔案和命令列參數在應用程式根目錄讀取, 應用程式根目錄將每個值作為參數或選項物件傳遞給函式庫。 然後呼叫者選擇值的來源, 並且測試可以傳入值,而不需要設定整個機器。

魔術——自動偵測、自動轉換、自動重連、 自動快取——只有在呼叫者能夠重構其行為原因時才可接受。 使用者無法除錯的便利性是奪取控制權,而非節省工作量。

不增加隱藏成本

事情發生的時機屬於呼叫者。 一個啟動執行緒的導入、一個開啟連線的建構子、 一個讀取環境變數的靜態初始化器——每個都將實際工作移動到一個呼叫者從未選擇且無法回溯的時刻。

一個依賴是你 API 的一部分。 其執行時間、更新節奏、二進位相容性、授權條款、 其自身的初始化副作用都會轉移給每個呼叫者; 你在代表他們做出選擇。

效能特性是合約的一部分。 將 O(1) 查找變成 O(n)——或者加倍分配和複製次數——是穿上相容簽名外衣的破壞性變更, 而呼叫者會在上線環境中發現它。

在輸入比輸出更快到達的地方——串流、事件、日誌、 佇列——一個無界內部緩衝區是延時發生的記憶體洩漏。 反壓是設計的一部分,而非運維細節。

對於抽象本身的基準是零成本, 包含 Stroustrup 的兩個條款。 呼叫者未使用的部分,他們不需要付費: 便利層不得因其記憶體分配、 其分派、或其初始化而增加原始層的成本。 而呼叫者使用的部分,他們無法合理用手寫程式碼做得更好: 一個被手寫程式碼擊敗的抽象會被繞過, 而被繞過的函式庫就是一個帶有版本號碼但被拋棄的函式庫。

不持有隱藏狀態

單例模式、靜態快取、全域事件循環、 程序級配置: 只要你的函式庫是進程中唯一的函式庫,每個都能運作, 但當第二個函式庫、一個外掛主機、 或一個並行測試套件共享它時就會發生衝突。 狀態存在於呼叫者建立、擁有、 且可以建立兩次的實例中。

不隱藏失敗

API 形狀宣告了可能發生什麼以及誰負責。 錯誤、取消、超時、部分成功、 不存在——以型別建模, 而不是透過「這種事通常不會發生」和例外來處理。 同樣的形狀承載了註解無法強制執行的所有權答案: 誰關閉串流、緩衝區是否可以重用、 回呼在哪個執行緒上送達。

保留原因。 將失敗簡化為 false 或 null 會剝奪上層決定所需的確切資訊; 自己記錄日誌然後繼續同樣會丟失該資訊,並奪走呼叫者的日誌記錄策略。

品質在異常路徑上分離。 在快樂路徑上,函式庫是可以互換的;超時、重連、 串流中斷、部分讀取、 損壞資料和重試才是差異所在, 因此首先要為它們的組合進行設計。

取消的改造很困難——一個取消權杖會穿透它所觸及的每個簽名。 I/O 和長時間運算從第一個版本開始就接受它。

不阻擋逃脫路徑

建立兩層架構。 一個高階 API,用一行程式碼服務常見情況, 建立在公開原始型別之上——ReadAsync() 基於 ReadAsync(Memory<byte>)——以便需求超出頂層的呼叫者可以向下遷移,而不是離開。 只有高層的函式庫在遇到第一個未預期的需求時就會被拋棄。

可觀測性是注入的,而非擁有的:日誌記錄器、指標、 追蹤插入到呼叫者的堆疊中。 只有你的函式庫能讀取的診斷格式是被挾持的遙測資料。

一種稅,兩種貨幣

此技能與乾淨架構是以兩種貨幣拒絕同一件事。 零成本抽象拒絕運行時效能的稅; 依賴規則拒絕可變性的稅。 在兩者中,抽象是根據它讓依賴方放棄什麼來評判。

此處的職責其架構名稱
機制與策略分離用例與框架和驅動程式保持分離
可替換的預設值依賴反轉——穩定方選擇實作
便利層下的原始型別外層細節可替換
無全域狀態依賴明確,因此可以劃定邊界
依賴是你 API 的一部分依賴規則

呼叫者的健康形狀將你的函式庫置於底層: 一個在其策略和領域之下的可替換細節。 反轉的形狀——應用程式存在於框架內部, 拖曳其生命週期、類型、依賴注入、錯誤模型、 非同步模型——是函式庫扮演應用程式的角色, 以架構規模編寫。

一般形式:好的架構最小化的不是依賴的數量,而是每個依賴所奪走的自由。

定論

函式庫中的抽象存在是為了擴大呼叫者的選擇。 當它用效能、控制或資訊交換時, 它就不再是抽象,而是開始做決定。

何時不適用

一個具有一個消費者、一個儲存庫、 一個團隊的模組尚未跨越凍結線——以 SemVer 的術語來說,它處於 0.y.z 階段, 無論它是否帶有版本號——為它建立每個可替換的策略接縫是一種帶有維護帳單的投機行為。 從第一天起就保留那些零成本的原則——無全域狀態、 保留原因的失敗、 在呼叫者詢問之前不做任何工作——並在第二個消費者到來時添加接縫, 這時它們就不再是投機性的了。

來源

  • 語義化版本控制 2.0.0,semver.org: 第 1 條 (MUST 宣告公開 API)、 第 6-8 條 (PATCH、MINOR、MAJOR 增量)、第 4 條 (0.y.z)、 以及關於棄用的 FAQ 條目 (文件化,然後發布一個包含棄用的次版本)。 用詞已與頁面核對。
  • Hyrum 法則是一個觀察結果,而非標準,並被視為一個觀察結果引用。

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

ShortArrow/interface-design

Deciding how a screen looks and behaves, triggered by the moments that hand the decision to the user unworked: about to ask which layout, which colour, which placement, how to draw a diagram, how wide a pane, how to split a view; about to call a visual choice a matter of taste; about to treat "it looks fine" as having checked it; about to ship a control sized by eye; about to read an automated accessibility pass, or a Lighthouse score of 100, as a clean bill of health. Most of these have an answer in a published standard, a measurable threshold, or one screenshot — WCAG 2.2 fixes contrast, target size, reflow and focus; a design system fixes spacing and components; axe-core finds about 57% of WCAG issues, and Lighthouse's accessibility score is that same engine weighted, so both leave the rest to a person. What survives is a genuine question, asked with the evidence attached. Use before asking any question about appearance, layout or interaction, and before calling

ShortArrow/test-design

Deriving test cases, triggered by the moments that produce weak ones: about to write a test list that is the happy path restated, about to derive tests by reading the implementation they check, about to test the bound the specification names and not the other one, about to enumerate the roles the specification lists and skip the caller it forgot, about to vary one condition at a time from the happy path, about to draw a state or decision table without tying each row to a test, about to write an expected result with "or" in it, about to skip the error paths as unlikely, about to leave a path unimplemented and untested, or about to grade a suite as adequate by reading it. A test derived from the implementation passes by construction; coverage counts classes, not tests. Use when writing a test plan or list, when a use case is the specification in hand, when adding tests to existing code, when reviewing a suite for gaps, and when a suite is all green and proves little.

相關技能