OOP 的四大支柱:如何設計一間咖啡廳的結帳系統?
2026年8月10日 凌晨12:43 · 閱讀 7 分鐘
系列文章: 常用設計模式整理 (第 2 篇)
為什麼要使用 OOP?
「物件導向」強調的是「分工合作」。比起一個人想好要如何經營一家咖啡廳,不如想好你需要什麼樣的咖啡師、管理人員、室內設計師、行銷人員,再讓大家一起合作,想來會容易得多。
OOP 讓每個人(物件)可以專注在自己的工作上,透過團體(多個物件)合作達成目標。咖啡師不需要知道行銷人員為什麼今天要推出買一送一,他只要專心於沖泡出眼前的一杯(或兩杯)好咖啡就好。
OOP 與 Design Patterns 的關係
OOP 讓我們能將不同職責分配給不同的物件,而 Design Pattern 則是進一步,將物件之間「好用的分工方式」記錄下來。每種設計模式都有適合的使用情境,讓開發者在未來遇到類似的情境時就可以直接拿來抄答案參考。
OOP 的強大之處可從其四大支柱來理解:Encapsulation, Abstraction, Inheritance, Polymorphism。Design Patterns 奠基於 OOP 之上,未來在認識各種 Design Patterns 時,也可回過頭來用四大支柱的觀點去分析各個設計模式的優點是什麼。
系統需求:一間咖啡廳的結帳系統
- 支援多種支付方式:信用卡(Visa、Mastercard)、Line Pay、Apple Pay
- 每筆交易需要經過驗證(額度 / 餘額是否足夠)
- 敏感資料(卡號、餘額)不能被外部程式任意讀取或竄改
- 結帳系統只在乎付款成功與否(遵守流程:「建立訂單 → 呼叫付款 → 顯示結果」),不在乎實際上是用哪種方式付款
你會想怎麼設計這個系統呢?可以先自己想看看,再往下看利用 OOP 四大支柱的視角會是如何進行實作
1. Encapsulation 封裝
隱藏內部細節,同時能防止外部破壞,保護資料完整性
實作:Line Pay 的驗證與資料保密
- 將驗證邏輯寫在
pay()內部,每次支付前都會確認餘額是否足夠 - 利用
privatefields,外部的 class 就無法存取到accountToken與現有餘額
public class LinePay {
private String accountToken;
private int balance;
public LinePay(String accountToken, int balance) {
this.accountToken = accountToken;
this.balance = balance;
}
public void pay(int amount) {
if (amount > balance) {
System.out.println("餘額不足,付款失敗");
return;
}
balance -= amount;
System.out.println("LINE Pay 扣款成功,剩餘餘額:" + balance);
}
}- 將驗證邏輯寫在
pay()內部,每次支付前都會確認餘額是否足夠 - 利用
privatedata members,外部的 class 就無法存取到accountToken與現有餘額
#include <iostream>
#include <string>
using namespace std;
class LinePay {
private:
string accountToken;
int balance;
public:
LinePay(string accountToken, int balance): accountToken(accountToken), balance(balance) {}
void pay(int amount) {
if (amount > balance) {
cout << "餘額不足,付款失敗" << endl;
return;
}
balance -= amount;
cout << "LINE Pay 扣款成功,剩餘餘額:" << balance << endl;
}
};- 將驗證邏輯寫在
pay()內部,每次支付前都會確認餘額是否足夠 - 依照 Python 慣例,利用
__前綴來表示 private attributes。由於 name mangling 機制,外部的 class 就不會不小心存取到 private attributes(但仍然可以繞過去)
class LinePay:
def __init__(self, account_token: str, balance: int):
self.__account_token = account_token
self.__balance = balance
def pay(self, amount: int):
if amount > self.__balance:
print("餘額不足,付款失敗")
return
self.__balance -= amount
print(f"LINE Pay 扣款成功,剩餘餘額:{self.__balance}")2. Abstraction 抽象
呼叫端只關注物件「能做什麼」,而不必知道是怎麼做到的,降低認知負載。
實作 — 介面:將付款功能寫成一個介面
- 介面的概念相當於訂定「合作契約」,讓呼叫端與實作端解耦
- 透過
interface定義介面
public interface PaymentMethod {
void pay(int amount);
}- 介面的概念相當於訂定「合作契約」,讓呼叫端與實作端解耦
- 透過 pure virtual function(
virtual ... = 0)來實現介面效果
#include <iostream>
#include <string>
using namespace std;
class PaymentMethod {
public:
virtual void pay(int amount) = 0;
virtual ~PaymentMethod() = default;
};- 介面的概念相當於訂定「合作契約」,讓呼叫端與實作端解耦
- 透過
abc.ABC與@abstractmethod實現介面 - 另一個方法則是乾脆不宣告介面,直接利用 Python 的 duck typing 機制
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount: int):
pass實作 — 呼叫端:結帳系統只要知道怎麼操作「付款功能」,不管細節
- 結帳系統
Checkout只要知道能透過介面執行pay()就好 - 解耦:呼叫端只跟「契約」互動,完全不知道實作端如何完成
pay()這件事
public class Checkout {
public void process(PaymentMethod payment, int amount) {
payment.pay(amount);
}
}#include <iostream>
#include <string>
using namespace std;
class Checkout {
public:
void process(PaymentMethod& payment, int amount) {
payment.pay(amount);
}
};class Checkout:
def process(self, payment: PaymentMethod, amount: int):
payment.pay(amount)實作 — 實作端:實際的付款平台,負責提供符合規定的付款功能
LinePay必須implementsPaymentMethod介面,實作pay()方法的實際內容- 可利用
@Override標註出被 overridden 的方法 - 解耦:實作端只跟「契約」互動,不知道呼叫端是誰、會如何使用
pay()方法
public class LinePay implements PaymentMethod {
// ...
@Override
public void pay(int amount) {
// ...
}
}LinePay必須繼承PaymentMethod類別,實作pay()方法的實際內容- 可利用
override標註出被 overridden 的方法 - 解耦:實作端只跟「契約」互動,不知道呼叫端是誰、會如何使用
pay()方法
#include <iostream>
#include <string>
using namespace std;
class LinePay : public PaymentMethod {
private:
string accountToken;
int balance;
public:
LinePay(string accountToken, int balance)
: accountToken(accountToken), balance(balance) {}
void pay(int amount) override {
if (amount > balance) {
cout << "餘額不足,付款失敗" << endl;
return;
}
balance -= amount;
cout << "LINE Pay 扣款成功,剩餘餘額:" << balance << endl;
}
};LinePay繼承PaymentMethod類別,實作pay()方法的實際內容(或不繼承,採用 duck typing 方式)- 解耦:實作端只跟「契約」互動,不知道呼叫端是誰、會如何使用
pay()方法
class LinePay(PaymentMethod):
def __init__(self, account_token: str, balance: int):
self.__account_token = account_token
self.__balance = balance
def pay(self, amount: int):
if amount > self.__balance:
print("餘額不足,付款失敗")
return
self.__balance -= amount
print(f"LINE Pay 扣款成功,剩餘餘額:{self.__balance}")3. Inheritance 繼承
有些類別之間會是 is-a 的關係,利用繼承將通用的邏輯寫在父類別中,達成 reuse 與 集中管理。
實作 — 父類別:兩種信用卡邏輯重複,那就寫在同個地方
- 將通用邏輯
cardNumber、limit、fraudCheck()等內容寫在 abstract classCreditCardPayment中 - 另外由於信用卡也是一種支付方式,故也要實作
PaymentMethod介面
public abstract class CreditCardPayment implements PaymentMethod {
protected String cardNumber;
protected int limit;
public CreditCardPayment(String cardNumber, int limit) {
this.cardNumber = cardNumber;
this.limit = limit;
}
protected boolean fraudCheck(int amount) {
return amount <= limit;
}
@Override
public void pay(int amount) {
if (!fraudCheck(amount)) {
System.out.println("超過額度,付款失敗");
return;
}
charge(amount);
}
protected abstract void charge(int amount);
}- 將通用邏輯
cardNumber、limit、fraudCheck()等內容寫在父類別CreditCardPayment中 - C++ 沒有區分 abstract class 與 interface,兩者都統一透過 pure virtual function 實現
- 另外由於信用卡也是一種支付方式,故也要繼承
PaymentMethod類別
#include <iostream>
#include <string>
using namespace std;
class CreditCardPayment : public PaymentMethod {
protected:
string cardNumber;
int limit;
bool fraudCheck(int amount) {
return amount <= limit;
}
virtual void charge(int amount) = 0;
public:
CreditCardPayment(string cardNumber, int limit)
: cardNumber(cardNumber), limit(limit) {}
void pay(int amount) override {
if (!fraudCheck(amount)) {
cout << "超過額度,付款失敗" << endl;
return;
}
charge(amount);
}
};- 將通用邏輯
cardNumber、limit、fraudCheck()等內容寫在 abstract classCreditCardPayment中 - 另外由於信用卡也是一種支付方式,故也要實作
PaymentMethod介面
class CreditCardPayment(PaymentMethod):
def __init__(self, card_number: str, limit: int):
self._card_number = card_number
self._limit = limit
def _fraud_check(self, amount: int) -> bool:
return amount <= self._limit
def pay(self, amount: int):
if not self._fraud_check(amount):
print("超過額度,付款失敗")
return
self._charge(amount)
@abstractmethod
def _charge(self, amount: int):
pass實作 — 子類別:兩種信用卡邏輯不同,那就寫在自己內部
VisaCard與MastercardCard的charge()內容不同,故獨立撰寫在子類別內- Reuse:其他 methods 與 fields 則沿用父類別的內容。一模一樣的內容不用寫兩次
public class VisaCard extends CreditCardPayment {
public VisaCard(String cardNumber, int limit) {
super(cardNumber, limit);
}
@Override
protected void charge(int amount) {
System.out.println("透過 Visa 金流商請款:" + amount);
}
}
public class MastercardCard extends CreditCardPayment {
public MastercardCard(String cardNumber, int limit) {
super(cardNumber, limit);
}
@Override
protected void charge(int amount) {
System.out.println("透過 Mastercard 金流商請款:" + amount);
}
}#include <iostream>
#include <string>
using namespace std;
class VisaCard : public CreditCardPayment {
public:
VisaCard(string cardNumber, int limit)
: CreditCardPayment(cardNumber, limit) {}
protected:
void charge(int amount) override {
cout << "透過 Visa 金流商請款:" << amount << endl;
}
};
class MastercardCard : public CreditCardPayment {
public:
MastercardCard(string cardNumber, int limit)
: CreditCardPayment(cardNumber, limit) {}
protected:
void charge(int amount) override {
cout << "透過 Mastercard 金流商請款:" << amount << endl;
}
};class VisaCard(CreditCardPayment):
def _charge(self, amount: int):
print(f"透過 Visa 金流商請款:{amount}")
class MastercardCard(CreditCardPayment):
def _charge(self, amount: int):
print(f"透過 Mastercard 金流商請款:{amount}")4. Polymorphism 多型
呼叫端使用同份程式碼,便能執行不同的具體實作。未來擴充新類別時也不用修改呼叫端程式碼。
實作:結帳時,程式碼不需要根據結帳方式而改變
- 不論付款方式是
LinePay還是VisaCard,都可以直接傳入Checkout.process()做呼叫 - 未來若要增加新的付款方式,只要把新的方式寫成 class,就可以直接拿來用,而不用動到
Checkout中的process()內容
public class Main {
public static void main(String[] args) {
Checkout checkout = new Checkout();
checkout.process(new LinePay("token-123", 1000), 300);
checkout.process(new VisaCard("4111-xxxx", 5000), 1200);
checkout.process(new MastercardCard("5500-xxxx", 3000), 800);
}
}#include <iostream>
#include <string>
using namespace std;
int main() {
Checkout checkout;
LinePay linePay("token-123", 1000);
VisaCard visa("4111-xxxx", 5000);
MastercardCard mastercard("5500-xxxx", 3000);
checkout.process(linePay, 300);
checkout.process(visa, 1200);
checkout.process(mastercard, 800);
return 0;
}if __name__ == "__main__":
checkout = Checkout()
checkout.process(LinePay("token-123", 1000), 300)
checkout.process(VisaCard("4111-xxxx", 5000), 1200)
checkout.process(MastercardCard("5500-xxxx", 3000), 800)系統類別圖 Class Diagram
(各語言實現 OOP 的方式略有不同,此系列的類別圖將以 java 為例來繪製)
OOP 語法整理
- 特色:OOP 風格明顯,有很多直接支援 OOP 概念的關鍵字(
interface,abstract, …)
| 相關特性 | 語法 |
|---|---|
| Encapsulation | 利用 private/protected/public/(default: package) 設定 fields 與 methods 的存取權限 |
| Abstraction | interface 或 abstract class |
| Inheritance | extends (class) 與 implements (interface) |
| Polymorphism | methods 預設就可以被子類別覆寫;加註 @Override 可支援編譯期檢查 |
- 特色:有功能類似的
class與struct可選擇。預設為 static binding。需要自己做 garbage collection 撰寫 destructor
| 相關特性 | 語法 |
|---|---|
| Encapsulation | 利用 private/protected/public 設定 data members、member functions 的存取權限;也可用來覆寫 class 或 struct 內部成員的預設權限 |
| Abstraction | 利用 pure virtual function 達到 interface 或 abstract class 的效果 |
| Inheritance | class Derived : public Base(可透過 private/protected/public 設定繼承而來的 member 的存取權限) |
| Polymorphism | 只有標註 virtual 的 member functions 可被子類別覆寫;加註 override 可支援編譯期檢查 |
- 特色:不需要編譯,執行結果都是動態決定。另外不特別區分 field 與 method,技術上都是
attribute。
| 相關特性 | 語法 |
|---|---|
| Encapsulation | 慣例上利用前綴 _ 表示 protected attributes,前綴 __ 表示 private attributes。但都無法強制限制外部存取。也可利用 @property 把方法包裝成像 attribute 的樣子 |
| Abstraction | 利用 abc.ABC 與 @abstractmethod 撰寫介面。或完全不宣告介面,直接依靠 duck typing |
| Inheritance | class Derived(Base) |
| Polymorphism | 沒有 virtual 概念,連變數型別本身都是動態的,故方法呼叫也都是執行期動態決定 |
三種語言的設計哲學:靜態與動態
- C++:預設是 static binding,意思是方法呼叫與實際執行的程式碼預設在編譯期就決定好。若要支援 dynamic binding,則必須透過
virtual關鍵字來讓 compiler 知道這個方法呼叫要等到執行期再做決定 - Java:預設是 dynamic binding,故所有 instance methods 呼叫都是執行期才動態決定,只有
staticmethod 是靜態綁定 - Python:為直譯語言,不需要編譯。不只方法呼叫時執行的內容,連變數本身的型別也是動態決定。呼叫方法時預設不會檢查類別,只要有對應的方法就可以合法執行呼叫,也就是 duck typing 機制 —— 只要叫聲像是鴨子,就算它實際是台音響 Python 也當作它是隻鴨子 —— 佛系的語言 :)
三種語言的設計哲學:多重繼承的不同策略
- C++:支援多重繼承,但要透過
virtualinheritance 自行解決 diamond inheritance 問題 - Java:只能單一繼承(
extends),但可以實作(implements)多個 interfaces。因為interface沒有 field,也沒有方法實作(除非主動定義defaultmethods),故不會有 diamond inheritance 問題 - Python:支援多重繼承,用 MRO 解決 diamond inheritance 問題