本章為通用理論。各語言的具體實踐請參考下方「在各語言中的實踐」。

程式設計範式(Programming Paradigm)是面對問題時的基本切入方式。理解每種範式的核心思想,才知道什麼時候選哪一種,寫出來的程式才會自然。

範式概覽#

範式核心思想典型語言
物件導向(Object-Oriented Programming, OOP)封裝、繼承、多型Java, C++, Python
函式式(Functional Programming, FP)不可變性、純函式、高階函式Haskell, Scala, Clojure
泛型(Generic Programming)型別參數化、程式碼重用C++, Java, Go
過程式(Procedural Programming)順序執行、狀態修改C, Pascal

現代語言往往支援多種範式並存。重點不是「我寫的是 OOP 還是 FP」,而是「這段需求最適合哪種抽象」。


物件導向#

以「物件」作為基本單元,把資料和操作綁在一起。

三大核心特性#

1. 封裝(Encapsulation)

  • 資料和行為綁在一起
  • 隱藏內部細節,只暴露介面
  • 降低耦合度

2. 繼承(Inheritance)

  • 子類繼承父類的屬性與方法
  • 達成程式碼重用
  • 建立型別層次結構

3. 多型(Polymorphism)

  • 同一個介面,多種實作
  • 執行時動態綁定
  • 提高彈性與可擴充性

對介面設計,而非對實作#

Program to an interface, not an implementation.

這是 OOP 的黃金法則:依賴抽象(介面),而不是依賴具體型別。

Go 介面範例
type Country struct {
    Name string
}

type City struct {
    Name string
}

type Stringable interface {
    ToString() string
}

func (c Country) ToString() string {
    return "Country = " + c.Name
}

func (c City) ToString() string {
    return "City = " + c.Name
}

func PrintStr(p Stringable) {
    fmt.Println(p.ToString())
}

// 使用
d1 := Country{"USA"}
d2 := City{"Los Angeles"}
PrintStr(d1)
PrintStr(d2)

CountryCity 是領域型別,PrintStr 是控制邏輯,兩者透過 Stringable 解耦。任何實作 Stringable 的型別都能餵給 PrintStr


函式式#

強調以純函式(Pure Function)與不可變資料為基礎構建程式。

核心概念#

1. 純函式(Pure Function)

  • 相同輸入永遠得到相同輸出
  • 沒有副作用(不修改外部狀態)
  • 易於測試與推理

2. 不可變性(Immutability)

  • 資料一旦建立就不能修改
  • 要改就建立新的副本
  • 避免共享狀態引起的 Bug

3. 高階函式(Higher-Order Function)

  • 函式可以當參數傳遞
  • 函式可以當回傳值
  • 達成更高層次的抽象

Functional Options 模式#

函式式抽象在工程上的代表案例,在 Go 社群非常流行。

Functional Options 模式詳解

問題場景: 建立物件時要設定多個可選參數。

傳統方案的問題:

// 方案一:多個建構函式
func NewDefaultServer(addr string, port int) (*Server, error)
func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error)
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error)
// 組合爆炸...

// 方案二:Config 物件
func NewServer(addr string, port int, conf *Config) (*Server, error)
// 呼叫端要判斷 nil 或空 Config{}

Functional Options 解法:

// 定義 Option 型別
type Option func(*Server)

// 各種設定函式
func Protocol(p string) Option {
    return func(s *Server) {
        s.Protocol = p
    }
}

func Timeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.Timeout = timeout
    }
}

func MaxConns(maxconns int) Option {
    return func(s *Server) {
        s.MaxConns = maxconns
    }
}

// 建構函式
func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {
    srv := Server{
        Addr:     addr,
        Port:     port,
        Protocol: "tcp",
        Timeout:  30 * time.Second,
        MaxConns: 1000,
    }
    for _, option := range options {
        option(&srv)
    }
    return &srv, nil
}

// 使用
s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))

優點:

  • 呼叫端直覺、可讀性高
  • 想加新設定不需要動既有 API
  • 沒有 nil / 空結構的判斷負擔
  • 自帶說明(函式名即是設定意圖)

泛型#

透過型別參數化,把演算法與資料結構獨立於具體型別。

核心價值#

  • 程式碼重用:同一套邏輯處理不同型別
  • 型別安全:編譯期就抓到型別錯誤
  • 效能:避免執行時的型別檢查與轉型

常見應用:集合(List、Map、Set)、演算法(排序、搜尋)、工具型別(Optional、Future)。


控制反轉與委託#

控制反轉(Inversion of Control, IoC)的核心是:把控制邏輯從業務邏輯中抽離出來

不要在業務邏輯裡寫控制邏輯。讓業務邏輯依賴控制邏輯,而不是反過來。

比喻: 開關與電燈

  • 開關是控制邏輯
  • 電燈是業務邏輯
  • 不要在電燈裡實作開關。把開關抽成介面(協定),讓電燈依賴它
控制反轉範例:Undo 功能

傳統做法(控制邏輯依賴業務邏輯):

type UndoableIntSet struct {
    IntSet    // 嵌入業務邏輯
    functions []func()
}

func (set *UndoableIntSet) Add(x int) {
    // Undo 邏輯與 IntSet 業務邏輯緊密耦合
    if !set.Contains(x) {
        set.data[x] = true
        set.functions = append(set.functions, func() { set.Delete(x) })
    }
}

控制反轉做法(業務邏輯依賴控制邏輯):

// 定義控制邏輯(協定)
type Undo []func()

func (undo *Undo) Add(function func()) {
    *undo = append(*undo, function)
}

func (undo *Undo) Undo() error {
    // ... 純粹的 Undo 邏輯
}

// 業務邏輯依賴控制邏輯
type IntSet struct {
    data map[int]bool
    undo Undo  // 嵌入控制邏輯
}

func (set *IntSet) Add(x int) {
    if !set.Contains(x) {
        set.data[x] = true
        set.undo.Add(func() { set.Delete(x) })
    }
}

好處: Undo 不再綁定特定業務型別,任何資料結構都能套用。


各範式的適用場景#

場景推薦範式原因
建模複雜業務領域物件導向類別自然對應現實世界的概念
資料轉換與處理函式式純函式與不可變性讓資料流清晰
通用資料結構與演算法泛型型別參數化達成高度重用
簡單腳本與工具過程式直接、最少抽象
並行程式設計函式式不可變性避開競爭條件

實務上幾乎都是混合使用:類別建模業務、Stream 處理集合、泛型寫容器。語言層面早就不再壁壘分明。


在各語言中的實踐#

  • Go 語言:以過程式為基底,透過介面與結構體嵌入達成「組合優於繼承」的輕量物件導向;以通道(Channel)與共常式(Goroutine)體現 CSP(Communicating Sequential Processes)併發模型。
  • Kotlin Coroutines:JVM 上的 OOP + FP 融合,並用 suspend function 與結構化併發解掉回呼地獄。
  • 語言實作:從 VM、bytecode、GC 到 JIT,看這些範式在語言層底下是怎麼被實現的。