本章探討**自動櫃員機(Automated Teller Machine, ATM)**的物件導向設計。ATM 的核心目的是為使用者自動化銀行業務:查詢餘額、提款、轉帳。本設計建模 ATM 機台、銀行帳戶、硬體介面與交易狀態。

Automated Teller Machine (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 ReaderKeypadScreenCash DispenserDeposit Slot,可選 Printer
  • 支援 WithdrawDeposit 交易
  • 處理例外(餘額不足、輸入錯誤)並在多次失敗後扣留卡片

非功能性需求#

  • 強化安全措施保護使用者資料;多次 PIN 錯誤後扣留卡片
  • 可靠運作;故障時安全退卡

Use Case Diagram#

Use Case Diagram of ATM

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。

Account class and AccountType enum

真實銀行通常維護交易帳本以利稽核並從中推導餘額。為簡化,本設計直接更新餘額——優先聚焦 ATM 核心功能與範圍管理。

Bank#

OOD 題目中系統通常自包,無真實資料庫或 API 呼叫。Bank 處理 ATM 運作所需的主要資料。

它儲存 Account 物件並連結到卡片以快速查詢,支援卡與 PIN 驗證、帳戶存取與餘額檢查。

我們定義 BankInterface 解耦 Bank 實作與 ATM。本地 Bank 可輕易換為網路實作(如 API client adapter)而不需更動 ATM 核心邏輯。

BankInterface interface and concrete class

替代方案:可將 Bank 功能直接整合進 ATMMachine,但會緊耦合帳戶管理與 ATM 操作,降低模組化並難以擴充為網路化銀行。

Transaction#

Transaction 介面定義所有交易類型的契約,搭配 TransactionType enum 指定 Withdraw 與 Deposit。具體類別 WithdrawTransactionDepositTransaction 負責驗證與執行。

Transaction interface and concrete classes

Design Choice:將 Transaction 抽象化為獨立物件,能以共享行為表示不同交易類型,未來新增類型不需修改核心邏輯。

ATMState#

ATMState 介面提供處理 ATM 互動流程不同階段的框架,讓 ATMMachine 能系統化管理每個使用者步驟。

具體狀態類別:

  • IdleState:等待插卡
  • PinEntryState:處理 PIN 輸入
  • TransactionSelectionState:處理交易選擇
  • WithdrawAmountEntryState:處理提款金額輸入
  • DepositCollectionState:處理存款金額

ATMState interface and concrete classes

狀態轉換圖#

State transition diagram

  • IdleState:等待插卡,卡片有效則進入 PinEntryState
  • PinEntryState:處理 PIN,正確則進入 TransactionSelectionState
  • TransactionSelectionState:處理交易選擇,進入 WithdrawAmountEntryStateDepositCollectionState
  • WithdrawAmountEntryState:處理金額輸入並執行,回到 TransactionSelectionStateIdleState
  • DepositCollectionState:處理存款並執行,回到 TransactionSelectionStateIdleState

想了解 State Pattern,請參考 Vending Machine 章節。

硬體元件介面#

硬體元件介面處理使用者互動與物理操作,確保 ATMMachine 獨立於特定硬體實作。

  • CardProcessor:插卡時讀取、session 結束後釋放
  • Keypad:捕捉 PIN、選擇、金額等輸入
  • DepositBox:收集存款
  • CashDispenser:提款時送出現金
  • Display:顯示訊息與提示

Hardware component interfaces

ATMMachine#

ATMMachine 作為 ATM 系統的 facade,協調使用者互動、硬體元件,並依賴 Bank 存取帳戶與處理交易。它使用 ATMState 管理 session 步驟。

ATMMachine class diagram

Design Choice:以 State PatternATMState 類別)管理 ATM 的順序流程,確保每個階段封裝清楚。同時以介面化的硬體元件抽象物理互動。這種職責分離提升模組化、簡化維護,並支援以模擬硬體進行測試。

完整類別圖#

Summarized Class Diagram of ATM System

程式實作#

Account#

Account 封裝核心銀行屬性,採用:

  • 不可變欄位accountNumbercardNumberaccountTypecardPinHash
  • 可變欄位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 確保金額精度。帳戶身份欄位(accountNumbercardNumber)為不可變以維持資料一致。

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());
    }
}

為節省篇幅,省略 PinEntryStateTransactionSelectionStateDepositCollectionState,結構類似。

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 欄位追蹤目前狀態;透過 transitionToStateIdleStatePinEntryState 等之間轉移

本章小結#

系統的可維護性與可擴展性透過清晰的職責分離達成:

  • AccountBank:管理帳戶資料與銀行操作
  • Transaction:處理財務操作
  • ATMState 及其具體類別:管理使用者流程階段
  • 硬體介面(Keypad、CardProcessor 等):處理使用者互動
  • ATMMachine:以 facade 編排整體流程

關鍵設計選擇:用 ATMState 實踐 State Pattern、把硬體互動分離為介面,提升模組化並支援未來擴充(新交易類型或硬體元件)。