Refactoring English:第 8 個月
原文由 Michael Lynch 于 發布,訂閱此部落格
一句話總結
也許不是每個人都想加入我的焦點團體。
亮點
- 我發現不是每個搶先預購書籍的讀者都想針對草稿給我回饋。
- 我釐清了時間都花在哪裡,並思考如何減少時間的浪費。
- 我花了 10 小時從零重寫了一個當初花了 300 小時才完成的網頁應用程式。
- 我持續用 Gleam 學習函式程式設計,但我可能有點作弊。
目標成績
每個月月初,我都會宣告當月想完成的目標。以下是這個月的達成狀況:
至少與 10 位先前沒聊過的讀者對談
- 結果:寄了七封信給讀者,收到三封回覆,進行了一次線上對談
- 成績:C
在實際坐下來計算之前,我以為回覆率要低得多,但其實不少讀者都有回覆。問題在於我主動聯繫的人還不夠多。
我可能在每封信上都花了太多時間,想寫出獨特、而且一看就知道不是 AI 寫的內容,結果往往不小心就花上一小時去讀對方的部落格,愈看愈深。
清空行銷點子的待辦清單
- 結果:完成了預定事項的約 70%
- 成績:C
大部分待辦事項都完成了,不過一年前錄的一場訪談到現在還沒發布,我希望能盡快把它處理完。
發布《Refactoring English》的新章節
- 結果:發布了〈少有人用卻有效的 Email 技巧〉
- 成績:A
我對這一章的成果很滿意,但也知道在社群媒體上分享會是一場賭注。結果是在 Lobsters 上獲得好評,但在 Hacker News 上毫無迴響。
《Refactoring English》數據指標
| 指標 | 2025 年 6 月 | 2025 年 7 月 | 變化 |
|---|---|---|---|
| 不重複訪客 | 6,574 | 8,061 | +1,487 (+23%) |
| 預購收入 | $597.24 | $800.04 | +$202.80 (+34%) |
| 顧問收入 | $242.45 | $0.00 | -$242.45 (-100%) |
| 贊助收入 | $48.25 | $48.25 | $0.00 (0%) |
| 總收入 | $887.94 | $848.29 | -$39.65 (-4%) |
網站造訪人數上升了,這是好現象,因為我這個月並沒有特別熱門的新文章。光靠現有的試閱內容就能維持穩定的流量,我覺得是個正面的訊號。
總收入略微下滑,但只是因為從有一份付費顧問案子變成零,沒有太大變化。我更開心的是看到預購收入比六月成長了 34%。
如果大家只是喜歡「買書的感覺」呢?
當我主動聯繫讀者、和他們進行視訊對談時,一再聽到同樣的回饋:「我還沒開始看。」
不管是幾天前才剛買的人,還是已經拿到書好幾個月的人,都這麼說。
我最大的擔心是,這本書是「維他命而非止痛藥」。大家會把寫作能力看成重要但不緊急的事。有人可能會為了即將到來的面試而去讀一本叫「如何搞定下一次程式面試」的書,但「提升寫作能力」這種事卻很容易被無限期延後。
當初我在還沒動筆前就開始預售,部分動機就是想驗證是否有足夠多人願意為這本書付費。結果確實有人買,而且持續有人下單,但我擔心這個訊號或許不如我想的那麼具有預測性。
如果我的書就是那種讓人買了之後,覺得自己好像已經在投資寫作能力、卻完全不用實際花時間去做的東西呢?會不會就像 Planet Fitness,那家據說靠著會員報名後從不上門而賺錢的知名連鎖健身房?

