Published on

《A Philosophy of Software Design》心得 II — 透過模組設計降低軟體複雜度,從介面開始

目錄

在上一篇《A Philosophy of Software Design 心得 I — 寫出複雜度低的軟體》中談到,要能夠在程式逐步規模化的同時,讓程式好維護、好擴張,必須要降低軟體的複雜度。那篇也談了常見的軟體複雜度過高的病徵。而這篇將會延續《A Philosophy of Software Design》一書當中提到的觀點,進一步討論如何透過「模組設計」降低軟體複雜度,以及要做好軟體的模組設計,要先從做好介面設計開始!假如對於「複雜度」這個概念不太熟悉的朋友,建議先回去看上一篇心得,在看這篇會比較清楚唷~

什麼是模組設計?

如前面提到,在軟體設計上,有一種降低複雜度的方式叫做模組設計 (modular design);所謂模組設計就是把龐大的程式拆解成不同的模組,這些模組彼此獨立 (至少理想上要這樣),然後當要執行軟體時,再把需要的模組組合起來。這麼做每個開發者要面對的,就不是整個系統的複雜度,而是僅用面對單一模組的複雜度。

舉例來說,如果要打造一個電商網站,從系統的角度來看是非常複雜的,電商網站需要成列商品、需要有購物車功能,等使用者挑選完商品後,還需要有結帳與金流的功能;甚至在使用者看不到的地方,會需要有庫存管理、消費者的會員管理等等不同的功能。可以想像假如要打造這樣完整的一個網站,當功能越多時,越有可能讓複雜度提高。

具體來說,上一篇談到讓軟體複雜度過高的三大病徵,以電商網站來看系統變大後很可能會:

  • 改動某處時需大量改動其他部分 (例如購物車與結帳有相關聯,可能改了一邊導致要改另一邊)
  • 讓其他開發者認知負擔過重 (網站功能一多就代表要花更多時間理解各個功能與功能間的關係)
  • 不知道不知道 (功能越多且彼此有關聯時,會有很多這類的陷阱)

這時模組化的設計就能有效處理這些問題。假如上面的各個功能都變成一個個模組,那麼購物車功能歸購物車模組,結帳與金流功能歸結帳與金流模組。負責開發購物車的工程師,不須用擔心改了什麼導致金流炸掉。這聽起來非常的理想 (但多數時候,這會止於理想……)。

因為在現實中模組之間很難做到完全獨立,更常見的是模組間相互依賴的問題。就像前面提到,購物車中的產品最終要被結帳的話,那即使拆成模組,兩邊也很難完全做到獨立沒關聯。這種相互依賴的問題,就是常聽到的「相依性」,也是為何資深的工程師前輩們,常常會耳提面命說要注意相依性問題,畢竟當相依性提高,整體系統的複雜度就會提高。

要能降低軟體設計的複雜度,其中一個策略就是管理好軟體的相依性。要管理模組的相依性,有個一定要記住的原則「介面要窄、功能要深」。這個原則符合在《A Philosophy of Software Design》一書中,又被稱為深模組 (deep module) 。

介面要窄

讀到這裡你可能有點一頭霧水,「介面要窄、功能要深」是什麼意思? 這邊先用自排車、手排車的例子來講解介面要窄的意思。

對自排車與手排車來說,介面就是車子跟人互動的地方。從這個定義下去討論,顯然自排車的介面比較窄,因為駕駛員要操作的東西與步驟比較少;以換檔為例,開自排車基本上不太用做什麼事,要開的時候換 D 檔,要倒退打 R 檔,要停車時用 P 檔,在換檔的過程中,駕駛不用知道背後發生了什麼事,輕鬆簡單就能完成。

然而手排車則不然,手排車的介面就寬了許多,換句話說,在開車時駕駛要做的操作多很多。以換檔來說,要踏離合器、要踩煞車踏板,換檔後還要加油門,基本上駕駛完全需要手腳並用才能完成。這種對操作者來說,需要碰觸到許多東西才能完成任務,就是介面太寬的跡象。

