這個工具是我自己寫來用的個人專案,一開始完全沒有測試——先求能動,能把片段兜起來輸出一份完整內容就算成功。測試是後來才慢慢補上的,補的過程裡踩過不少坑:解密結果對不上、合併出來的內容規格不一致、重跑程式時該省下的下載成本沒省下來。這個系列想整理的,不是「怎麼寫一個下載工具」,而是「一個滿是不可控依賴的個人專案,怎麼一步步變成一個敢放心修改的東西」。
第一部(Day 1-7):爬蟲層的抽象設計。 用 Template Method 把「共用流程」跟「各來源專屬的差異」分開,用 Factory 把「該用哪個實作」的判斷集中管理,處理過脆弱的字串解析、防禦性寫法,也誠實討論過抽象化本身的成本、什麼時候值得做。這一部收斂成一句話:爬蟲層的責任,是把不確定的外部資料轉換成確定的內部結構,不多不少。
第二部(Day 8-15):HLS 協定本身。 從索引清單的基本結構、巢狀的 Master/Media playlist、加密金鑰與 IV 的規格細節,到片段規格一致性的驗證。這一部示範的是「把一個看起來複雜的協定拆解成一連串可以逐步驗證的小問題」,也誠實揭露了對照規格後才發現、自己程式碼裡真實存在的一個缺口。
第三部(Day 16-22):下載引擎的工程細節。 併發模型的選擇跟一段混用兩種模型的技術債、局部重試與退避策略、進度呈現、合併前的驗證。這一部的核心是:錯誤處理不是加出來的防護網,是要跟主要邏輯一起設計出來的。
第四部(Day 23-30):測試策略。 這是整個系列真正想收斂到的地方——網路、檔案系統、加密,各自用不同的替身策略讓不可控依賴變成可測試的東西,但背後是同一個判斷邏輯:找出依賴對外承諾的穩定邊界,在那條邊界上做出精確吻合的替身。
老實說,這個工具最早的版本完全沒有測試。當時的心態很典型:「這只是我自己用的小工具,能動就好」。真正開始補測試,是踩了幾次「改一個地方,另一個地方悄悄壞掉,卻要等到某次執行結果不對才發現」的坑之後,才體會到:工具的規模跟「值不值得寫測試」沒有直接關係,真正的判斷依據是「它依賴的東西有多不可控」。個人專案不代表可以隨便,只是沒有人在旁邊逼你認真而已。
補測試的過程也不是一路順利——第一版的測試很多是 Day 27 講的那種假陽性測試,只驗證了「跑起來沒出錯」,直到養成故意改壞邏輯檢查測試會不會抓到的習慣,才慢慢把測試的品質提升上來。這個過程沒有捷徑,就是一次次踩坑、一次次回頭修測試本身。
貫穿全系列的原則——依賴替身策略、爬蟲抽象層設計、失敗處理要分層設計、fail fast——都跟程式語言無關,換成 Node.js、Java、Go 一樣適用。但具體的技術手法是 Python 生態的實現方式:pyfakefs 換到 Node.js 可以用 mock-fs,換到 Java 可以用 Jimfs;asyncio 換到其他語言會對應到各自的併發模型——Node.js 的 event loop 跟 asyncio 一樣是單執行緒、協作式排程,對應程度較高;Go 的 goroutine 則是由 runtime 排程、可搭配多執行緒真正平行執行的模型,底層機制跟 asyncio/event loop 不完全相同,只是同樣用來處理高並行 I/O,類比時值得留意這層差異。原則可以直接搬過去用,工具要在對應生態裡找到等效的替代品,但工具背後的機制不一定一模一樣。
如果你手上也有一個「能跑就好、沒人敢動」的個人工具,一個可行的第一步不是「把它全部重寫並補滿測試」——而是先問自己:這個工具依賴的東西裡,哪一個最不可控?先針對那一個依賴,練習一次找出它的穩定邊界,做出一個替身。走完一次之後,你會發現同樣的判斷邏輯可以套用到下一個依賴,逐步把整個工具變成一個敢放心修改的東西,而不需要一次到位。
Day 01 立的主題句,這裡再說一次:不可控依賴不代表不能測,關鍵是找到每種依賴各自的替身策略。 這句話原本是為了整理這個個人小工具寫的,但寫完這 30 天回頭看,它其實適用在任何一個「你以為只是規模小、不值得認真對待」的專案上——規模從來不是判斷該不該認真的依據,依賴的可控程度才是。