Structuring with SOLID — 文章與文集的結構決策標準
Structuring with SOLID(用 SOLID 原則做結構決策)
本 reference 為「文章 / 模組 / 系列的結構決策」情境:該不該拆篇、擴充點放哪裡、引用往哪個方向、讀者怎麼分流。
適用:單篇文章長出第二個責任時的拆分判斷;系列 / 文集成長時的擴充點設計;方法論層與案例層的依賴方向檢查;多讀者群的入口設計;拆分動作完成當下的交界檢查;對既有文集做結構 review。 不適用:文集的組織操作模板(三層結構、MOC 格式,見 SKILL.md 路由到 managing-article-collections);單篇內部的段落寫法(見 SKILL.md 路由到 writing-articles)。
自包含聲明:閱讀本文件不需要先讀其他 reference。SOLID 五原則的程式定義在文內就地給出、不預設讀者熟悉物件導向設計。
何時參閱本文件
| 觸發情境 | 對應章節 |
|---|---|
| 要往既有文件加一節、或一份文件被不同主題反覆修改 | S 單一責任 |
| 每次加新內容都要回頭改多篇既有文章 | O 開放封閉 |
| description / 索引條目跟內文對不上 | L 契約履行 |
| 文章要服務多種讀者、內容交錯 | I 讀者分流 |
| 方法論文件跟案例文件互相引用、方向混亂 | D 依賴方向 |
| 剛把一份文件拆成多檔、或讀者回報跳躍割裂 | 交界檢查 |
| 對整個文集做一輪結構 review | 結構檢查清單 |
為什麼結構決策需要獨立指引
文章體系跟程式體系承受同一組結構壓力:內容會變動、體系會擴充、讀者多樣、單位之間有依賴。單篇寫作的品質規則(原子化、意圖顯性、可查詢性)管的是單位內部;結構壓力發生在單位之間——哪篇承擔什麼、誰引用誰、新內容往哪放。SOLID 原則管理的正是「多單位系統的責任分配與依賴方向」、跟單位是函式還是文章無關、所以可以映射。
分工定位:原子化原則(定義在 SKILL.md 的核心原則一)(一卡一概念)管原子層、本文件的 SOLID 映射管組合層。S 是兩層的接縫——「一卡一概念」跟「一篇一責任」是同一條原則在不同粒度的形式。
概念對應表:
| 程式概念 | 文章對應物 | 共同承受的壓力 |
|---|---|---|
| 模組 | 一篇文章 / 一張卡 | 都是一個責任單位 |
| 介面 | title、description、index 條目 | 都是使用者看到的承諾 |
| 呼叫者 | 讀者 | 都從承諾進入、依賴行為符合承諾 |
| 依賴 | 引用、前置知識 | 都有方向、都會傳遞變動 |
| 重構 | 改版、拆篇、搬段落 | 都要在不破壞使用者的前提下進行 |
S 單一責任:一篇文章一個變動理由
程式定義:一個模組只有一個變動理由。寫作映射:一個寫作單位只承擔一個知識責任、只因一種讀者需求而修改。
判別測試:
- 變動理由測試——這篇文章會因為什麼事件需要更新?獨立事件超過一種、就是拆分候選。
- 修改紀錄測試——已經有版本歷史的文件,改用實際的紀錄:
git log --follow --no-merges --format='%h %s' -- <檔案路徑>列出修改,依 commit 標題歸類,類數就是職責數。變動理由測試是作者的預測,而作者對自己剛加的那一節總是預測得出它屬於這裡;紀錄記的是已經發生過的修改。 - 刪除測試——刪掉某段後核心論證仍完整、該段承擔的就是另一個責任、該路由出去。
- 分類範圍測試——這一段換到同一個內容集合的另一個分類底下會不會更合適。文章的範圍是標題與所屬分類兩個限定的交集,標題常常不寫分類已經限定的條件(放在某個語言分類裡的文章不寫那個語言),所以只比標題時,一段講得比分類更泛的內容會被判成在範圍內。
長度不是這條原則的觸發條件:600 行的參數表只有一種職責,80 行的 README 可以有四種。長度只決定一種職責之內要不要分節或分層;拆不拆、沿哪條軸拆看職責。集中登記型文件(變更紀錄、索引、術語表)被許多主題修改是它的設計目的,依職責拆它不降低衝突,改用每項一個片段檔。
加段落之前先問一次:這一節的修改理由,在不在這份文件既有的修改理由裡(從修改紀錄讀,不從標題猜)。不在的話先找承接它的文件,找不到才開新檔、原檔留一句連結。累積式的規則檔(專案規範、agent 指令、skill 檔)最需要這一問。判定與實測見 change-history-counts-a-documents-responsibilities 與 section-scope-is-bounded-by-the-category-too。
錯:一篇個體案例分析內嵌了完整的跨個體比較(多維比較表 + 逐維差距拆解)。兩個責任互相擠壓——案例深度被比較表佔空間、比較內容被鎖在單一個體的敘事裡、加入第三個比較對象時不知道改哪篇。
對:案例篇專注個體自身的分析、比較獨立成篇;案例篇留一句基準引用(「同業 X 的指標是 N、差距 M」)建立相對位置、完整比較用連結路由。拆分後案例篇騰出空間深化自身分析、比較篇可獨立擴充新對象。
邊界:一兩句的基準引用是案例分析的一部分、不觸發拆分;完整的多維比較表才是第二責任的訊號。
O 開放封閉:對擴充開放、對重寫封閉
程式定義:對擴充開放、對修改封閉。寫作映射:預期會成長的結構、設計成「新增用追加的、既有內容維持穩定」。
擴充點設計是這條原則的可操作形式:比較表的比較軸、索引的表格結構、系列的骨架是穩定結構、成長發生在往裡面加行加篇。
錯:比較內容散落在多篇個體文章裡、每篇各自解釋別人的模式。加入新比較對象時要改動每一篇。
對:比較篇的比較軸(成本結構 / 集中度 / 策略 / 估值這類穩定維度)設計好之後、新對象加行即可、既有個體文章零改動。
邊界(最重要):O 管結構骨架、內容不在管轄範圍,而這條邊界對本文件的五條映射都成立、不只對 O(完整陳述見〈類比的邊界〉的模板化風險那一條)。把 O 推到內容層會變成「所有文章共用同一模板」——情境敘事、案例語言、失敗條件依各自情境重寫、守 O 的只有表格軸、索引結構、系列骨架這些擴充點。拿結構原則去否定合理的情境差異(「這篇的章節跟那篇長得不一樣」),那個否定本身就是誤用的訊號。
L 契約履行:同類文章對讀者履行同一契約
程式定義(Liskov 替換):子型別可以替換父型別而不破壞呼叫者的預期。寫作映射在讀者預期層、有兩個檢查面:
- 介面對實作:title、description、index hook 是介面、內文是實作。讀者依承諾進入、內文必須履行——description 承諾「批判性分析」、內文就要有批判檢驗。
- 同類對同類:同一個索引分類下的文章對讀者履行同型的功能契約——讀者讀過一篇、對下一篇的功能預期(有定位、有判讀、有可遷移框架)應該成立。
錯:正文改版後建立了新的核心概念、title 跟索引條目保留舊 frame——介面跟實作脫鉤、讀者從索引進來拿到的內容跟承諾不符。另一種形態:description 摘要把本體的「條件允許(可延後但要記錄)」壓成「絕對禁止」、讀者依摘要行動就偏離本體。
對:每輪內容修訂把 title / description / 索引條目列入同一次 review 的 surface 清單;摘要壓縮時可以丟細節、模態詞(可以 / 必須 / 禁止 / 建議)跟本體保持一致。
邊界:契約是功能層承諾、模板是結構層複製——L 要求前者。這也是五個映射裡最依賴類比延伸的一條:程式的替換性有型別系統驗證、讀者預期沒有、可操作的形式就是「介面承諾 vs 實作履行」的比對。
I 讀者分流:讀者不被迫消費無關內容
程式定義(介面隔離):client 不該被迫依賴用不到的方法。寫作映射:不同讀者群透過不同入口進入體系、每條入口只包含該讀者需要的內容。
實作在三個層次:
| 層次 | 手段 |
|---|---|
| 模組層 | 讀者路線表——同一批文章、對不同讀者群給不同的讀取序列 |
| 文章層 | 視角分段——多視角並存時各自成段、讀者可跳過非自身視角 |
| 術語層 | 術語卡外移——解釋放獨立卡片、需要的讀者點進去、熟悉的不被打斷 |
錯:一篇文章的目標讀者要用「和」連接(「給投資人和從業者和工程師」)、三種讀者需要的內容交錯出現、任何一種讀者都要掃過大量無關段落。
對:入口層分流(模組索引給三條讀者路線)、或文章內視角分段(「投資人視角」「從業者視角」各自成段)。
文章的 I 比程式更嚴苛:程式的 caller 被迫依賴多餘介面、代價是編譯負擔;讀者被迫閱讀無關內容、代價是棄讀。
D 依賴方向:具體依賴抽象、抽象不依賴具體
程式定義(依賴反轉):高層模組跟低層模組都依賴抽象。寫作映射:具體層(案例文章)依賴抽象層(方法論、術語卡);抽象層引用具體只作為示例、不作為前提。
依賴方向決定變動傳遞方向:案例更新(資料換季)不迫使方法論改寫、方法論修正提示所有案例重檢——這個不對稱正是體系要的:抽象層穩定、具體層流動。
錯:方法論文件的判斷標準用某個案例的特殊條件寫成(過度擬合)、讀者必須先讀過該案例才看得懂方法論;修改該案例時要回頭改方法論。
對:方法論用自包含的通用示例說明判斷標準、案例連結降級為選讀;案例文章引用方法論做分析框架、方法論不需要知道任何一篇案例的存在。層級規則顯式寫進體系規範(例如「能力層不反向依賴實作層」)、讓依賴方向是宣告過的 fact、不是每篇各自的判斷。
拆完之後的交界檢查
前面五條決定該不該拆、沿哪條軸拆。拆完之後還有一組檢查,對象是拆分自己造出來的東西:檔與檔之間的交界。 連續閱讀時讀者一路往下走,拆開之後他要在某處離開一份檔案、進入另一份,或者根本不知道還有另一份可以進。缺陷落在那些位置上,而每一份檔案單獨讀都是對的,所以逐檔的自審與逐檔的冷讀對這一類恆真。
逐項執行:
- 兄弟檔的互指要對稱:每一份檔頭列出其餘各份。做法是逐檔取檔頭的同目錄引用、排成矩陣、看有沒有缺格——一次實測裡七份有六份列滿、剩下那一份漏了一個,讀那一份的人因此不知道第三條路徑存在。
- 每一份要有位置座標,而座標寫上一步與下一步、不寫總數:總數會隨每次增刪過期,上一步與下一步不會。這是集合命名不內嵌數量那條原則在檔案層的形態,見 name-collections-by-role-not-count。
- 層級不得混疊:子檔內的步驟編號與全程的步驟編號不可以指同一件事而各編一次號。檢查是把兩套並排。
- 用詞的定義與用詞的使用不得分屬兩檔:定義住在讀者到得了的地方——用得最多的那一份就地給,或首次用到的那一點給。
- 內嵌於散文的計數在拆分之後會過期(「後四列」「共用兩節」),改用不帶計數的分組名。判別問句是這個數字由什麼決定:由會被改動的成員數決定就不安全。
- 受管詞的指路寫在首次用點、不寫在檔頭:受管詞指本檔會用到、而定義住在別處的那些詞(方法論名稱、工具名、規格檔名)。見〈受管詞的指路要在首次用點〉。
- 指向原檔的外部引用要找出來改:其他文件、腳本、agent 指令與書籤引用的是原檔路徑與原段標錨點。段落搬走之後,連到原錨點的連結仍然打得開原檔、只是停在頁面頂端,而 skill 目錄、其他 repo 與 agent 指令裡的引用多半不在連結檢查的範圍內。用
grep -rl '<原檔路徑>'與grep -rn '<原檔名>#'列出來逐一改。 - 原檔保留成導覽表、不刪除:原檔的路徑已經被引用,刪掉它等於讓上一條的失效全部發生。每一列寫一種讀者需求、對應的新文件與那份文件涵蓋什麼。
外部引用與原檔導覽表這兩項的對象是新檔以外的引用者,其餘各項的對象是新檔彼此之間,見 change-history-counts-a-documents-responsibilities。
切法會決定要不要處理層級混疊
依角色切(每個子檔承擔一種角色或主題)會讓每一份從自己的第一步開始編號,而子檔裡的後段步驟經常就是全程的中段步驟——在子檔裡是「步驟六、七、八」,在全程裡是「步驟三、四、五」。兩套編號各自在自己的檔案裡連續、合理、沒有跳號,所以逐檔讀看不到它;讀者那一側的後果是走完「步驟八」以為結束,而他實際上跨了三個層級。
依步驟切不產生這個形態:每一份就是全程的一段,編號只有一套,交棒點自己浮現。一次實測改切法之後,三條路徑各自收斂為五步以內,冷讀對「走完之後要做什麼」的回答從三份全體答不出來轉為三份都答得出來。
所以流程型內容的拆分軸優先選步驟、不選角色。 角色軸適合查表型內容(每一份是一類對象的規格),那裡沒有全程編號可以撞。
受管詞的指路要在首次用點
在每一份的檔頭放一句總括式的指路(本檔出現的方法論、範本、工具與規格檔一律查某份地址檔)讀起來已經盡到責任,而它對讀者的效果量得出來是零:一次實測八份檔頭都有那句話,三份冷讀仍把其中兩個詞判為「沒說去哪查」;第三輪只把一個詞改成首次用到的那一點就地具名並帶指路(其餘一律不動),該詞的判定降為零份。
讀者在正文遇到那個詞時不會回頭查檔頭。 這與串連要交代目的地是兩個獨立的要求——一條寫在檔頭的指路可以把目的地交代得很完整而讀者仍然拿不到它,見 cross-links-eat-the-article-they-live-in。
外移一段之後,留在原處的殘餘要自足
把一段下沉到後面的子檔之後會出現一個擺盪,它是拆分固有的、不是失手。原處會被讀成掏空(實測三份分別寫「被掏空」「摸不著頭腦」「本節內容不夠自足」);補回內容之後掏空感消失,而閱讀負擔的回報立刻移到補回的那幾句上。
兩次改寫之間的差別是份量:補回三項後果被讀成堆砌,補回一個具體實例就夠。判斷標準是留在原處的殘餘要能自足回答「為什麼這一步值得慢慢做」,而自足的成本是一個具體實例、不是一串後果。
完整判定、六種形態的症狀對照與偵測方式見 split-defects-land-between-the-files。
結構檢查清單
| 情境訊號 | 原則 | 行動 |
|---|---|---|
| 一篇文章有兩種獨立的更新理由 | S | 依變動理由拆篇、各自引用 |
| 修改紀錄裡的 commit 標題講的是幾件互不相干的事 | S | 依主題歸類、每類一份候選文件;集中登記型文件改片段檔 |
| 一段在標題範圍內、換到另一個分類卻更合適 | S | 移到承接它的分類、原處留一句連結 |
| 完整的跨個體比較表出現在單一個體的文章裡 | S | 比較獨立成篇、個體篇留一句基準引用 |
| 每次加新內容都要改動多篇既有文章 | O | 找出擴充點(表格軸 / 索引結構)、重設計成加行加篇 |
| description / index hook 跟內文的功能不一致 | L | 對齊介面與實作——改承諾或改內文、二選一 |
| 同分類文章功能參差、讀者預期無法遷移 | L | 對齊功能契約、結構仍依情境自由 |
| 目標讀者要用「和」連接、內容交錯無法跳讀 | I | 拆讀者路線、或文章內視角分段 |
| 讀方法論要先讀某案例才懂 | D | 方法論改自包含通用示例、案例降級為選讀連結 |
| 修案例要回頭改方法論 | D | 檢查方法論是否過度擬合該案例、特殊條件還原成通用判斷標準 |
| 讀者回報不知道還有某一份檔案 | 交界 | 逐檔取檔頭的同目錄引用做成矩陣、補上缺格 |
| 子檔與入口檔各有一套步驟編號、沒有一處寫出對應 | 交界 | 改依步驟切,或就地寫出兩套編號的對應 |
| 檔頭有一句總括式指路、涵蓋多個受管詞 | 交界 | 指路移到每個詞首次用到的那一點 |
| 拆完之後原檔被刪、或外部引用還指向原段標 | 交界 | 原檔改成導覽表;grep 原路徑與原檔錨點逐一改 |
檢查時機:新文章動筆前(決定責任邊界跟依賴方向)、體系出現結構張力時(加新內容越來越貴是結構 review 的訊號)、對文集做多輪 review 時(結構軸作為獨立的檢查 frame、跟字句層 / 語意層的 frame 分輪執行)。交界那一組的時機不同——它在拆分動作完成的當下跑,而且受測單位是整組檔案、不是其中任何一份。
類比的邊界(誠實聲明)
- 執行機制:程式違反 SOLID 由編譯器 / 測試暴露、文章違反由 review 暴露——本清單要掛進 review 流程才有效力、放著不會自己執行。
- L 最弱:替換性在程式有型別系統驗證、在文章只有讀者預期——操作化成介面 / 實作比對就是它的極限。
- 模板化風險:O 跟 L 被過度應用會收斂成「統一模板」。判別線:SOLID 管責任分配、依賴方向、擴充點(結構);敘事、案例語言、失敗條件描述(內容)依情境重寫、不受本清單管轄。
Version: 1.2.0 — S 單一責任:觸發條件從「越寫越長」改成「要往既有文件加一節、或被不同主題反覆修改」;判別測試加修改紀錄測試(git log 依主題歸類)與分類範圍測試;寫明長度不是觸發條件、集中登記型文件例外、加段落之前的檢查。交界檢查加兩條新檔以外的項目(外部引用、原檔保留成導覽表),「六條」改成不帶計數;清單加三列
Version: 1.1.1 — O 原則的邊界段補上轄域:那條邊界對五條映射都成立、不只對 O(原文只寫 O 而〈類比的邊界〉寫的是全體,兩處轄域不同而讀第一處的人拿不到第二處),末句主詞由「結構原則被拿來否定」改為具名的動作。一份冷讀在初測與複驗都列這一處,歸因為文章自己寫的轄域落差、不是讀者誤讀
Version: 1.1.0 — 新增〈拆完之後的交界檢查〉:六條逐項檢查(互指對稱、位置座標不寫總數、層級不混疊、定義與使用同檔、內嵌計數改分組名、指路在首次用點)、切法決定層級混疊會不會發生(流程型內容優先依步驟切)、受管詞指路的單變項實測(檔頭總括三份判沒說、改首次用點降為零份)、外移之後殘餘自足的份量標準(一個具體實例而非一串後果);結構檢查清單加三列交界訊號、檢查時機補交界組的時點與受測單位
Version: 1.0.0 — 初版:SOLID 五原則的寫作映射(結構同構表、各原則的判別測試 / 錯對範例 / 邊界、結構檢查清單、類比邊界三聲明);從一次「個體案例 vs 跨個體比較」的實際拆分經驗抽出組合層方法、與原子化原則分層分工
走完之後
結構檢查清單逐行對過、命中的訊號照「行動」欄改完之後,這一份的責任就結束了。往下接兩個方向:改動落到單篇內部的段落寫法時回 SKILL.md 路由到 writing-articles.md;改動落到整個文集的組織操作(三層結構、MOC 格式、素材庫比例)時回 SKILL.md 路由到 managing-article-collections.md。拆分改變了檔案的邊界,所以改完要重跑一次交界檢查——新的切法會產生新的交界。