當介面窄 (例如自排車),使用的人可以無腦操作,不需用有太多擔心;但介面寬,則需要有非常多動作,要擔心的面向也因此變多許多。當介面寬,就容易讓人有出錯的機會。以車子為例,介面最寬的代表是 F1 賽車,F1 車手在賽車的同時,要在短時間內做非常多的操作;這導致可以從歷史上看到,很多車手換車隊後,需要適應新的車 (適應新的複雜介面),導致剛換隊前期都會表現比較差。

在看完車的案例,回到軟體設計上也是相同道理,如果你寫了某個模組,理想的狀況是,要使用該模組的人不太需用做太多事就能用,那就是介面窄;但假如要用該模組的人,要做非常多的操作,那就是介面太寬了。就跟開車一樣,軟體也是介面太寬操作就容易出問題,介面窄則會像自排車一樣,讓操作的人更輕鬆。介面寬就像自排車一樣。假如你設計出來的模組,讓其他工程師操作時會發生類似打換檔忘了踏離合器的狀況,那就是介面太寬了。

假如自排車、手排車描述得沒辦法讓你馬上懂,希望上面這張比較圖能讓大家更直覺理解。在介面要窄這個原則,軟體模組設計,跟使用者介面 (UI) 設計,是異曲同工之妙。左右兩個遙控器可以達到的功能相似,但右邊的使用者介面顯然太複雜了。假如你設計的軟體模組,對其他開發者來說,就像是在用右邊的遙控器,那或許你該多想想,如何把模組設計成左邊的那樣,提供的功能不變,但把複雜度藏起來,簡單又好用。

功能要深

還記得上面提到,除了介面要窄外,模組設計的另一個要點是功能要深。最好的模組就是能提供強大的功能,但對接的介面不暴露過多複雜度。在書中,作者提到一個深模組的典範,就是 Unix 作業系統的檔案 I/O。

我們一起來看看該模組的介面。該模組的介面基本上就是這五個項目,打開檔案 (open)、讀檔案 (read)、寫入內容到檔案 (write)、調整檔案讀寫位置 (lseek),以及關掉檔案 (close)。

int open(const char* path, int flags, mode_t permissions);
ssize_t read(int fd, void* buffer, size_t count);
ssize_t write(int fd, const void* buffer, size_t count);
off_t lseek(int fd, off_t offset, int referencePosition);
int close(int fd);

對於要使用該模組的人來說,要接觸到的基本介面就是這五項,是不是非常的精簡、介面非常地窄呢? 事實上,這五個操作背後,是靠數千行程碼是來達成的!之所以說這是深模組的典範,是因為要用的人,不用管背後許多複雜的問題,只需要簡單地用這五個操作即可。

舉例來說,你不需用知道檔案要如何在硬碟中該怎麼被呈現,才能有效率地被讀取。你不需要知道階層路徑名如何被處理,讓系統能找到對的檔案。你不需要檔案如何被快取在記憶體,藉此降低從硬碟讀取的次數。你也不用知道如何把不同的存儲裝置,例如硬碟、隨身碟等與檔案系統整合。

上面這些都還只是冰山一角。作業系統的檔案 I/O 還有非常多需要考慮、處理的事情。但 Unix 作業系統之所以能被稱為好的模組設計,正是因為他讓用的人不用去處理那些複雜的事情,只需簡單地跟介面互動即可。就像你不需用懂微波爐背後的物理、機械原理,只需用簡單按幾個按鈕就能操做一樣。

小結

身為網頁與行動裝置的前端工程師,在工作中我最常遇到的介面莫過於各種 API (沒錯 API 的 I 正是介面的意思)。在讀模組化設計這個章節時,我不禁想到之前有串過某些 API,讓人能清楚且簡單地串接,同時提供強大的功能,現在明白這正是符合介面窄、功能深的原則。

不過同時也想起,過去有串過一些第三方 API,在串的過程踩到各種坑,甚至有許多狀況是文件中沒寫的,導致我忽略該注意的點;當時內心咒罵該 API 文件寫得不清不楚,但現在重新思考,或許真正的問題出在該 API 的介面設計太寬了。

圖片來源:《A Philosophy of Software Design》

總結來說,希望大家讀完上述摘要與心得,都有掌握深模組的定義。要做好模組設計來降低軟體複雜度,就從介面要窄、功能要深開始吧!最後在《A Philosophy of Software Design》當中有這個圖示,協助我們視覺化理解,推薦大家多咀嚼咀嚼。