預優化是軟件交付的殺手

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文最初發表於 Medium 博客,經原作者 Grace Ke 授權,InfoQ 中文站翻譯並分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"導讀"},{"type":"text","text":":本文作者用她的經驗教訓爲我們娓娓道來:預優化是不是軟件交付的殺手?相信她這篇文章能幫你避開軟件交付的那些坑。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我目前的崗位上,我有一個團隊花了兩年多的時間,試圖讓自己從前一個團隊留下的坑“爬”出來,而這個坑是源於那個團隊很差勁的設計,這些設計是一些處理平臺服務的相當關鍵的應用,這些應用是許多其他微服務的上游功能。我認爲問題的很大一部分在於,前一個團隊是過度預優化的受害者。我知道,當事後諸葛亮是站着說話不腰疼,因爲我們現在正承受着我們所繼承的服務生態系統的後果和持續存在的問題。然而,我相信,我們不得不解決的一些問題本是可以避免的。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"謬論"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"預優化是指,在遇到問題之前,我們必須一次性解決所有的問題。這幾乎是在說,作爲開發人員,我們應該有一個“水晶球”,可以預測我們在應用程序生命週期中將會遇到的所有問題。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"例子與經驗教訓"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我所見過的團隊進行預優化的一些事情包括:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"通過過早地擴展來預測自動擴展需求。我見過的最常見的問題之一是,團隊會根據遺留應用程序在內部執行的情況,配置他們可以使用的最大的 CPU\/mem EC2 組合。問題是,當我們監控這些新服務時,我們注意到,每個實例只使用了 CPU 的 2%。就操作成本而言,這些非常大的實例其實是非常昂貴的,但對服務的性能似乎並沒有多少貢獻。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":有時候,使用較小規模的實例進行擴展,要比過早地擴展實例要好。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"與上述情況相反,我還發現團隊只專注於一件事進行優化。我們繼承的一個應用程序,它有一個擴展策略,根據遺留系統過去的數據去假設系統負載將是什麼樣的情況。然而不幸的是,儘管他們考慮了服務方面的可擴展性,但團隊並沒有考慮數據庫方面的性能問題。在人們登陸到系統的幾個小時內,數據庫就變得不堪重負,我們花了幾乎一整天的時間試圖找出動態數據庫擴展策略(在生產環境中)來解決這一問題。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":不要根據你正在建模的遺留系統的工作方式進行優化,要始終進行測試和驗證。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"我發現的另一個預優化活動是從一開始就計算出所有的數據模型,然後過早地對數據進行歸一化。當我還是一名開發人員時,我記得在數據模型被佈置時,我與所在的團隊進行一番討論。爲適應未來的增長和存儲他們認爲用戶會感興趣的數據,團隊花了近兩次 sprint(在我看來,比我想象的長了 1.5 個 sprint)來討論數據模型應該是什麼樣子的。作爲一個從電商商店出身,有着多年後端服務開發經驗的人,我知道我們正在艱難地掙扎、前行,因爲我的經驗告訴我們,我們永遠不可能提前預測所有的數據需求,但我們可以使數據合同具有可擴展性,這樣一來,我們就不會不斷地進行違約更改。但是,我又知道些什麼呢?不幸的是,我屬於少數派,而團隊中聲音最大的那個人贏了。快進到我們交付的時候,數據模型是如此複雜且難以更改,以至於每當我們進行調整時,我們都不得不要求下游的所有人也要修改和重新部署他們的代碼,以適應我們的更改。數據查詢的性能也非常低效,因爲表過於歸一化而不是更扁平。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":有時候,簡單的設計可能會更高效。歸一化總是可以在以後進行,但它並不總是解決問題的辦法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/15\/eb\/15a5a44dd18df84a6bcaaf3f518e84eb.jpg","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":4,"normalizeStart":4},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"我還見過有些團隊在應用程序中加入了花哨的功能,不僅是因爲它很酷,而且“非常前衛”,還因爲他們認爲我們的客戶不知道他們想要什麼,我們會給他們想要的東西。團隊最終會構建“某一天的功能”。當公司的一個團隊開始着手開發一個新的報告產品時,這就是團隊前進的目標。然而,當他們交付的時候,顧客卻很討厭這個產品。他們覺得交付給他們的功能並不是他們要求的。遺憾的是,基本功能甚至都無法正常工作。今天,我們公司正在與第三個不同的團隊一起開發該產品的第三次迭代。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":在開發某個東西之前,一定要問清楚客戶想要什麼,千萬不要僅僅因爲它“很酷”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":5,"normalizeStart":5},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"我還看到過團隊因爲陷入“分析癱瘓”而永遠無法推進開發。很多時候,由於團隊試圖設計這樣的一個沒有故障且不會發生任何問題的系統,但過去了這麼長時間,還沒有交付一個能夠工作的產品。我在一個項目中就遇到了這種情況。團隊花了很長時間來設計\/預測潛在的問題,以至於我們延遲了產品的交付,而一旦交付到客戶手中,我們似乎仍然沒有抓住所有的問題。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":只需根據目前瞭解的需求進行構建即可。軟件應該是靈活的,如果需要的話,應該能夠容忍修改。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":6,"normalizeStart":6},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"有時,我會查看我們繼承的一些服務,對於某些服務,可能至少有 20 多個端點超出了必要的範圍。例如,真的有必要有一個端點來公開所有可能的數據組合嗎?也許這裏的答案是,使用查詢參數和一個端點即可。另一種觀點是,也許沒有人會使用或需要這些數據。我已經數不清我的團隊對某些端點被調用的頻率進行了多少次研究,而答案恰恰是零次。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":請記住,構建服務端點有多種方法,但請遵循最佳實踐,並且僅在需要數據時才公開數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":7,"normalizeStart":7},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"最後,我還見過一些團隊在沒有意義的情況下爲工具構建 UI。但我要問的問題是,這個 UI 是爲誰設計的?他們能否使用 Web 服務來獲得這些功能?"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":雖然構建 UI 來實現我們的全棧開發能力很酷,但有時團隊應該將精力集中在構建其他工具中(也許是監控,而這通常是事後纔想到的)。要遵守 YAGNI(You Aren't Gonna Need It,你不需要它)原則。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"榮譽獎"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在預優化的另一面,還有一些工程團隊應該做但沒有做的活動,這些活動也會導致軟件交付失敗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"如果有意義的話,要進行“構建還是購買”的評估"},{"type":"text","text":"。我在團隊中發現的一個問題是,他們太過專注於構建炫酷的解決方案,卻從來不對他們交付的產品進行構建還是購買的分析。我的另一個團隊,負責 DevOps 的實施,他們一直在研究不同的功能標記解決方案,以便在出現問題時,我們就可以引入新功能並鼓勵更頻繁的部署,而不會讓我們的客戶接觸新代碼。有一些開源和商業的解決方案可以很好的做到這一點。其他交付團隊之一決定他們可以在內部構建一個。然而,一旦其他團隊開始使用它,很明顯,設計\/構建它的團隊並沒有考慮到所有的用例,而且這個應用程序絕對不是爲了支持數千個功能標誌而構建的。實際上,可伸縮性就是一個如此嚴重的問題,以至於有一天,它導致了一次重大的停機事故,調用它的應用程序甚至都無法恢復到正常行爲,因爲它們沒有內置任何中斷和失敗機制。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":不要爲別人已經解決並且已經做得很好的問題構建東西。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"不要相信高測試覆蓋率就等同於高質量的產品"},{"type":"text","text":"。我們在工程部門有一個 QA 子組織,它有一個獨立的報告結構。他們制定的一些規則有時是武斷的。例如,他們要求團隊在部署之前,所有的應用程序和服務的測試覆蓋率必須達到 95%。然而,據我所見,人們爲了達到該目標而對測試設置進行了調整,在我的職業生涯中,增加測試覆蓋率(因爲它可以被人爲操縱)從來就不是必然意味着就沒有 Bug。我真不知道這種幻覺從何而來。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":不要爲了勾選一個複選框就過於專注那些毫無意義的指標。確定該指標試圖實現的目標,然後將注意力集中在這個目標上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"一定要考慮監控"},{"type":"text","text":"。由於我的團隊有 SRE,所以監控是我們促進並推動團隊與我們的 DevOps 實踐社區一起做的事情。對於團隊來說,爲他們的應用程序和服務創建監控和警報的重要性再怎麼強調也不爲過,這樣我們的客戶就不會在我們之前發現問題了。如果我們沒有率先發現這些問題的話,那隻會讓我們看起來很糟糕。儘管圍繞這一問題進行了多次溝通,但仍然令我感到震驚的是,我們幾天生產的數百個應用程序\/服務中,甚至沒有進行綜合測試或運行狀況檢查。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":創建監視器當然是一項時間投資,但與團隊率先發現問題相比,客戶發現問題的成本是很高的。“預防勝於治療”這句格言在這裏很適用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":4,"normalizeStart":4},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"保持安全第一的心態"},{"type":"text","text":"。我還經常看到團隊將安全性視爲“技術債務”,並推遲解決安全性違規問題。在雲端環境中,這樣的做法更危險,因爲安全漏洞很容易變得非常公開(例如,在公共 S3 存儲桶中意外泄漏 PII 數據),並且代價非常高昂。由於我的一個團隊負責我們所有的 AWS 基礎設施,因此我們親眼目睹了安全漏洞的代價有多大。通常,我們最終會刪除該賬戶,並不得不重新構建所有內容,特別是當它有關鍵的生產工作負載時。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":不要以爲安全漏洞最終不會影響到你的團隊。這往往是時間的問題,而不是會不會發生的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":5,"normalizeStart":5},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"不要創建循環依賴項"},{"type":"text","text":"。我甚至都不知道這是怎麼發生的。我見過團隊設計的服務必須按照非常特定的順序進行部署才能正常工作。我還見過一些是先有雞還是先有蛋的情況,其中服務 A(比如說身份驗證服務)需要服務 B(比如說服務查找),但它需要來自服務 A 的某些內容(比如身份驗證密鑰)才能工作。這就違反了很多設計原則,並散發着“代碼的味道”。我甚至都不知道它是怎麼通過架構審批的。然而,這種模式我最近看得太多了,我開始懷疑自己是否錯過了什麼東西,因爲軟件設計已經發展了那麼多年。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"經驗教訓"},{"type":"text","text":":不要忘記使用經過實踐驗證的設計模式。它們的存在是有原因的。如果有什麼不對勁的地方,就對設計提出質疑,如果有必要的話,再從頭開始。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"爬,走,跑"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在某些情況下,一些預優化還是有意義的(例如,對於代碼可維護性或可移植性)。避免預優化可不是懶惰的藉口。但是,如果我們爲永遠不會發生的事情進行預優化,那麼它就是浪費了。預優化有時也會導致設計決策,讓我們陷入無法擺脫的困境。我相信,有時候,預優化是反敏捷的。軟件交付是一個迭代的過程,特別是對於複雜的項目。我看到過一些交付團隊,特別是那些非常樂觀和初級開發人員的團隊,在項目的早期往往就做出超出他們能力範圍的事情。我並不否認他們的熱情,但有時候這纔是經驗的真正價值所在,有時候,磨練還是必要的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在軟件開發中,有這樣一種“爬,走,跑”(crawl, walk, run)的原則。這意味着首先要構建所謂的最小可行產品(MVP,即“爬”階段)。MVP 並非開發人員爲了取悅產品所有者而胡亂拼湊的東西。這是滿足客戶基本需求的實際有用產品的最小版本。將 MVP 從足夠好到更好的關鍵在於迭代:獲得反饋,重構\/修復,然後再次獲得反饋,同時要牢記質量。創建 MVP 可以幫助團隊能夠快速失敗,因爲它可以快速識別問題,並在需要時進行調整。在“走”階段,團隊可以通過引入性能(例如,確定自動擴展策略)、成本優化(調整資源大小)和整體應用程序調優來“加速”,這有時可能包括重新架構(例如,從 EC2 託管的應用程序到使用無服務器解決方案的容器化應用程序)。最後,在“跑”階段,團隊現在可以加速到他們感到舒適的速度。也許這意味着一天要多次部署服務。這通常也是團隊開始考慮災備等更大問題,並進行“遊戲日”,這是一項實踐,以確定如果最糟糕的情況發生(又稱“混沌工程”),可能會出現哪些潛在的問題。團隊可以借鑑 Netflix 和 Amazon 的做法,看看他們是否可以利用這些公司從這些實踐中學到的一些經驗教訓。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"預優化並不像有些人說的那樣是“萬惡之源”。有些東西你可以\/應該進行預優化,但也有些東西你不能\/不應該進行預優化。對錯誤的東西進行預優化可能會讓團隊蒙受很大的損失,而他們本可以交付高質量且可用的軟件,讓客戶對此讚不絕口的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者介紹:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Grace Ke,在 IT 行業工作多年,沉迷於卡通爆炸(Toon Blast)遊戲和網購。希望有朝一日能出版一本兒童讀物。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/medium.com\/swlh\/pre-optimization-is-a-killer-of-software-delivery-f04231f24c4e"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章