如果我寫的是書籍界的 Planet Fitness 呢?
照片由 Mike Mozart 拍攝,採用 CC-BY-2.0 授權
就算我真能成為書籍界的 Planet Fitness,那也無法長久。我的書要能財務上可行,靠的是看過書、覺得喜歡的人口耳相傳。我希望受歡迎的部落客會引用我的書,說它幫助了他們。當有開發者說想提升寫作能力時,我希望我的書就是大家第一個會推薦的選擇。
也許顧客根本不想加入我的焦點團體
我當初預售的另一個原因是,我預期搶先預購的讀者會特別熱衷於在寫作過程中提供回饋。
和同樣是顧客的朋友們聊過後,大多數人都說,他們預購是為了支持這個計畫,想等書完成後再來好好閱讀,但不一定想參與焦點團體或對草稿給意見。他們只想看完成版,這完全可以理解。
我應該繼續一個一個主動聯繫
我發現,當我一個一個聯繫顧客、安排線上對談時,還是有些讀者非常熱情、很樂意提供回饋。雖然只找到少數幾位,但他們給的意見都非常有幫助。
目前我找到最有效的方法,就是一個一個寫信給他們,所以我會繼續這麼做。
我的時間都花到哪去了?
每 年 或 so,我回顧上個月完成了什麼時,就會想:「咦,為什麼上個月只做了這麼一點?我其他時間到底在做什麼?」
七月就是這樣的月份。我照著正常的全職工時工作,但回頭一看,卻在想:「怎麼整個七月只完成了一個新章節?」
所以,讓我回頭檢視一下,當我沒有在處理每月目標時,時間都跑哪去了。
在章節上過度投入
幾個月前,我意識到自己花了太多時間在字斟句酌上,而不是在章節達到 80 到 90 分的完成度時就先交給讀者。我的解法是為每個章節設定時間上限,在預算內能寫多少就寫多少。
七月寫的那章 Email 技巧,我原本的預算是五小時,實際卻花了 17.5 小時。有一部分是故意的,因為我在設定目標後才發現,這一章很適合當作網站上的獨立試閱內容。要寫成試閱文章再整合回書中,本來就會花更久。
但我現在正在寫的這一章也一樣。我已經花了 6.5 小時,超過原本 6 小時的預算,而且大概還需要至少 3 小時才會覺得可以放心交給讀者。我覺得這既是我過度打磨的問題,也是我對這本書最重要、也是第一章的預算抓得太低的問題。
解法:為會公開作為試閱的章節多抓一些預算,並嚴格把寫作時間控制在設定的範圍內。
額外的部落格文章
我喜歡在學到新東西後馬上透過寫部落格文章記錄下來,但就算每週花 20 小時寫部落格,還是寫不完所有想寫的題目。
有些部落格文章會吸引到可能對我的書有興趣的讀者,有些則不會。我必須謹慎決定要在「純粹好玩」的文章上投入多少時間。
上個月我發布了一篇新的部落格文章,〈將 ZFS 儲存池從 RAIDZ1 遷移到 RAIDZ2〉。我知道它對書的推廣沒什麼幫助,但我以為只要花幾個小時就能寫完,而且是在解釋一件我覺得還沒有人好好說清楚的事。
實際上,那篇 ZFS 文章花了七小時才寫完,對於一篇「純粹好玩」的文章來說,投入得有點太多,尤其是在那個沒有更吸睛的《Refactoring English》章節可以分享的月份裡。
解法:對非書籍相關的部落格文章更挑剔一些,優先寫那些也能吸引到會看我書的讀者的題目。
糟糕的社群媒體習慣
每當我感到無聊,或是沒動力去認真思考困難的寫作或程式問題時,就會不自覺去刷社群媒體。我告訴自己只是快速看一下就回去工作,結果卻被一篇文章吸進去。如果我還留言了,就會一直忍不住回來檢查有沒有人回覆。
通常如果文章在 Hacker News 或 reddit 上引起很大迴響,我會空出一天來回覆留言。我的 ZFS 文章其實沒有引起太大迴響,但我還是花了一整天在回覆留言。
有一段時間我曾用一個叫 LeechBlockNG 的瀏覽器擴充套件來克制不良的社群媒體習慣,但它好像會漏記憶體、拖慢整個瀏覽器,所以我就把它停用了。現在我又重新試用,目前還沒發現漏記憶體的問題,或許這次能派上用場。
解法:再給生產力相關的瀏覽器擴充套件一次機會,為那些浪費時間的網站增加一點阻力。
從睡眠中斷中恢復
我的幼兒最近睡得很不好,意味著我和太太也都睡不好。
睡眠中斷本身可能還不是最大的問題。更嚴重的是,我會拿睡眠不好當作偷懶的藉口,例如跳過寫作時段或去刷社群媒體。我會想:「昨晚睡那麼差,今天不該還要那麼拚吧!」但實際上我還是能維持平常 80 到 90% 的效率,只是拿睡不好來合理化自己的鬆懈。
解法:不要再把睡眠中斷當成偷懶的藉口。
拖延付費的編輯工作
我最近把編輯工作也納入《Refactoring English》提供的服務之一。
我喜歡幫別人編輯部落格文章,但這件事非常耗費心力。編自己的文章已經很難了,幫別人改就更難。改自己的東西時,我可以憑感覺下手,不用解釋為什麼要這樣改;但當我幫其他部落客給編輯建議時,就必須清楚說明我在草稿中看到什麼弱點,以及為什麼我的建議會更好。
我發現自己會拖延編輯工作,就算明明一天內就能完成,也會拖上好幾天。而當我在拖延編輯工作時,也會連帶拖延自己的寫作,因為我想先把客戶的工作擺在自己之前。
解法:認清拖延編輯工作會吃掉大量時間,並盡早著手處理。
其他專案
用 10 小時以靜態網站產生器取代花了 300 小時的 Vue 應用程式
2019 年,我曾嘗試打造一個叫 What Got Done 的服務。這是一款讓團隊成員可以彼此分享每週工作摘要的應用程式。

