本章探討**自動櫃員機(Automated Teller Machine, ATM)**的物件導向設計。ATM 的核心目的是為使用者自動化銀行業務:查詢餘額、提款、轉帳。本設計建模 ATM 機台、銀行帳戶、硬體介面與交易狀態。
需求蒐集#
題目情境#
想像你在繁忙午後走向 ATM 處理銀行業務。插入卡片、輸入 PIN,從選單選擇查餘額、提款或存款。系統幾秒內驗證身份並執行請求。背景中,ATM 與銀行協調、管理交易、與讀卡機與出鈔機等硬體互動。請設計這樣的系統。
需求釐清對話#
Candidate:使用者互動方面,ATM 應有讀卡機、輸入 PIN 與選項的鍵盤、顯示螢幕、出鈔機、存款投入口。涵蓋主要元件嗎?
Interviewer:是的。
Candidate:流程是:插卡 → 輸入 PIN 認證 → 主選單(查餘額、提款、存款)→ 完成後選擇繼續或結束 → 結束時退卡?
Interviewer:正確。
Candidate:認證上,ATM 驗證卡與 PIN 組合,無效則顯示錯誤?
Interviewer:是的。請加上「PIN 嘗試三次失敗鎖卡」的安全措施。
Candidate:帳戶方面,每位使用者可關聯多個帳戶(Checking、Savings),可選擇進行交易?
Interviewer:正確。
Candidate:交易部分處理 Withdraw 與 Deposit。要包含轉帳嗎?
Interviewer:簡化起見,僅 Withdraw 與 Deposit。確保提款驗證餘額、存款正確處理現金。
Candidate:錯誤處理上,ATM 顯示清楚的訊息(餘額不足、PIN 錯誤、硬體故障)?
Interviewer:同意,清楚的錯誤訊息很重要。
需求整理#
功能性需求#
- 透過 debit card 與 PIN 認證使用者
- 支援每位使用者多帳戶(Checking、Savings)
- 包含硬體元件:Card Reader、Keypad、Screen、Cash Dispenser、Deposit Slot,可選 Printer
- 支援 Withdraw 與 Deposit 交易
- 處理例外(餘額不足、輸入錯誤)並在多次失敗後扣留卡片
非功能性需求#
- 強化安全措施保護使用者資料;多次 PIN 錯誤後扣留卡片
- 可靠運作;故障時安全退卡
Use Case Diagram#
Customer 的 use cases#
- Insert Card:插卡開啟 session
- Enter PIN:輸入 PIN 認證
- Select Transaction:選擇交易類型
- Select Account:選擇 Checking 或 Savings
- Withdraw Cash:提款
- Deposit Funds:存款
- Eject Card:退卡結束 session
System 的 use cases#
- Validate Card and PIN:驗證卡與 PIN
- Withdraw Cash:確認餘額並出鈔
- Deposit Funds:接受現金並更新餘額
- Eject Card:退卡並結束 session
辨識核心物件#
- Bank:儲存與管理帳戶
- Account:管理帳戶細節(餘額、帳號、卡號、PIN hash、類型)
- ATMMachine:主協調器,管理使用者互動並連結硬體元件
- Transaction:管理財務交易(提款、存款),含驗證與執行
Design Choice:將硬體存取與管理整合於
ATMMachine,確保元件行為一致,提供從插卡到完成交易的順暢流程。
類別圖設計#
Account#
Account 封裝帳戶基本資料與認證/交易操作:餘額、帳號、關聯卡號、PIN(為安全已 hash),並用 AccountType enum 區分 Checking 與 Savings。
真實銀行通常維護交易帳本以利稽核並從中推導餘額。為簡化,本設計直接更新餘額——優先聚焦 ATM 核心功能與範圍管理。
Bank#
OOD 題目中系統通常自包,無真實資料庫或 API 呼叫。Bank 處理 ATM 運作所需的主要資料。
它儲存 Account 物件並連結到卡片以快速查詢,支援卡與 PIN 驗證、帳戶存取與餘額檢查。
我們定義 BankInterface 解耦 Bank 實作與 ATM。本地 Bank 可輕易換為網路實作(如 API client adapter)而不需更動 ATM 核心邏輯。
替代方案:可將 Bank 功能直接整合進
ATMMachine,但會緊耦合帳戶管理與 ATM 操作,降低模組化並難以擴充為網路化銀行。
Transaction#
Transaction 介面定義所有交易類型的契約,搭配 TransactionType enum 指定 Withdraw 與 Deposit。具體類別 WithdrawTransaction 與 DepositTransaction 負責驗證與執行。
Design Choice:將
Transaction抽象化為獨立物件,能以共享行為表示不同交易類型,未來新增類型不需修改核心邏輯。
ATMState#
ATMState 介面提供處理 ATM 互動流程不同階段的框架,讓 ATMMachine 能系統化管理每個使用者步驟。
具體狀態類別:
- IdleState:等待插卡
- PinEntryState:處理 PIN 輸入
- TransactionSelectionState:處理交易選擇
- WithdrawAmountEntryState:處理提款金額輸入
- DepositCollectionState:處理存款金額
狀態轉換圖#
- IdleState:等待插卡,卡片有效則進入
PinEntryState - PinEntryState:處理 PIN,正確則進入
TransactionSelectionState - TransactionSelectionState:處理交易選擇,進入
WithdrawAmountEntryState或DepositCollectionState - WithdrawAmountEntryState:處理金額輸入並執行,回到
TransactionSelectionState或IdleState - DepositCollectionState:處理存款並執行,回到
TransactionSelectionState或IdleState
想了解 State Pattern,請參考 Vending Machine 章節。
硬體元件介面#
硬體元件介面處理使用者互動與物理操作,確保 ATMMachine 獨立於特定硬體實作。
- CardProcessor:插卡時讀取、session 結束後釋放
- Keypad:捕捉 PIN、選擇、金額等輸入
- DepositBox:收集存款
- CashDispenser:提款時送出現金
- Display:顯示訊息與提示
ATMMachine#
ATMMachine 作為 ATM 系統的 facade,協調使用者互動、硬體元件,並依賴 Bank 存取帳戶與處理交易。它使用 ATMState 管理 session 步驟。
Design Choice:以 State Pattern(
ATMState類別)管理 ATM 的順序流程,確保每個階段封裝清楚。同時以介面化的硬體元件抽象物理互動。這種職責分離提升模組化、簡化維護,並支援以模擬硬體進行測試。
完整類別圖#
程式實作#
Account#
Account 封裝核心銀行屬性,採用:
- 不可變欄位:
accountNumber、cardNumber、accountType、cardPinHash - 可變欄位:
balance
// Represents a bank account with balance, card details, and PIN security
public class Account {
private BigDecimal balance;
private final String accountNumber;
private final String cardNumber;
private final byte[] cardPinHash;
private final AccountType accountType;
// Creates a new account with initial zero balance and hashed PIN
public Account(
final String accountNumber,
final AccountType type,
final String cardNumber,
final String pin) {
this.accountNumber = accountNumber;
this.accountType = type;
this.cardNumber = cardNumber;
this.cardPinHash = calculateMd5(pin); // PIN is hashed for security
this.balance = BigDecimal.ZERO;
}
// Validates the entered PIN against stored hash
public boolean validatePin(String pinNumber) {
byte[] entryPinHash = calculateMd5(pinNumber);
return Arrays.equals(cardPinHash, entryPinHash);
}
// Updates account balance by adding the specified amount
public void updateBalanceWithTransaction(final BigDecimal balanceChange) {
this.balance = this.balance.add(balanceChange);
}
// getter methods omitted for brevity
}
// Defines the type of bank account
public enum AccountType {
// Regular checking account for daily transactions
CHECKING,
// Interest-bearing savings account
SAVING
}validatePin:透過比對 PIN hash 確保安全認證updateBalanceWithTransaction:以單一方法處理存提款(正數存款、負數提款)
Implementation Choice:餘額用
BigDecimal確保金額精度。帳戶身份欄位(accountNumber、cardNumber)為不可變以維持資料一致。
Bank#
public interface BankInterface {
void addAccount(String accountNumber, AccountType type, String cardNumber, String pin);
boolean validateCard(String cardNumber);
boolean checkPin(String cardNumber, String pinNumber);
Account getAccountByAccountNumber(String accountNumber);
Account getAccountByCard(String cardNumber);
boolean withdrawFunds(Account account, BigDecimal amount);
}
// Manages bank accounts and provides banking operations like validation and transactions
public class Bank implements BankInterface {
private final Map<String, Account> accounts = new HashMap<>();
private final Map<String, Account> accountByCard = new HashMap<>();
// Creates a new account and stores it in both account and card maps
@Override
public void addAccount(
final String accountNumber,
final AccountType type,
final String cardNumber,
final String pin) {
final Account newAccount = new Account(accountNumber, type, cardNumber, pin);
accounts.put(newAccount.getAccountNumber(), newAccount);
accountByCard.put(newAccount.getCardNumber(), newAccount);
}
// Checks if a card number exists in the bank's records
@Override
public boolean validateCard(final String cardNumber) {
return getAccountByCard(cardNumber) != null;
}
// Verifies if the provided PIN matches the card's stored PIN
@Override
public boolean checkPin(String cardNumber, String pinNumber) {
Account account = getAccountByCard(cardNumber);
if (account != null) {
return account.validatePin(pinNumber);
}
return false;
}
// Retrieves account by account number
@Override
public Account getAccountByAccountNumber(String accountNumber) {
return accounts.get(accountNumber);
}
// Retrieves account by card number
@Override
public Account getAccountByCard(String cardNumber) {
return accountByCard.get(cardNumber);
}
// Attempts to withdraw specified amount from account if sufficient funds exist
@Override
public boolean withdrawFunds(Account account, BigDecimal amount) {
if (account.getBalance().compareTo(amount) >= 0) {
account.updateBalanceWithTransaction(amount.negate());
return true;
}
return false;
}
}Bank 使用兩個 HashMap 提供 O(1) 查找:
- accounts:帳號 → Account
- accountByCard:卡號 → Account(用於認證時快速存取)
Implementation Choice:HashMap 提供平均 O(1) 查找時間,確保即時操作(卡驗證、帳戶取得)的快速效能。
Transaction#
public interface Transaction {
TransactionType getType();
boolean validateTransaction();
void executeTransaction();
}
// Handles the withdrawal transaction process for removing funds from an account
public class WithdrawTransaction implements Transaction {
Account account;
BigDecimal amount;
// Returns the transaction type as WITHDRAW
@Override
public TransactionType getType() {
return TransactionType.WITHDRAW;
}
// Validates if the account has sufficient funds for withdrawal
@Override
public boolean validateTransaction() {
assert account != null;
return account.getBalance().compareTo(amount) > 0;
}
// Creates a new withdrawal transaction, throws exception if validation fails
public WithdrawTransaction(Account account, BigDecimal amount) {
if (!validateTransaction()) {
throw new IllegalStateException(
"Cannot complete withdrawal: Insufficient funds in account");
}
this.account = account;
this.amount = amount;
}
// Executes the withdrawal by subtracting the amount from account balance
@Override
public void executeTransaction() {
account.updateBalanceWithTransaction(amount.negate());
}
}
// Handles the deposit transaction process for adding funds to an account
public class DepositTransaction implements Transaction {
final Account account;
final BigDecimal amount;
// Returns the transaction type as DEPOSIT
@Override
public TransactionType getType() {
return TransactionType.DEPOSIT;
}
// Deposit transactions are always valid
@Override
public boolean validateTransaction() {
return true;
}
public DepositTransaction(Account account, BigDecimal amount) {
this.account = account;
this.amount = amount;
}
// Executes the deposit by adding the amount to the account balance
@Override
public void executeTransaction() {
account.updateBalanceWithTransaction(amount);
}
}ATMState#
ATMState 抽象類別構成 ATM 的狀態機基礎,由具體實作管理不同階段。
public class ATMState {
// Displays an invalid action message on the ATM screen
private static void renderDefaultAction(ATMMachine atmMachine) {
atmMachine.getDisplay().showMessage("Invalid action, please try again.");
}
// Default implementation for card insertion
public void processCardInsertion(ATMMachine atmMachine, String cardNumber) {
renderDefaultAction(atmMachine);
}
// Default implementation for card ejection
public void processCardEjection(ATMMachine atmMachine) {
renderDefaultAction(atmMachine);
}
// Default implementation for PIN entry
public void processPinEntry(ATMMachine atmMachine, String pin) {
renderDefaultAction(atmMachine);
}
// Default implementation for withdrawal request
public void processWithdrawalRequest(ATMMachine atmMachine) {
renderDefaultAction(atmMachine);
}
// Default implementation for deposit request
public void processDepositRequest(ATMMachine atmMachine) {
renderDefaultAction(atmMachine);
}
// Default implementation for amount entry
public void processAmountEntry(ATMMachine atmMachine, BigDecimal amount) {
renderDefaultAction(atmMachine);
}
// Default implementation for deposit collection
public void processDepositCollection(ATMMachine atmMachine, BigDecimal amount) {
renderDefaultAction(atmMachine);
}
}IdleState 等待插卡,卡片有效則轉至 PinEntryState:
public class IdleState extends ATMState {
/**
* This method is called when a card is inserted into the ATM. This transitions the ATM to the
* PinEntryState if the card is valid.
*/
@Override
public void processCardInsertion(ATMMachine atmMachine, String cardNumber) {
if (atmMachine.getBankInterface().validateCard(cardNumber)) {
atmMachine.getDisplay().showMessage("Please enter your PIN");
atmMachine.transitionToState(new PinEntryState());
} else {
atmMachine.getDisplay().showMessage("Invalid card. Please try again.");
}
}
}WithdrawAmountEntryState 管理提款金額輸入並透過 Bank 執行:
public class WithdrawAmountEntryState extends ATMState {
// Handles card ejection by canceling transaction and returning to idle state
@Override
public void processCardEjection(ATMMachine atmMachine) {
atmMachine.getDisplay().showMessage("Transaction cancelled, card ejected");
atmMachine.transitionToState(new IdleState());
}
// Processes withdrawal request by checking balance and dispensing cash if sufficient funds
@Override
public void processAmountEntry(ATMMachine atmMachine, BigDecimal amount) {
String cardNumber = atmMachine.getCardProcessor().getCardNumber();
Account account = atmMachine.getBankInterface().getAccountByCard(cardNumber);
boolean isSuccess = atmMachine.getBankInterface().withdrawFunds(account, amount);
if (isSuccess) {
atmMachine.getCashDispenser().dispenseCash(amount);
atmMachine.getDisplay().showMessage("Please take your cash.");
} else {
atmMachine.getDisplay().showMessage("Insufficient funds, please try again.");
}
atmMachine.transitionToState(new TransactionSelectionState());
}
}為節省篇幅,省略 PinEntryState、TransactionSelectionState、DepositCollectionState,結構類似。
ATMMachine#
// Main ATM machine class that manages the state and hardware components of the ATM
public class ATMMachine {
private ATMState state;
private final CardProcessor cardProcessor;
private final DepositBox depositBox;
private final CashDispenser cashDispenser;
private final Keypad keypad;
private final Display display;
private final Bank bank;
// Initializes ATM with all required hardware components and bank interface
public ATMMachine(
Bank bank,
CardProcessor cardProcessor,
DepositBox depositBox,
CashDispenser cashDispenser,
Keypad keypad,
Display display) {
this.bank = bank;
this.cardProcessor = cardProcessor;
this.depositBox = depositBox;
this.cashDispenser = cashDispenser;
this.keypad = keypad;
this.display = display;
this.state = new IdleState();
}
// Forwards card insertion to current state for processing
public void insertCard(String cardNumber) {
state.processCardInsertion(this, cardNumber);
}
// Forwards card ejection to current state for processing
public void ejectCard() {
state.processCardEjection(this);
}
// Forwards PIN entry to current state for validation
public void enterPin(String pin) {
state.processPinEntry(this, pin);
}
// Forwards withdrawal request to current state for processing
public void withdrawRequest() {
state.processWithdrawalRequest(this);
}
// Forwards deposit request to current state for processing
public void depositRequest() {
state.processDepositRequest(this);
}
// Forwards amount entry to current state for processing
public void enterAmount(BigDecimal amount) {
state.processAmountEntry(this, amount);
}
// Forwards deposit collection to current state for processing
public void collectDeposit(BigDecimal amount) {
state.processDepositCollection(this, amount);
}
// Returns the display component for showing messages
public Display getDisplay() {
return display;
}
// Returns the cash dispenser component for handling withdrawals
public CashDispenser getCashDispenser() {
return cashDispenser;
}
// Returns the bank interface for account operations
public BankInterface getBankInterface() {
return bank;
}
// Returns the card processor component for handling card operations
public CardProcessor getCardProcessor() {
return cardProcessor;
}
// Returns the keypad component for user input
public Keypad getKeypad() {
return keypad;
}
// Updates the current state of the ATM
public void transitionToState(ATMState nextState) {
this.state = nextState;
}
// Returns the current state of the ATM
public ATMState getCurrentState() {
return state;
}
// Returns the deposit box component for handling deposits
public DepositBox getDepositBox() {
return depositBox;
}
}職責摘要:
- 硬體管理:管理所有硬體元件(card processor、cash dispenser、deposit box、keypad、display),透過 getter 提供存取
- 狀態管理:以
state欄位追蹤目前狀態;透過transitionToState在IdleState、PinEntryState等之間轉移
本章小結#
系統的可維護性與可擴展性透過清晰的職責分離達成:
- Account 與 Bank:管理帳戶資料與銀行操作
- Transaction:處理財務操作
- ATMState 及其具體類別:管理使用者流程階段
- 硬體介面(Keypad、CardProcessor 等):處理使用者互動
- ATMMachine:以 facade 編排整體流程
關鍵設計選擇:用 ATMState 實踐 State Pattern、把硬體互動分離為介面,提升模組化並支援未來擴充(新交易類型或硬體元件)。