當我們在學程式時,要學的到底是什麼?

(本文轉載自 Huli 部落格

小明原本是個從事行銷相關工作的新鮮人,在工作屆滿一年之時萌生了退意;沒專業知識又愛亂指揮人的主管、每天加班卻沒有加班費還是做不完事情的日子、絲毫不尊重專業也不尊重人的客戶,這一切的一切慢慢把他逼到極限。

有一天,當他在閱讀臉書的時候,看到了某個培訓班的廣告,標榜著只要來這邊上半年的課程,就可以輕鬆轉職成 Web 工程師,還附上一大堆參考資料以及強烈呼籲大家工程師的時代已經來臨,想上車要趁現在,這一班列車將帶你前往無限光明的未來。

小明仔細一想,覺得也挺合理的。畢竟日常生活看到一大堆 AI 征服世界、區塊鏈金融革命、AR/VR 突破虛實界限的新聞,這些走在前沿的科技已經愈來愈純熟,感覺再不寫程式就要被時代的尖端給刺死,或是被潮流給吞噬了。

當晚,他就向主管提了離職,小明還很有責任的問說那是不是一個月後走差不多?這樣才能把現有的專案交接給其他同事。主管只是淡淡地說了:「你這種草莓族我看多了,不用待一個月,你明天就走吧」,小明雖想反駁,可也不想在這邊浪費時間,他倒是比較擔心要接專案的同事,什麼都沒有該怎麼接?但轉個念頭,自己當初進公司時也是這樣的,什麼都沒有,什麼都不在,什麼都不奇怪,與其擔心同事,不如勸同事趕快離職比較實在。

重獲自由以後,小明立刻報名了他昨天看到的培訓班,時機剛剛好,下個禮拜正好開課。繳了將近十萬塊的學費以後,小明滿心期待的盼望著課程,與此同時也自己先去網站了查了一些與程式相關的資料。

到了下週,課程正式開跑,為期半年的課程,上課時間簡直就像回到了高中生活,禮拜一到五、早上九點到晚上六點(還是比高中生幸福一點,不用七八點就到學校),要學的東西可不少,光是課綱就讓小明眼花撩亂,一大堆看不懂的英文簡寫,完全不知道是要幹嘛的。

接著讓我們來個電影式的快轉,後製麻煩幫我上個「半年後」的字樣在右下角,背景搭配台北市的縮時空景。

小明結業以後覺得自信滿滿,跟半年前的自己比起來了,現在可是會了一大堆技能,像是 Ruby on Rails(可填入任意一個後端相關技術)、HTML、CSS、JavaScript、jQuery、Node.js、React,怎麼想都覺得自己應該能輕鬆勝任工程師的工作。拜託,結業時的作品可是跟幾個同學完成了一個線上購物網站,該具備的技能可是都具備了。

除了培訓班有合作的幾間公司以外,他也自己上數字人力銀行網站投了履歷,但效果不是很好,他跟幾個同學交換求職的狀況,發現機會沒有想像中的多,還有同學說聽聞業界某些公司闡明了自己不收培訓班出來的學生。

小明心想是不是以前的學長姐雷了一波,導致公司把培訓班列入黑名單,否則自己會的技能那麼多,沒理由連面試的機會都不給吧。

雖說機會沒有想像中的多,但還是收到了大約十個面試邀請,不久後就迎來人生中的第一場工程師面試,地點在某辦公大樓,是一間做外包的接案公司,面試官看起來人滿好的,想必待會也會很順利吧。

「你先來個自我介紹吧,順便也介紹一下你履歷上的作品集」

『您好,我叫小明,從某某培訓班畢業,我附上的作品後端是用 RoR + MySQL,前端則是採用 React 來實作,有跟其他同學一起合作完成,是直接幫同學的親戚做了一個線上的購物網站』小明介紹完,心想:如何,還不錯吧!

「嗯,看起來還不錯,接著我們來問一些技術問題吧。你知道 Session 是什麼嗎?」

『知道,就是一個可以識別身份的機制,在 Rails 裡面可以用 session[:id] 來實作』

「那如果今天不讓你用這個,你還能做出一個 session 機制嗎?」

小明傻住了,想說這什麼鬼問題,不就是用框架內建的就好了嗎,更何況除了內建的以外,自己也不是完全理解 session 這東西,只知道怎麼用而已。

『呃…這個我可能就要去查一下資料,但我相信我有能力可以完成』

「好,再來,既然 Ruby 是個注重物件導向的程式語言,我們來談談它好了,可以請你說明一下類別跟物件的差異,以及什麼是繼承、什麼又是封裝嗎?」

小明心想:「這不簡單嗎,這個老師都有教過了,結業前的面試考前猜題也有這幾題,根本天助我也」,於是他就把之前早已回答過無數遍的答案給講了出來。

「這題答得不錯,接著我們來寫一點 code 吧?這一段程式碼你稍微研究一下,給你二十分鐘,我等等回來的時候希望看到你怎麼重構這段程式碼,用物件導向的幾個特點讓它更好維護」

小明看了看 code,發呆了二十分鐘不知所措。你要我回答什麼是物件導向這個簡單,可是你要我重構一段程式碼,這我還真的沒碰過,我連繼承都沒用過幾次,連 class 都沒寫過幾個,框架都有內建好的類別,自己根本不怎麼用這些功能的。

最後小明只改了幾個變數名稱,想說也只能這樣了。

面試官一回來,看了看程式碼之後稍微搖了搖頭,接著說:「最後我們來考一些簡單的程式題目好了,寫過九九乘法表嗎?」

小明點了點頭,回憶起之前自己特地練了這題,就因為聽說有同學不會寫這個而被刷掉,所以上了 leetcode 刷了幾題簡單的題目做足準備。

「那我們來個不一樣的,我想要一個 19*9 乘法表,然後輸出的順序要是 0, m, 1, m-1, 2, m-2…以此類推,舉例來說,當要輸出 7 的時候會長這樣:
7 * 1 = 1
7 * 9 = 63
7 * 2 = 14
7 * 8 = 56
7 * 3 = 21
7 * 7 = 49
7 * 4 = 28
7 * 6 = 42
7 * 5 = 35」

小明心裡一驚,想說怎麼從來沒有看過這個題目,然後先把自己熟知的九九乘法表的程式碼寫出來,再開始東改西改,可是怎麼改都發現會有 bug,要嘛就是順序不對,要嘛就是跑到程式當掉。

小明奮戰二十分鐘之後,自知今天的面試之路應該就到這邊,之後也沒戲了,就對面試官說:「不好意思…這題我解不太出來」

面試官看了看小明東改西改的 code,説:「沒關係,那今天的面試差不多就到這邊,我跟幾個同事再討論一下,之後會請人資跟你聯絡」

第一次的面試就給了小明一記當頭棒喝,出現了一大堆以前沒有見過的問題,但也不是完全沒見過,他覺得自己面試失利的原因是題目還刷得不夠多,回家之後繼續上 leetcode 刷題,為下一次的面試做準備。

時間又過了一個月,小明陸陸續續接到面試邀請,每間公司的面試問題都不太一樣,只要是小明碰過的問題就答的很好,沒碰過的就七零八落。就在小明的努力不懈之下,在幾天後終於如意以償地拿到了某間公司的 offer,一個月 33k,做的是網頁開發,以後端為主軸,但前端也會碰到。

隨著在公司待的時間越長,小明漸漸變得迷惘,為什麼平常工作時候的那些技能以前都沒教?為什麼以前學了這麼多,可是工作上還是有一大堆問題解不開?為什麼工程師的工作跟他以前想像中的不一樣?

這一切的一切都還沒有人給他答案,而他雖然察覺不對勁,但也只能先這樣繼續的迷惘著,繼續的工作,繼續的寫 code。

落落長的開場白故事結束了,以上情節純屬虛構,如有雷同的話也很正常,因為我聽過的很多例子差不多就長這樣,但我沒有要針對某個特定培訓班的意思,無論是去外面上課還是自學,都可能擁有類似的經歷。

你學的程式,到底哪裡出了問題?

如果你想讓一個人很快的就覺得自己會寫程式,那你應該怎麼做?

你就讓他去學而且只學一些現代的框架跟工具,他用著那些快速方便的指令、看著系統自動生成的幾個檔案,發現只要改一改就可以超快的做出自己想要的東西,他就會覺得自己會寫程式了。

或者是你叫他每天把這段程式碼打十遍,連續打個一個禮拜,讓他的手指記憶起來這些程式碼,過不久以後他就可以批哩啪啦在三分鐘內寫出一個部落格,哇,厲害!

看似厲害歸厲害,但如果我們把他用的這套工具給拿掉了,那還剩下什麼?

剩粉,bang!沒有啦開個小玩笑,正確答案是什麼都不剩。

他熟悉的不是程式,是那套框架、那套工具,只有在那個範疇裡面的東西才是他所熟悉的,一但換了一套工具或者是出現他沒碰過的狀況,就居居了。

這種工程師通常被稱為炮灰型工程師,或也可以叫做工具導向工程師吧。

那是什麼造成這樣的問題?問題在於,他只學到了表面而沒學到裡面,但重要的往往都是裡面而不是表面,光鮮亮麗的表面之下一定是複雜層疊的裡面所構造出來的。

舉個大家很熟悉的例子,請回憶起國高中時學習物理或數學時的情形,這大概是我們最早開始「刷題」的時候。

寫了作業、寫了點麵線的數學講義、寫了數學段考的歷屆考題,甚至有些人還會直接去拿數學競賽的題目來寫。可是儘管你再怎麼寫,段考時總是會出現一些你沒看過的題目,然後你就腦袋一片空白什麼都不會寫了。

但神奇的是,考完檢討考卷的時候,你發現這個解法你根本就會,你發現這一題也沒有想像中的困難,只不過是把以前學會的東西變換一下而已,解法一點也不深奧反而還很平易近人。

你是在訓練記憶力還是解題能力?

有些人在用題海戰術刷題的時候,以為自己在鍛鍊解題能力,但殊不知其實在鍛鍊的是記憶力。他們在做題目的時候是這樣的:

  1. 看看題目,如果看過就寫得出來
  2. 沒看過的話思考一下,想不出來看解答
  3. 看完解答做下一題

題目看得多了,能回答正確的題目也就多了,因為已經知道解法是什麼了,自然能夠做得出來。

可是題目是無窮的,人的時間跟記憶力是有限的。

這種方法一但碰到了完全陌生的題目或是根據經典題目做一些改變的變化題,就完全沒轍,什麼都做不出來。

我不會說這種方法完全沒用,因為在某些特定場景下還真的滿好用的,特別是變化沒那麼多的時候,可能有八九成的題目都能被你命中。

但它不是一個可以被規模化的方法。它的成長是線性的,你看過 100 題你就會 100 題,你看過 n 題你就會 n 題,你要多學會 10 倍的題目,你就要花 10 倍的時間。

那可以被規模化的方法是什麼?是思考。

做題目的時候,重點是鍛鍊你的解題能力,好好想想應該怎麼把這道題目解出來。真的想不透了才看解答,可是看解答的時候也不是看過就算了,而是要邊看邊想說:「這個解答可能是怎麼想出來的?背後的原理是什麼?」,接著過一陣子重新寫一次同一道題目,確定自己還是能解出來,才能證明自己真的有把解法學起來,而不是用背的。

除此之外,也可以自己加一些變化題,這邊變一點那邊改一點,看看自己能否在其他條件下也能把題目解出來,與此同時也順便加強自己對這道題目的理解,如果你是真的有把解法吸收學習進去,那每一種變化應該都沒問題才對。

這樣做雖然在每一題會花更多時間,但效益卻是巨大的。

因為隨著解題能力慢慢被鍛鍊以後,會有一套潛移默化出來的解題方法,也會對題目越來越有「感覺」,就可以做出很多神奇的事,例如說:

  1. 這題看起來就有種遞迴感,我猜是遞迴
  2. 這題很簡單啊,你就在這裡畫輔助線,你看,這樣你就會了吧
  3. 我們把這公式換一下然後兩邊同時加 x,欸你看,這不就是之前那題嗎

我一直都認為刷題不是件壞事,這也是練習的一種。可是如果你的刷題只是為了衝高題目數量,以速度為優先一直寫題目而不管思考,就算刷了一百題,你所學到的解題能力可能跟十題差不多。

所以刷題的重點不在「刷」,看更多題目只是幫助你理解你是不是真的有把某個概念給搞懂,重點幫助自己檢驗以及補強解題方法,題目是手段,鍛鍊解題能力才是目的。可是卻有很多人把題目本身當成是目的。

就算真的靠這種題海戰術進了公司,我相信之後的職涯一定還是會碰到沒看過的題目,畢竟現實中的需求千變萬化,同一個需求在不同人口中可以有不同的樣貌,你再怎麼刷也不可能刷完。比起這些,好好鍛鍊自己解決問題的能力比較實在。

學程式,但不只學寫程式

如同開頭的例子所說的,有些人學程式還真的就只學「寫程式」,只要你會寫那些 code,會用那些作業系統或是框架提供的 API 就行了。

可是真正重要的不是這個,大家都看到小明在面試的時候被電得多慘了。

真正重要的是你透過寫程式所學習到的那一套思考方法。

這一點剛好我最近很有感,因為近期剛好帶了一群學生,儘管他們程式語法都懂,可是一些簡單的題目寫不出來。原因就是出在他們還沒熟悉那一套「程式的思考方法」。

通常數理邏輯好的人,學程式比較沒什麼大問題,因為背後的那一套思考體系是差不多的,我的課程開宗明義就講說希望帶給大家四種能力:

  1. 具有找資料的能力,能夠知道如何找到相關資訊
  2. 具有分析問題的能力,能夠快速定位問題
  3. 知道如何解決問題,包括但不限於拆解問題、簡化問題、轉化問題
  4. 解決問題後能夠重新歸納並整理

對我來說,用這些方式思考就像吃飯喝水那樣再正常不過,可是對很多沒有相關背景也毫無經驗的人,他們還是學不會「像電腦那樣一行一行思考」,他們寫出來的程式是一回事,心裡真的想要執行的流程又是另外一回事。可是程式是照你寫的跑,而不是照你想的跑。

而會不會寫程式的區別,我覺得就在於這些透過寫程式學習到的思考方法,你一但掌握了,今天就算教你一個新的程式語言你也能夠快速上手;反之,若是掌握不到,教你三個程式語言你還真的會當作三個全新的東西在學。

我上面所講的這個思考方法,其實就是大家在說的 Computational Thinking,中文翻叫計算思維。

如同臺大資工系教授洪士灝在資訊通識教育以及計算思維的教學法一文中所提到的:

我個人認為,計算思維中最重要的概念之一,是能夠將計算「抽象化」與「具體化」的能力,例如與影像相關的演算法那麼多,我不需要知道各種演算法長得什麼樣子,但我知道影像壓縮演算法可以幫我省下傳輸時間,影像辨認演算法可以幫我了解影像裡面有什麼物件(以上是抽象化),而這些演算法早已經有聰明的專家實作出來,我們可以透過網際網路,利用資料中心的雲端服務來使用這些演算法(這是具體化)。

這種虛實轉換,一下抽象一下具體的能力是很需要的。其實我原本沒有意識到這件事情,是有學生跟我講之後我才想到的。

他跟我說,你不覺得寫程式很抽象嗎?在談的都是一些很抽象的東西,讓很不習慣這種思維方式的他覺得學習的很辛苦。

我仔細想想還真的滿抽象,例如說我們談到 IoC 控制反轉或是 DI 依賴注入的時候,它的目標就是在於把某一層抽出來而減少依賴,在程式的領域中這類型的方法用的可多了,對於工程師來說司空見慣,但對新手來說,根本看不懂你們在做什麼。

他們懂的使用迴圈、判斷式跟函式,可是卻不知道怎麼把它們組裝在一起,也不知道該怎麼把自己腦海中的解法(抽象)轉換成程式碼(實體)。或者是看了參考解答的程式碼,卻沒辦法轉換回去拼湊出原本的解法。

我記得有某陣子大家一直在討論國小國中的程式教育課程,我覺得這要看課綱的設計,如果能學習到 Computational Thinking,那我舉雙手贊成。要認清的一點是寫程式只是手段,目的是要讓他們能學習到背後的這個計算思維。如果只是停留在表面的「會寫程式碼就好」,那就沒什麼必要教了。

有了計算思維,就算不會寫程式也沒關係。因為這些思考方式不只有寫程式能用到,放到其他類似的領域也一定用得到。

所以如果我想學程式,到底要學什麼才好?

就照著你原本的計畫來學就好,但要記住兩個重點:

  1. 計算思維
  2. 前因後果

簡單來說就是一句話:要一直不斷的思考。

舉例來說,當你學習到 CRUD 的時候,你要去想說為什麼是這四種?只有這四種就夠了嗎?他們搭配的 HTTP 方法分別是什麼?如果用別的動詞可以嗎?為什麼 HTTP 的方法有這麼多種?那些方法的用途是什麼?可以舉一些實際案例嗎?

或者是當你在學習一道新的題目,你可以先用紙筆列出解法的步驟,接著把每個一步驟都轉換為程式碼,一步一步慢慢試,用 debugger 一行一行執行,確保每一行的結果都跟你心裡想的一樣,或是也可以加 log 來驗證。

多想幾次,然後把題目做一些變化,試著讓自己嘗試一些不同的解法,自然就會對這種思考模式越來越熟練。

當你學到 session 的時候,也不能停留在會用的層次,你得知道它背後的原理大概是如何,可以自己利用資料庫或是 in-memory 的方式實作出一個簡單的 session 機制。一但你實作過了,當你被問到 cookie 與 session 的差異時這問題就簡單得多。

或是上完物件導向之後,不能只是停留在「知道」的層次,而是要動手下去做,自己隨便寫一個物件出來,然後隨意用用看。接著再把自己寫的程式碼重構,感受一下前後的差別在哪邊。

當你在學習一個新的概念的時候,先確保自己有基礎的理解,再來透過動手練習慢慢更加理解這項概念,最後試著自己實作出類似的東西,你會發現自己的理解程度又更上了一層樓。

而前因後果指的是當你在學習一項新的技術時,可以問自己以下五個問題:

  1. 這個技術出現以前是什麼樣子?
  2. 那時候碰到什麼樣的問題?
  3. 這個技術的出現如何解決問題?
  4. 所以這項技術應該如何使用?
  5. 跟以前的解法比起來,差別在哪裡?有什麼優缺點?

舉個例子好了,JSONP,有些人只會用它,可是卻不知道它為什麼會出現。

雖然現在大家都很熟悉 CORS 以及 Ajax 跨網域的問題,可是在很久以前,例如說還需要支援 IE7 的年代,它是不支援 CORS 的。那你想在 IE7 上面做跨網域的 Ajax 怎麼辦?你可以選擇用 Flash,或者是自己寫個 proxy server 做轉介,否則別無他法,但無論用哪一個解法都滿麻煩的。

直到 JSONP 的出現。

什麼是 JSONP?大家還記得 <script> 標籤嗎?這個標籤的 src 是不受網域限制的,你可以去載入其他 domain 的 script,例如說 Google Analytics 啦,或是一些 Public CDN 的資源。</script>

既然這個標籤可以跨網域,假如載入的 script 內容可以幫我把資料帶回來,我不就達成了跨網域的 Ajax 嗎?因此我只要帶一個 callback 的參數到 server,server 輸出資料時去呼叫我帶的那個 function,不就 ok 了嗎?

舉例來說,我在頁面中安插一段 code 是:
<script src=””https://example.com?callback=receiveData””></script>

然後 server 的 response 是:receiveData([id: 1, name: ….])

這樣子我只要在我的程式碼裡面寫了個 receiveData 的 callback function,我就能夠非同步的去拿到 Server 帶過來的資料了!

這就是 JSONP,就是這麼簡單。有了 JSONP 之後,就能夠在 IE7 也順利地使用跨網域的 Ajax 了!不過因為是使用這種方式,所以只支援 GET 方法。

回過頭來看我前面提的那五個問題,上面的這段你可以找到全部的答案。當你學習 JSONP 的時候,不只要學他怎麼用,更重要的是學習你為什麼要用以及不用會怎麼樣

如果你能夠回答出那五個問題,就能夠保證你對這項技術有一定的理解,而不是只會語法只會工具,不會被問到一點相關的問題就被問倒。

所以我才說你要一直不斷地去思考,不斷去想說我今天學的這個是為了什麼,當你的知識累積的越來越多,你就會發現一大堆東西都驚人的相似,學習一項新東西的時間也會變快。

例如說三大前端框架我只會 React(雖然他嚴格來講不算框架,但如果把整套生態系涵蓋進去,我覺得也可以稱作是框架了),但因為我知道它的核心概念,我知道它想解決的問題,所以假如我今天進了一間寫 Vue 或是寫 Angular 的公司,我也能夠保證在相對短的時間內可以快速上手(上手歸上手,要到熟練或是精通還是需要一定時間)。

因為原理都差不多,只是實作換了、語法換了、生態系的工具換了。但如果你只學到表面,你就會覺得怎麼又要學一整套新的框架,怎麼永遠都有學不完的東西。

工具只是敲門磚,思維模式才能讓你走得長久

當然,對非本科系想轉職的初學者來說,工具還是必要的,你還是必須要會那些基本的技能,才能符合公司徵才的需求。

但要記住的是,更重要的東西是背後的那些思維模式,體現在你要怎麼解決一個問題,體現在你對一項技術到底只是會用,還是真的理解它在幹嘛,這些才是長期的發展中最重要的。

以我個人的例子來說,我一直以為自己職涯比較順遂是因為寫程式的能力,但我後來漸漸發現似乎不是這樣,如果你要論寫 code 的話,有一大堆人寫的 code 比我多比我好。

但是當今天真正要下去解決一個問題時,寫 code 反而是最後一件事情。在寫 code 以前,你必須要先去找到問題的癥結點,反覆確認這是造成問題的主因,再來是你要評估解法,有些解法可能不用寫 code,寫一些文件就可以解決了,當你把前面的事情都搞定以後,最後才是寫程式。

我會寫 code,但我不只會寫 code。

對我來說,怎麼樣去解決一個問題,怎麼試圖去看透問題的本質並且衡量各種解法的優缺點,那才是真正的核心競爭力。