What Got Done 是一個讓人們分享每週工作進度的網站。
我在 Google 任職時,公司內部有個叫 Snippets 的工具,功能就和 What Got Done 一樣。我很喜歡它,離開 Google 後就算一個人工作,也持續每週寫更新。
我始終沒能為 What Got Done 找到付費用戶,所以就把它開源,並在過去六年裡當作興趣專案來維護。
What Got Done 一開始是用 Vue、Firestore 和 App Engine 打造的,而我後來愈來愈討厭這些技術。我花了很長時間把 Firestore 換成 SQLite、把 App Engine 換成 fly.io,但 Vue 一直留著,讓開發體驗很不愉快。
每週我在 What Got Done 上發布更新時,都會想起自己更喜歡用 VS Code 和 Hugo 寫部落格的工作流程。於是某個週末,我就直接用 Hugo 把 What Got Done 重做成一個簡單的靜態網站,現在架在 weeks.mtlynch.io 上。

我用 Hugo 把 What Got Done 重做成一個靜態網站。
所以,六年來我大概花了 300 小時去實作和維護那個以 Go + Vue + SQLite + fly.io 為架構的 What Got Done。而用簡單的 Markdown 檔案加上 Hugo 重做成靜態網站,卻只花了 10 小時。
因為新版是只為我自己做的應用程式,我可以加入一些個人化的功能,例如從 git commit 自動預填每週更新。當然,它在託管、維護和備份上也簡單、便宜了好幾個數量級,因為它只是一個有版本控制的靜態網站,而不是一個前端、後端、資料庫各有不同技術堆疊的完整網頁應用程式。
讓 What Got Done 退役
我不想永遠維護 What Got Done,尤其是現在我自己已經沒在用了。
雖然 What Got Done 只剩下少數活躍使用者,但我很討厭拋下那些曾經開始使用我服務的人,所以我盡量讓 What Got Done 的退場體驗做得貼心一點:
- 我在網站上公告 What Got Done 將在今年底停止營運。
- 我新增了讓使用者以 Markdown 格式匯出文章的功能。
- 反正我本來就需要這個功能來把自己的資料搬到 Hugo 上,所以就想說乾脆直接做進網頁應用程式裡,讓所有使用者都能使用。
- 我新增了讓使用者在 What Got Done 關閉後設定轉址的功能。
- 例如,我已將我的個人頁面
whatgotdone.com/michael設定為永久重新導向至 weeks.mtlynch.io。
- 例如,我已將我的個人頁面
我在 Gleam 上的 AIM 對話紀錄解析器進度
我還在透過動手做一個解析高中、大學時期舊 AIM 對話紀錄的解析器,來學習 Gleam 程式語言。最基本的紀錄長這樣:
Session Start (AIM - DumbAIMScreenName:Jane): Mon Sep 12 18:44:17 2005
[18:44] Jane: hi
[18:55] Me: hey whats up
Session Close (Jane): Mon Sep 12 18:56:02 2005
解析時間戳記
六月時,我的解析器已經能在基本層面上運作,可以把上面的紀錄轉換並擷取寄件者和訊息內容,像這樣:
[
Message(sender: "Jane", body: "hi"),
Message(sender: "Me", body: "hey whats up"),
]七月我又有了進展,解析器現在能看懂時間戳記了,這部分有點棘手,因為它必須把對話階段中繼資料裡的日期,和訊息中簡單的 HH:MM 資訊結合起來。所以,我的對話紀錄解析器現在可以把上面的紀錄轉成這樣:
[
Message(
timestamp: must_parse_rfc3339("2005-09-12T18:44:00-04:00"),
sender: "Jane",
body: "hi",
),
Message(
timestamp: must_parse_rfc3339("2005-09-12T18:55:00-04:00"),
sender: "Me",
body: "hey whats up",
),
]把詞法分析與語法解析合併為單一步驟
我也把解析器簡化成單次遍歷,而不是分開做詞法分析和語法解析。
一開始我以為把對話紀錄先切成一串符記(token)再來解析,會更正規、更優雅。所以,比起讓解析器直接去解析像 Session Start (AIM - DumbAIMScreenName:Jane): Mon Sep 12 18:44:17 2005 這樣的一行,我希望先由詞法分析器把它轉成這樣的一系列符記:
[
SessionStart,
Word("(DumbAIMScreenName:Jane)"),
ColonSpace,
Word("Mon"),
Word("Sep"),
...
]但那意味著我需要一個隱藏的前置步驟,先把字串切成詞法分析器能辨識的子字串,像是 ["Session", " ", "Start"],而且我還得自己實作字串分割的邏輯,因為 Gleam 內建的函式庫沒辦法在用子字串分割字串的同時還保留分隔符號。例如,如果我先用換行分割再用空白分割,最後會得到一串字串,但就無法分辨分隔符號到底是空白還是換行。
感覺起來我其實把輸入解析了三次:一次是用我自製的字串分割,一次是詞法分析,一次才是真正的語法解析。我一開始以為只是因為我對函式語言或文字解析器不夠了解,之後就會找到更優雅的詞法分析與解析方法。
我就拿這個困惑當藉口,終於買了一本紙本的Crafting Interpreters,這是我看過設計最精美的軟體書籍。

我的 Gleam 專案終於給了我一個藉口去買一本紙本的Crafting Interpreters。
讀完書中關於詞法分析的那一章後,我的結論是,我的 AIM 紀錄結構並不適合做詞法分析。我試著把所有東西收斂成一個逐字元讀取輸入的解析器,感覺反而更簡單。
逐字元解析比較討厭的地方是,Gleam 的模式匹配看起來醜多了。在舊的實作中,我可以像這樣尋找字串模式:
case contents {
["Session", "Start", ..rest] -> tokenize_list(rest, [SessionStart, ..acc])
["Session", "Close", ..rest] -> tokenize_list(rest, [SessionClose, ..acc])
而現在則變成更長、更雜亂的模式匹配:
case state.remaining_graphemes {
[
"S",
"e",
"s",
"s",
"i",
"o",
"n",
" ",
"S",
"t",
"a",
"r",
"t",
...
我是不是硬把類別塞進 Gleam 裡?
隨著解析器的進展,我發現自己一直在寫簽章一模一樣的函式:
fn parse_tokens_with_messages(
tokens: List(Token),
messages: List(Message),
) -> List(Message) {
很多函式都接收相同的參數、回傳相同的值,而隨著程式碼愈寫愈多,參數與回傳值的列表也愈變愈長。
所以,我建立了一個 ParseState 型別,改成把這個狀態傳來傳去:
type ParseState {
ParseState(
last_timestamp: timestamp.Timestamp,
messages: List(Message),
remaining_graphemes: List(String),
)
}
fn parse_graphemes(state: ParseState) -> ParseState {
但那感覺就像偷偷把物件導向的類別塞進 Gleam 裡。因為如果這是 Go,程式碼看起來會像這樣:
type Parser struct {
LastTimestamp time.Time
Messages []Message
RemainingGraphemes []rune
}
func (p Parser) Parse() {
函式程式設計的高手們:我這樣算作弊嗎?還是說這就是在函式語言中傳遞狀態的正確方式?
收尾
完成了什麼?
- 發布了〈少有人用卻有效的 Email 技巧〉,並將擴充版寄給搶先體驗的讀者。
- 將《Refactoring English》最後一批僅在網站上的內容移入電子書中。
- 發布了部落格文章〈將 ZFS 儲存池從 RAIDZ1 遷移到 RAIDZ2〉。
- 為 ScreenJournal 建立了更完善的密碼重設流程。
- 為 PicoShare 的訪客新增了檔案到期選項。
- 為一篇即將發布的部落格文章提供無償編輯,以換取將回饋內容公開作為編輯服務的行銷素材。
- 為 What Got Done 制定了退役計畫,並將我的資料遷移至 weeks.mtlynch.io。
學到的教訓
- 不是每一位搶先閱讀《Refactoring English》的讀者都想提供回饋也沒關係。我可以繼續主動聯繫讀者,找到少數願意更積極參與的人。
- 定期檢視自己浪費時間的方式,並思考如何改善,總是有幫助的。
下個月的目標
- 寫個人化的信件給 20 位還沒聊過的讀者。
- 發布《Refactoring English》的新章節。
- 完成剩下的行銷待辦事項。
需要協助
如果你是想提升寫作能力的開發者,或你認識這樣的人,歡迎與我聯繫。
隨機一篇部落格
留言
登入後參與討論