03.19 程序員的核心能力

在編程技術飛速發展的今天,一方面大家的開發效率越來越高,但另一方面也越來越焦慮,內心的獨白是“我每天拼死拼活的學了一堆東西,但現在學的幾年之後就過時了,那時程序員這碗青春飯就吃不下去了…” 這是一個很現實的問題,但發現它後仍置之不理的話,幾年後你就會陷入困境。那該如何解決呢?有人說做幾年技術之後轉型管理,這是完全可行的,但不是今天我們要聊的話題。如果是繼續做技術,你需要找到一些核心的技術能力。它們使你更快速地工作,產生更高質量的成果,不容易被後來者超越,最最重要的一點是,不會因為某項技術的過時而失去價值。我希望通過接下來的幾篇文章,和大家分享我的想法,今天講的第一項核心能力,我稱它為“引擎式思維”。

當你寫程序的時候,除了要解決眼前的問題,你有沒有想過將來是否會需要解決類似的問題。把你的解決方案從解決一個問題擴展到解決一組問題是一項非常重要的能力,也往往是區分新人與資深技術人員的一條分界線。大家都知道寫遊戲的時候,會先寫一個核心引擎,它把顯示遊戲場景、粒子系統、物理模擬等遊戲中最普遍的問題抽象在一個代碼庫中,這部分的邏輯會在遊戲中被不斷重用,甚至在多個遊戲中重用。在有了引擎之後,上層的遊戲開發人員就可以把精力集中在遊戲內容的製作上。把你代碼中普適的、可重用的部分與具體業務邏輯分離的思想,就是引擎式思維。在遊戲中我們叫它引擎(Engine),在應用開發中我們叫它框架(Framework),在基礎服務裡我們又叫它架構(Architecture),但它們背後的思想是相同的。

有些新手可能會覺得引擎、框架這些東西都是大牛開發的,我們只要會用就好了。但其實事情不論大小,只要有套路可尋,都可以用引擎式思維去解決。在你日常要解決的問題中,粗看可能各不相同,但這時你若退後一步,從一個更高地視角去發現問題之間的共同點,把解決方案中通用的部分從具體的問題中抽離出來,這時你就有了自己的框架。這是程序員的一種核心能力,它不會因為技術的日新月異而過時,但你也不可能一躍而蹴,它會隨著你技術能力的成熟而逐漸精進。就如同武俠小說裡的內功,難以速成,但卻比學習任何具體的招式都更重要。

從一個例子講起

引擎未必是龐大而複雜的,它只是一種思想,也可以被用於解決小問題。我們來看個例子,假如有一天團隊開始抓代碼質量,所有文件在提交之前都要做語法檢查。這就需要利用 git hook 寫一個 precommit 腳本,它會在每次 git commit 之前運行。不瞭解 git hook 的同學戳這裡

你用 Python 寫了第一版的 precommit 腳本,代碼如下,簡單清晰沒毛病。

def precommit():
files = get_commit_files()
for fname in files:
if fname.endswith('.py'):
pylint(fname)
elif fname.endswith('.js'):
jslint(fname)

過了兩天,你團隊的代碼質量又進步了,對所有的 Python 模塊加上了單元測試,這時就要求所有 Python 文件在提交前還需要跑對應的單元測試,如果單元測試不通過,就讓提交失敗。所以你改了一下 precommit 腳本

def precommit():
files = get_commit_files()
for fname in files:
if fname.endswith('.py'):
if py_unittest(fname) == 'failed':
sys.exit(1)
pylint(fname)
elif fname.endswith('.js'):
jslint(fname)

雖然比之前稍醜了一些,但可讀性還是可以的。大家在嚐到了 precommit 腳本的甜頭之後,更加變本加厲,各種需求接踵而來

  1. “加上 Javascript 的單元測試吧”

  2. “圖片提交之前能不能用 TinyPNG 壓縮一下”

  3. “data.py 是數據文件,不需要運行單元測試”

  4. “我們另一個 git repo 也想用你的 precommit 腳本,但要把 Python 的單元測試先禁掉”

1), 2)都是小意思,加上就行了。3)讓你有點為難,precommit 函數已經有點長了,要不這個判斷寫在 <code>py_unittest/<code> 裡吧。到了4),你忍不住頂了回去“我沒空寫,拷貝一份自己去改吧!”

這時你開始意識到這樣下去不是辦法,但別人的這些需求又很瑣碎,讓你沒辦法很好地組織代碼。如果要系統化地解決這些問題,該怎麼做呢?你發現所有的需求都可以抽象成同一個套路

如果文件名符合某個條件,就對該文件執行一個特定函數

如果讓使用者來提供文件名的條件以及要執行的函數,那麼你只需要構建一個引擎來做條件判斷,併為要執行的函數準備參數就行了。有了這個思路後,你把每個需求抽象成了一條規則,規則包含以下幾個屬性

  • id: 規則的名字

  • include: 適合的文件名

  • exclude: 要剔除的文件名

  • func: 對符合條件的文件,要執行的函數

  • warning_only: 如果設為 <code>True/<code>,在 func 執行失敗時,只打印警告。否則,中斷提交。默認為 <code>False/<code>

你把代碼重構成了下面這個樣子

rules = [
{
'id': 'pylint',
'include': ['*.py'],
'func': pylint,
'warning_only': True
},
{
'id': 'py-unittest',
'include': ['*.py'],
'exclude': ['data.py'],
'func': py_unittest
},
...]def precommit():
files = get_commit_files()
run_hooks(files, rules)

<code>run_hooks/<code> 是你整個引擎的入口。而那些具體的需求不再是引擎的一部分,而只是外部輸入。更進一步,你把 rules 放在一個單獨的 JSON 或是 YAML 文件中。這樣別的團隊可以定製自己的 rules 配置文件,然後直接使用你的 precommit 引擎,這樣就完美解決了之前的第4個需求。

這裡不給出 <code>run_hooks/<code> 的具體實現,有興趣的同學可以自己寫寫看。

避免過度設計

當你有了引擎思維之後,也要小心另一個極端,那就是過度設計。這裡有兩種情況,一種是過早地設計引擎,沒有從實際的需求中去總結抽象,而是憑自己的臆測。對於一個瞭解業務的資深工程師來說,這樣做也許是可行的,但如果你經驗不足,還是別太相信你的預判。另一種過度設計是想讓引擎能滿足所有的需求,這可能讓你的引擎變得複雜而難用。在設計引擎時,我也會使用80/20原則,讓引擎能很好地解決80%的需求,對於剩下的20%,引擎需要有一種 fallback 機制,它只要做到不擋道,能讓使用者自己去解決這些需求就行了。過度設計可以聊的點很多,將來可以單獨發文討論,這裡就此打住。

培養引擎式思維

我是在工作幾年之後,才逐漸有引擎式思維的意識。對於每個待解決的問題,開始問自己“它是不是某類問題中的一個個例,這類問題能否被系統化地解決”,而隨著經驗地增長,越來越多的時候我會得出肯定的答案。因為這是一個長時間累積的過程,我鼓勵大家儘早地思考這個問題,對你的技術進步一定會有幫助的。

從另一個角度,如果你已經是團隊中的技術領導,在與團隊成員的技術討論中,在 Code Review 時,你該教他們些什麼?Coding style, 某種語言的語法糖,一個可以解決他們手頭問題的第三方庫?這些是必要的,但還不夠。初出茅廬的小夥伴們可能為了完成佈置的任務就已經疲於奔命了,或是沉醉在某項新技術的學習中,但他們未必會有意識地去思考如何搭建一個引擎來解決一類問題。在這方面,初期他們需要有好的範例去模仿,中期需要有人提醒指引,日久之後他們才會自發地造引擎,在中前期一個優秀的技術領導若給於他們這方面的幫助,就能大大加速他們核心能力的成長。我覺得這是培養團隊成員的重要一環。


分享到:


相關文章: