一文看懂三種 Programming Paradigm 對於軟體架構的影響

Ryan Yang
Ryan Yang
Dec 4, 2019 · 6 min read

這三個 Paradigm 分別是 Structured Programming,Object Oriented Programming 以及 Functional Programming。這三種 Paradigm 的出現,其實並不是給我們帶來什麼新功能,反而是去規範我們什麼不能做。(本篇文章是從 Clean Architecture 一書中整理與延伸)

那就讓我們來看看吧!


Structured Programming Imposing discipline on direct transfer of control (對程式控制權的直接轉移進行了限制)。一切從 Dijkstra (對!就是 Dijkstra’s Algorithm 發明者) 於 1968 年提出 “goto 有害論” (完全無限制的 goto) 講起 (https://en.wikipedia.org/wiki/Considered_harmful),並說明應該要以 Structured Programming 替代,也就是循序、分支 (if/else/switch) 及重複 (while/for)。這是因為在當時 goto 的過度使用會造成可讀性變得很差,當專案越來越大的時候,複雜度也會急劇地上升,以致於 Programmer 對程式已經失去了控制也就無法驗證程式的對錯了。

Dijkstra 認為 goto 的濫用造成一個 Module 沒有辦法被遞迴地拆成更多更小的可證明單元,也就無法用 divide-and-conquer 來證明一段程式的正確性。在 Dijkstra 之前已經有人證明任何程式都能用循序、分支、重複結構來完成,所以只要 Dijkstra 能夠用數學的方式證明循序、分支、重複結構是對的,那麼就可以證明整個程式是對的啦 (divide-and-conquer),而 Dijkstra 確實也證明出來了 (循序和分支:枚舉法、重複:數學歸納法),於是第一個正式的數學方法來證明程式的正確性誕生了。但是每次如果都用數學證明的話那也太麻煩了,所以其實一直到今天我們都沒有用這樣的方式來證明程式的正確性 (耗時費工呀),而今天我們用的方式是比較偏向證明科學的方式,也就是證偽 (對比於證實),想辦法去證明理論是錯的 (因為無法用推論的方式證明例如萬有引力),那我們如何用證偽的方式來證明程式是對的呢?其實就是我們習以為常的寫測試啦!

想想我們是不是寫了很多測試,只要程式能夠通過這些測試,我們就 “假定” 這段程式沒問題呢?總結下 Structured Programming 帶給我們的就是可以將功能進行降級拆解成可證偽的單元 (就是寫測試啦) 的思維 (function、module、service),不過對一般的 Programmer,這樣的思想應該已經跟呼吸一樣自然啦!


Object Oriented Programming:imposeing discipline on indirect transfer of control (對程式控制權的間接轉移進行了限制)。我們常看到大家在說 Object Oriented Programming 就是隱含了 Encapsulation (封裝)、Inheritance (繼承) 和 polymorphism (多型) 這三個概念。

先來看封裝,由於 OOP 用了 Class 有效地封裝了 Data 和 Method,所以我們常覺得封裝是 OOP 的特性,但封裝這個概念其實並非 OOP 獨有,早在 C 語言 (不是 OOP) 就已經透過在 header file 聲明 Struct 和函數,讓使用者使用時無法去得知結構的內容以及函數實作細節,可說是比現今所謂 OO 語言像是 Java 有更好的封裝性,以 Java 為例,沒有 header file 而是直接將聲明和實作放在一起,其實比起 C 語言更削弱了封裝性。

再來談繼承,簡單來說就是可以讓我們 reuse code、覆蓋或延伸,其實在 C 語言的時候也有一些 Hacky 的方式可以做到,也是在還沒有 OOP 之前就已經經常被這樣使用,不過雖然繼承也不是 OOP 的創新之舉,但是 OOP 讓我們在做繼承這件事情的時候相當便利。

最後就是多型,C 語言有沒有多型的概念呢?答案也是有的,就是操作 Struct 中的 Function Pointer 來執行實際的函數,常見的如 I/O 相關的程式,File Struct 中定義了如 open、read、write 等等 Function Pointer,而不同 I/O Device 會實作這些 Function Pointer 所指的 Function,來讓例如 getChar 這個函數透過操作 File Struct 的 Function Pointer 來做到從不同 Device 拿到資料而不會依賴特定的 Device。然而我們都知道直接操作 Pointer 是一件危險的事情且不易 Debug,而 OOP 的貢獻就是創造了一個可以讓多型更加安全以及方便使用的環境,因此 Programmer 可以更加大膽安全地在軟體架構上導入所謂的 Plugin Architecture,也就是 Dependency Inversion 囉!簡單來說就是,High level function 不會再直接依賴 low level function,而是依賴一層抽象 (interface),這樣我們就可以隨時抽換底層實作,不用更改 High level function 的程式,而達到了 Plugin 的效果,這表示各個組件就能夠獨立開發和部署啦!


Function Programming:Imposing discipline upon assignment (對程式中的賦值進行了限制)。FP 最核心的概念就是 Immutability:Variable 的值是不可變的!為什麼我們需要這件事情?因為常常像是 Race Condition、Deadlock、Concurrency 的問題都是因為 Variable 的值可變造成的。

然而實際上要做到全部都不可變是有困難的,成本也太高 (例如記憶體容量) 所以在軟體架構上也不會這麼做。實際的作法是會把軟體中不可變 (大部分的邏輯所組成的純函數) 和可變的部分 (一些狀態變量) 拆開,不可變的部分彼此透過溝通來去修改可變的部分,並且用合適的方式來保護可變量 (例如 compare-and-swap)。

最後我們來思考下,假使今天沒有成本考量,例如存儲和處理能力無限大的時候,我們的系統是不是就都可以不用維護可變量,成為一個真正的函數式系統呢?這個答案就是 Event Sourcing,例如你的存款好了,我們今天不要用一個變量來存你的存款數字,而是將每次的 Transaction 給記錄下來,這樣當你要查詢存款的時候,我們只要把所有曾經的 Transaction 都取出來進行計算,就可以得到目前的存款數字了。當然隨著時間,這樣的處理方式會越來越慢 (越來越多筆),也會成本越來越高 (計算時間更久、更多空間要拿來儲存)。

Event Sourcing 帶來的好處是可以留下每個時間點的狀態並進行回朔、由於都是寫入事件所以理論上不會有 Deadlock、事後也能對歷史資料進行分析等等,所以在一些情境下,還是可以使用 Event Sourcing 來達到這些好處。


由以上可以發現,這三個 Paradigm 其實也不是 Mutual exclusive,而是在軟體開發的演進道路上,人類逐漸學習到什麼事情是不該做的。

Ryan Yang
Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade