[Daily Questions Challenge 32]
OOP 與 FP:物件導向與函數式程式設計的深入對比
2026-06-26
![[Daily Questions Challenge 32] OOP 與 FP:物件導向與函數式程式設計的深入對比](/daily-questions-challenge.png)
⬆
程式設計有兩種主流典範:OOP(物件導向程式設計) 和 FP(函數式程式設計)。
OOP 的核心是「物件」——把資料與行為包在一起,透過物件之間的互動完成工作。FP 的核心是「函數」——把程式視為一系列資料轉換,避免可變狀態與副作用。
聽起來都很合理,但這兩種思維在實作上會帶來截然不同的設計選擇:狀態怎麼管、副作用放在哪、程式碼怎麼組合、測試難不難寫——每一個問題,兩種典範都有不同的答案。
這篇文章會逐一拆解這些差異,並以 Python 程式碼說明。
OOP 核心概念
物件導向程式設計(Object-Oriented Programming, OOP)的核心思想是:把資料與行為綁在一起,形成物件。程式由互相溝通的物件組成,每個物件負責管理自己的狀態,並透過方法對外暴露行為。
OOP 的四個基本柱子:
- 封裝(Encapsulation):把資料(屬性)與操作資料的方法包在物件內,隱藏內部細節,只暴露必要的介面。
- 繼承(Inheritance):子類別可以繼承父類別的屬性與方法,達成程式碼重用與擴充。
- 多型(Polymorphism):不同類別的物件可以用統一的介面呼叫,各自執行自己的實作。
- 抽象(Abstraction):只暴露操作的「是什麼」,隱藏「怎麼做」的細節。
Python OOP 範例:
python
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name: str):
self.name = name # 封裝:name 是物件內部狀態
@abstractmethod
def speak(self) -> str: # 抽象:定義介面,子類別決定實作
pass
class Dog(Animal):
def speak(self) -> str: # 多型:同介面,不同行為
return f"{self.name} 說:汪!"
class Cat(Animal):
def speak(self) -> str:
return f"{self.name} 說:喵~"
animals: list[Animal] = [Dog("小黑"), Cat("咪咪")]
for a in animals:
print(a.speak())
# 小黑 說:汪!
# 咪咪 說:喵~FP 核心概念
函數式程式設計(Functional Programming, FP)的核心思想是:用函數的組合描述計算過程,避免可變狀態與副作用。程式是由一系列資料轉換組成,而不是物件之間的互動。
FP 的四個核心概念:
- 純函數(Pure Function):相同輸入永遠產生相同輸出,且不改變外部狀態。純函數沒有副作用。
- 不可變性(Immutability):資料建立後不應被修改,轉換資料時產生新的資料,而非修改原有資料。
- 高階函數(Higher-Order Function):函數可以作為參數傳入另一個函數,或作為回傳值,讓程式碼更靈活組合。
- 函數組合(Function Composition):將多個小函數串接,讓資料依序流過每個函數,完成複雜的轉換。
Python FP 範例:
python
from functools import reduce
# 純函數:相同輸入,永遠相同輸出,無副作用
def add(a: float, b: float) -> float:
return a + b
# 高階函數:接受函數作為參數
def apply(fn, values: list[float]) -> list[float]:
return [fn(v) for v in values]
prices = [100.0, 200.0, 300.0]
discounted = apply(lambda p: p * 0.9, prices) # [90.0, 180.0, 270.0]
# 函數組合:filter → map → reduce 串接
total = reduce(
add,
map(lambda p: p * 0.9, filter(lambda p: p > 100, prices))
)
# filter: [200.0, 300.0]
# map: [180.0, 270.0]
# reduce: 450.0深入對比
1. 狀態管理:可變狀態 vs 不可變資料
這是 OOP 與 FP 最根本的哲學差異。
OOP 版本:購物車是一個物件,狀態隨時間改變。
python
# OOP:可變狀態
class Cart:
def __init__(self):
self.items: list[dict] = []
def add_item(self, name: str, price: float) -> None:
self.items.append({"name": name, "price": price}) # 直接修改內部狀態
def total(self) -> float:
return sum(item["price"] for item in self.items)
cart = Cart()
cart.add_item("Python 書籍", 600)
cart.add_item("鍵盤", 1200)
print(cart.total()) # 1800.0
# cart.items 已被修改,無法追溯加入商品前的狀態FP 版本:資料是不可變的 tuple,每次「新增」都回傳一個新的資料結構。
python
# FP:不可變資料
from typing import NamedTuple
class Item(NamedTuple):
name: str
price: float
def add_item(cart: tuple[Item, ...], item: Item) -> tuple[Item, ...]:
return cart + (item,) # 不修改原本的 cart,回傳新的 tuple
def total(cart: tuple[Item, ...]) -> float:
return sum(item.price for item in cart)
cart: tuple[Item, ...] = ()
cart_with_book = add_item(cart, Item("Python 書籍", 600))
cart_with_both = add_item(cart_with_book, Item("鍵盤", 1200))
print(total(cart_with_both)) # 1800.0
# 原始的 cart 從未被修改,任何中間狀態都可保留OOP 版本的 cart.items 在呼叫 add_item 後就被永久改變;FP 版本每次都產生一個新的 tuple,可以保留任何中間狀態,更容易追溯與除錯。
2. 程式碼組織:類別與繼承 vs 函數組合
OOP 版本:透過繼承擴充行為,形成固定的類別階層。
python
# OOP:繼承擴充
class Order:
def __init__(self, amount: float):
self.amount = amount
def final_price(self) -> float:
return self.amount
class DiscountedOrder(Order):
def __init__(self, amount: float, discount_rate: float):
super().__init__(amount)
self.discount_rate = discount_rate
def final_price(self) -> float:
return self.amount * (1 - self.discount_rate)
class TaxedOrder(Order):
def __init__(self, amount: float, tax_rate: float):
super().__init__(amount)
self.tax_rate = tax_rate
def final_price(self) -> float:
return self.amount * (1 + self.tax_rate)
print(DiscountedOrder(1000, 0.1).final_price()) # 900.0
print(TaxedOrder(1000, 0.05).final_price()) # 1050.0
# 若需要「先折扣再加稅」,就必須再定義一個 DiscountedAndTaxedOrderFP 版本:透過函數組合,將折扣與稅金各自做成純函數,再自由組合。
python
# FP:函數組合
from functools import reduce
from typing import Callable
def apply_discount(rate: float) -> Callable[[float], float]:
return lambda amount: amount * (1 - rate)
def apply_tax(rate: float) -> Callable[[float], float]:
return lambda amount: amount * (1 + rate)
def compose(*fns: Callable[[float], float]) -> Callable[[float], float]:
return lambda x: reduce(lambda v, f: f(v), fns, x)
# 自由組合:先打折,再加稅,不需要新的類別
discounted_then_taxed = compose(apply_discount(0.1), apply_tax(0.05))
print(discounted_then_taxed(1000)) # 1000 * 0.9 * 1.05 = 945.0
# 只打折,不加稅
just_discounted = compose(apply_discount(0.2))
print(just_discounted(1000)) # 800.0OOP 的繼承會形成固定的類別階層,增加新的「組合規則」需要新增類別;FP 的函數組合像樂高積木,可以任意拆裝,彈性更高。
3. 副作用處理
副作用(Side Effect) 是指函數修改了自身範圍以外的狀態——例如修改全域變數、寫入資料庫、列印到 console,或修改傳入的物件。
OOP 版本:方法通常直接修改物件狀態,業務邏輯與副作用混在一起。
python
# OOP:副作用與業務邏輯耦合
class ReportService:
def __init__(self):
self.log: list[str] = []
def calculate_and_log(self, orders: list[float], discount: float) -> float:
total = sum(o * (1 - discount) for o in orders)
self.log.append(f"計算完成,金額:{total}") # 副作用:修改 self.log
print(f"[LOG] 計算完成,金額:{total}") # 副作用:印出到 console
return total
service = ReportService()
result = service.calculate_and_log([100.0, 200.0, 300.0], 0.1)
# 「計算」與「記錄」綁在同一個方法,無法單獨測試純計算邏輯FP 版本:純計算與副作用明確分層,副作用集中在程式最外層邊界。
python
# FP:純計算與副作用分離
def calculate_total(orders: tuple[float, ...], discount: float) -> float:
return sum(o * (1 - discount) for o in orders) # 純函數,無副作用
def build_log_message(total: float) -> str:
return f"計算完成,金額:{total}" # 純函數,只產生資料,不輸出
# 副作用集中在最外層處理
orders = (100.0, 200.0, 300.0)
total = calculate_total(orders, 0.1) # 純計算
message = build_log_message(total) # 純計算
print(message) # 副作用:只在邊界發生FP 的做法讓「計算」部分完全可測試,「副作用」部分集中到程式的最外層,大幅降低意外改到狀態的風險。
4. 測試難度
副作用的差異直接影響測試的難易度。
OOP 測試:需要先建立物件、設置狀態,測試後還要確認副作用是否正確發生。
python
import unittest
class TestReportService(unittest.TestCase):
def test_calculate_and_log(self):
service = ReportService() # 需要建立物件
result = service.calculate_and_log([100.0, 200.0], 0.1)
self.assertEqual(result, 270.0)
self.assertEqual(len(service.log), 1) # 還需要驗證副作用是否發生
# 若 calculate_and_log 內有 print,測試輸出也會被污染FP 測試:純函數只看輸入與輸出,不需要任何前置設置。
python
import unittest
class TestCalculations(unittest.TestCase):
def test_calculate_total(self):
result = calculate_total((100.0, 200.0), 0.1)
self.assertEqual(result, 270.0)
# 不需要 mock、不需要物件狀態,直接給輸入、驗輸出
def test_build_log_message(self):
msg = build_log_message(270.0)
self.assertEqual(msg, "計算完成,金額:270.0")純函數天然適合參數化測試(Parameterized Test):輸入固定,輸出必然固定,可以用一個測試函數驗證大量輸入輸出組合,而不需要針對每種狀態各建一個物件。
各自適用場景
OOP 適合:
- 領域建模(Domain Modeling):系統中有明確的「實體」概念,例如
User、Order、Product,每個實體有自己的狀態與行為。 - GUI / 元件系統:如 UI 框架中的元件(Widget、View),天然具有狀態(顯示、隱藏、選中)與行為(點擊、滑動)。
- 長生命週期物件:需要管理複雜狀態的物件,例如資料庫連線池、Session 管理、遊戲角色。
FP 適合:
- 資料轉換 Pipeline:ETL 流程、日誌解析、報表生成等需要對資料做一系列轉換的場景。
- 並行計算(Concurrent / Parallel Computing):不可變性讓多個執行緒可以安全地同時讀取資料,不需要加鎖。
- 業務規則引擎:定價規則、折扣計算、資格判斷,純函數讓規則獨立可測試,規則本身也容易組合。
典範專責語言
有些語言在設計之初就選定了一種典範,並以此為核心。
以 FP 為主的語言
- Haskell:最純粹的 FP 語言。預設不可變,副作用必須透過
IO Monad明確標記,根本不存在傳統的可變狀態。 - Erlang / Elixir:為高並行系統設計,資料完全不可變,沒有 class 概念,程式由獨立的輕量級 Process 組成,透過訊息傳遞溝通。
- Clojure:執行在 JVM 上的 Lisp 方言,強調不可變資料結構,狀態變更有明確的並行控制機制。
以 OOP 為主的語言
- Smalltalk:OOP 的起源。「所有東西都是物件」,連整數、布林值也是物件,方法呼叫稱為「傳訊息(Message Passing)」。
- Ruby:設計上一切皆物件,語言核心緊扣 OOP,FP 風格相對薄弱。
- Java(Java 8 以前):幾乎強制用 class 組織程式,沒有 first-class function;直到 Java 8 才加入 lambda 與 Stream API。
現代主流語言(Python、Kotlin、Scala、TypeScript、C#)幾乎都走向多典範,同時支援 OOP 與 FP,讓開發者按場景選擇。本文所有範例皆以 Python 示範,正是因為它能自然地呈現兩種風格。
總結
| 面向 | OOP | FP |
|---|---|---|
| 核心抽象 | 物件(狀態 + 行為) | 函數(輸入 → 輸出) |
| 狀態管理 | 可變狀態,物件擁有自己的狀態 | 不可變資料,轉換產生新資料 |
| 副作用 | 方法可以改變物件或外部狀態 | 明確隔離副作用,純函數無副作用 |
| 程式組織 | 繼承與類別階層 | 函數組合 |
| 測試難度 | 需要設置物件狀態,副作用要 mock | 純函數直接給輸入驗輸出,無需 mock |
| 適合場景 | 領域建模、GUI、長生命週期物件 | 資料轉換、並行計算、業務規則引擎 |
面試中如果被問到這道題,建議的回答結構是:
- 說明兩者的核心抽象不同:OOP 以物件封裝狀態與行為,FP 以純函數轉換資料。
- 點出狀態管理與副作用的哲學差異:OOP 接受可變狀態,FP 明確隔離副作用。
- 舉一個具體的使用場景說明各自優勢。
- 補充現代語言通常兩者兼支援,Python 就是一個好例子,根據場景選擇風格才是重點。
參考
- Functional Programming HOWTO — Python 3 Docs
- functools — Higher-order functions and operations on callable objects
- dataclasses — Data Classes — Python 3 Docs
- Object-Oriented Programming (OOP) in Python — Real Python
- Functional Programming in Python — Real Python
- Why Functional Programming Matters — John Hughes (1990)