唸研究所開始當助教,偶而會有學弟妹問:怎樣寫好程式?老實說,這是個大哉問,連我學開發軟體這麼久,我也只能回答他們:多培養自己釐清問題、拆解問題、解決問題與抽象化的能力。但他們通常只會一臉狐疑看著我,感覺我說的話好抽象。事實上,這也不是我第一個這樣說的,有句軟體工程諺語是這樣說的:
Why is it that some software engineers and computer scientists are able to produce clear, elegant desings and programs, while others cannot? Critical to these questions is the notion of abstraction. (為何有些軟體工程師與電腦科學家能夠產生清楚而且優美的設計與程式,但其他人卻不能?關鍵在於抽象觀念。) — Jeff Kramer CACM 50(4) 2007
釐清問題
釐清問題是讓自己能解決對的問題的第一步,當一開始什麼都不想,根據使用者或顧客一個模糊的需求或是想法就開始埋頭苦寫,即便程式寫好了也不一定解決對方真正的問題,因此有一個說法是找出問題背後的問題,使用者提出一個問題,通常是在現實生活中遇到困難,但在描述時,卻不一定能精確的描述問題 (這當然不能當著顧客的面說),或是把背後的問題給描述出來,所以在理解需求的過程中,是要去幫使用者找出真正的問題。不過,我不打算在這裡說明釐清問題的方法。
拆解問題
當釐清真正的問題後,問題有時很大,有時很小,問題小也許就可以開始找尋解法,但問題很大時,會像毛線球般糾結很難好好處理,所以應該先試著將大問題拆解成小的問題,然後再根據每個小的問題去尋找解法。舉個例子,雖然現在網路就像空氣一樣,幾乎成為生活中不可或缺的一個元素,即便如此,我們還是可以問一個問題:當我們輸入一個網址後,電腦是如何呈現這個網頁?
這樣一個大問題可以被拆解成好幾個小問題:
- 瀏覽器是怎麼知道一個網址對應到網路上哪一個伺服器?
- 瀏覽器的請求是如何送到伺服器的?
- 當伺服器知道某人想看某個網頁時,網頁是以什麼形式回到當初請求的電腦?
- 當瀏覽器收到伺服器的內容又要如何呈現網頁?
而上述這些問題其實都還很大,還可以再被拆成更多小的問題,在過去許多人的努力下,定義成一個
七層的 OSI 網路模型,每一層都提供一個功能(或換個說法解決一個特定問題),例如:
- 屬於應用層的 HTTP 協定,讓伺服器能知道使用者想要看什麼文件(網頁)並回傳指定的文件
- 屬於傳輸層的 TCP 協定,建立瀏覽器與伺服器之間一個虛擬連線 [1],並負責確保傳輸資料的完整性
- 屬於網路層的 IP 協定,為網路上每個節點提供地址,並負責將資料在眾多網路節點中繞送到正確的節點
- 屬於實體層的 WiFi 協定 [2],負責將電腦的數位訊息能夠在空氣中用電波傳輸,並檢查接收的數位訊號完整性
解決問題
事實上,如何解決問題或是如何邏輯思考?與電腦無關,在沒有電腦之前,我們有數學公式、物理公式、化學公式與機械等,很多的問題其實也都能被解決,只是可能需要很大量的人,或是無法快速的得到想要的結果,因此,當要用電腦解決問題時,需要的是計算思維 (computational thinking),讓解決問題的方法是可以用電腦去計算的,就像前陣子很熱門的Alpha Go圍棋大戰,
先要做的是替圍棋找出一個模型讓電腦能夠計算,這其實需要的就是抽象化能力。
當問題能被計算,但可能不夠快,例如下一步棋需要一天或一個小時,這時候需要另一種演算法思維 (algorithmic thinking),透過特殊設計的資料結構,以及找出能讓
電腦做更少的計算就能得到結果的演算法,讓電腦能更快的解決問題,類似的例子像是影像壓縮和解壓縮是個抽象概念,可以用更少的網路頻寬傳送更高畫質的影片,而具體的演算法,H.265則可以比H.264有更好的效率與影片品質。
抽象化
說了這麼多,終於又回到抽象化這個詞,抽象化可以用在好幾個不同層面,像剛剛提到的 OSI 網路模型和影片壓縮都是提供抽象概念。但實際寫程式時,抽象化是讓程式容易閱讀的關鍵,畢竟大部分時間讀程式的是人而不是電腦,所以這讓我想起一句話:
Any fool can write code that a computer can understand. Good programmers write code that humans can understand. (隨便找個傻瓜都能寫出電腦能懂的程式碼。好的程式設計師寫人能看得懂的程式碼。) — Martin Fowler
當軟體持續開發,維護程式碼比開發新程式碼要更傷腦筋,如何讓後續的開發者讀程式像是讀文章般容易懂,重要的就是能用問題 domain 中的術詞 (term) 來描述程式。舉個例子,假設有一個 my-book-store 的網路書店,提供若干 REST API 讓客戶端可以使用:
取得Isaac Asimov的科幻類作品
GET http://my-book-store.com/books?category=science-fiction&author=Isaac%20Asimov
在編號19333910書籍新增一筆評論
POST http://my-book-store.com/books/19333910/comments
修改編號13332144書籍的資訊
PUT http://my-book-store.com/books/13332144
刪除編號19333912的第12筆評論
DELETE http://my-book-store.com/books/19333912/comments/12
以 Java 來說,想要使用 REST API,客戶端可以用 Socket 建立連線到 my-book-store 建立連線到 my-book-store,準備好 HTTP 協定相關的標頭與內容,然後傳送給伺服器然後再取得結果,但我想很多人都知道:Socket 是作業系統或 JVM 提供給軟體開發者操作 TCP 的 API,對於我們要做的功能來說太低階了。事實上,Java有提供 HttpURLConnection 讓開發者可以直接建立 HTTP 連線,因此可以用如下的程式呼叫 REST API 取得 Isaac Asimov 的科幻類作品(範例程式中未處理所有可能拋出的例外)。
可以用下面的程式呼叫 RES API 在編號 19333910 書籍新增一筆評論,但寫到這,是否發現有太多與上面重複的程式?而這些程式其實還是在處理很多低階的 IO 處理。也許需要另一層抽象可以跟 REST 伺服器溝通,而不用去處理實作細節。
所以,我們可以觀察一下上述四個 API 不同的地方與相同的地方,可以發現如下,我們的抽象層需要能指定伺服器的位置 (host),API 的路徑 (path),路徑上可能有參數可以設定 (path parameters),額外可以夾帶查詢參數 (query parameters),最後,最重要的是可以指定呼叫的方法 (HTTP Method)。
HTTP Mehotd https://host/path/[{path parameters}][?query parameters]
因此,我們可以根據剛剛的描述設計一個 RestClient,直接來看例子,下面直接以 RestClient 改寫 getBooks(category, author) 和 postComment(bookId, comment) 函式,是否與原先的版本讀起來,在語意上是不是有完全不同的感受?
有了 RestClient,要實作 updateBook(bookId, updates) 跟 deleteComment(bookId, commentId) 是不是也變得很容易,或許,有人覺得只是透過封裝,可以重複使用程式碼,但對我來說,這並不是主要的目的,在看兩個例子,讀起來是否開始有感覺了呢?透過結合 domain 的術語與
Fluent Interface,其實我們已經完成了一個為 REST API設計的
Domain Specific Language [3]。
當軟體越開發越大,為軟體進行模組化是絕對必要的,而抽象化也是模組設計所需要的一項很重要的能力,我們可以將剛剛四個函式包裝成一個 MyBookStoreService,因此,使用起來就如下所示,讀起來語意上又提高一個層級,對使用者來說,也已經不知道底層使用的是 REST API。
如同 OSI 網路模型一樣,軟體的開發是透過一層又一層的抽象堆疊完成,解決各種不同層次的問題 (前提是問題已經先被拆解),因此有個說法:
All problems in computer science can be solved by another level of indirection. — David Wheeler
但要怎麼提升抽象化的能力呢?以自己的經驗,首先,先試著讓自己能寫出有條理的文章,因為對現在的我來說,寫程式其實是在寫文章,寫讓其他人看得懂的文章,所以透過寫文章訓練自己的如何思考與如何整理思緒,是非常有幫助的(這真要感謝我的指導教授在我寫碩博士論文時的訓練)。再來,多看別人好的程式碼,瞭解其中使用哪些 design principles、design pattern和 architecture pattern,以及背後使用的意圖,耳濡目染久了,就會有自己對於事物抽象化的想法 (是的,抽象化沒有標準答案的,只有合不合適解決問題與否和是否容易理解的差別)。
結語
語意的抽象化可以說從過去到現在都仍然是進行式,從最早期用打卡機寫程式,到後來可以用組合語言寫程式,到能用 C 寫程序導向的程式,演變到可以用 Java/C# 等語言寫物件導向的程式,甚至最近很流行的用 functional paradigm 的概念寫程式,每次演進都在提高程式語言的抽象程度,到最後每個 domain 都會有自己的 domain specific language,讓原先 domain 的人可以讀懂程式。
其實要開發大型的軟體,不論是在學界或在業界,真正好的軟體開發者要具備的要素還有很多,像是能與同儕溝通、引領思考、軟體工程實務 (開發流程、建構管理等) 的實踐,都會讓一個好的軟體開發者與一個差的軟體開發者在效率上差上好幾倍 (有一說是10倍,但我找不到出處),但就以平日每天在寫程式的層級來看,抽象化能力是最不可缺少的一項能力。寫了這麼多,我想如果再有人問我:怎樣寫好程式?或是怎麼能成為一個好的軟體開發者?這文章是我目前能給的最好說法,希望對想學習軟體開發的人有幫助。
如果你想毀掉一個人的一天,就給他一個程式; 如果你想毀掉一個人的一生,就教他寫程式。 (If you give someone a program, you will frustrate them for a day; if you teach them how to program, you will frustrate them for a lifetime.)
如果真是如此,那我們正在進行毀掉全國學生的計畫(笑)?
附註
- 實際上瀏覽器的電腦與伺服器之間不可能真的有一條連線,只是透過封包在網路上多個節點轉送達成類似的現象。
- 這說法不是很精確,WiFi 事實上包含了實體層與資料鏈結層,但為了方便說明,請體諒一下。
- 雖然有人覺得 Objective C 或 Swift 語言寫程式時需要寫出參數名稱很多餘,但設計DSL時,這卻常常能讓程式更像自然語言,例如:client.where("bookId", is: 19333910)。
- 文中引用很多諺語主要來自《軟體工程諺語》部落格。
這是從 Medium 搬家到方格子的第三篇文章,個人在 Medium 上有《閒談軟體設計》與《
Java Magazine 翻譯系列》兩個大主題,但畢竟翻譯系列不是我原創,當初寫信給官方也是說不會用翻譯營利,就不搬到這裡了,有興趣的就到 Medium 上看吧。接下來應該會是每周一篇的方式陸續將《閒談軟體設計》搬到方格子,至於為什麼不用匯入的方式?因為方格子目前支援 Gist 的方式,無法像圖片那樣加上標題,內文需要搭配對應的修改,稍微麻煩了點。