ATM 系统设计实战:基于 awesome-low-level-design 的 Python 低层设计实现解析
ATM 系统设计实战:基于 awesome-low-level-design 的 Python 低层设计实现解析
导读
本文围绕开源仓库 awesome-low-level-design 中 ATM 系统的低层设计(Low Level Design)文档与 Python 实现展开。你将掌握 ATM 的六大核心需求拆解方法、从 Card/Account/Transaction 到 BankingService/CashDispenser/ATM 的完整类设计,以及并发安全(线程锁)、状态模式(State Pattern)、责任链模式(Chain of Responsibility)在真实项目中的落地写法。读完后,你可以直接把这套设计骨架复用到银行、支付、取款机等需要"认证 + 事务 + 并发一致性"的场景中。
一、需求分析与设计目标
ATM 系统的设计文档(solutions/python/atm/README.md)将需求明确划分为六条,它们是后续所有类的设计依据:
| 需求 | 对应设计落点 |
|---|---|
| 余额查询、取款、存款三种基础操作 | OperationType 枚举 + ATM.select_operation 分发 |
| 卡片 + PIN 身份认证 | Card 类与 BankService.authenticate |
| 与银行后端交互校验账户并执行事务 | BankService 作为银行侧服务门面 |
| 现金出钞 | CashDispenser + 责任链式 NoteDispenser |
| 并发访问与数据一致性 | threading.Lock 对账户余额与出钞的互斥保护 |
| 用户友好交互界面 | ATMState 状态机对流程的约束与错误提示 |
文档给出的七个核心构件(Card、Account、Transaction、BankingService、CashDispenser、ATM、ATMDriver)在 Python 实现中全部有对应文件,可以直接对照阅读。
二、核心类设计与源码走读
1. Card —— 卡片实体
card.py 是最简单的实体类,只承载两个不可变字段:
class Card:
def __init__(self, card_number: str, pin: str):
self._card_number = card_number
self._pin = pin
PIN 以字符串形式存储(示例中为 "1234")。真实系统中 PIN 应加密存储,本示例仅用于教学演示。
2. Account —— 账户与余额操作
account.py 实现了余额的存取,并引入了第一个并发安全点:
class Account:
def __init__(self, account_number: str, balance: float):
self._balance = balance
self._cards: Dict[str, Card] = {}
self._lock = threading.Lock()
def deposit(self, amount: float):
with self._lock:
self._balance += amount
def withdraw(self, amount: float) -> bool:
with self._lock:
if self._balance >= amount:
self._balance -= amount
return True
return False
- 每个
Account拥有独立的threading.Lock,存款/取款均在锁内完成,避免并发读写导致余额不一致。 withdraw先校验余额是否充足再扣减,返回bool表示是否成功;deposit直接累加。_cards记录了绑定到该账户的卡片,形成"账户 ↔ 卡片"的多对多关联。
3. OperationType —— 操作枚举
operation_type.py 用标准库 enum 定义了三种操作:
class OperationType(Enum):
CHECK_BALANCE = "CHECK_BALANCE"
WITHDRAW_CASH = "WITHDRAW_CASH"
DEPOSIT_CASH = "DEPOSIT_CASH"
4. BankService —— 银行后端门面
bank_service.py 扮演"银行后端系统",负责账户/卡片管理、认证与事务执行。构造函数中预置了两组演示数据:
account1 = self.create_account("1234567890", 1000.0)
card1 = self.create_card("1234-5678-9012-3456", "1234")
self.link_card_to_account(card1, account1)
account2 = self.create_account("9876543210", 500.0)
card2 = self.create_card("9876-5432-1098-7654", "4321")
self.link_card_to_account(card2, account2)
关键方法:
authenticate(card, pin):比较传入 PIN 与卡片存储 PIN 是否一致;authenticate_card(card_number):按卡号从_cards字典取回Card对象,卡不存在返回None;get_balance / withdraw_money / deposit_money:通过_card_account_map从卡反查账户,再委托给Account执行;link_card_to_account:建立卡片与账户的双向绑定。
文档中描述的 Transaction 抽象类(由 WithdrawalTransaction、DepositTransaction 继承)在 Go 实现中以 transaction.go 的形式存在,Python 版本则将取款/存款逻辑直接收敛到 ATM 层,属于同需求的不同实现风格,可对照学习。
5. CashDispenser 与责任链出钞
文档要求"现金分发器负责出钞并保证线程安全"。Python 实现拆成了两个文件:接口 dispense_chain.py 与实现 note_dispenser.py。
CashDispenser(cash_dispenser.py)持有链头并整体加锁:
class CashDispenser:
def __init__(self, chain: DispenseChain):
self._chain = chain
self._lock = threading.Lock()
def dispense_cash(self, amount: int):
with self._lock:
self._chain.dispense(amount)
def can_dispense_cash(self, amount: int) -> bool:
with self._lock:
if amount % 10 != 0:
return False
return self._chain.can_dispense(amount)
- 出钞前先调用
can_dispense_cash做可用性预检,取款金额必须为 10 的倍数; NoteDispenser实现dispense:优先用大面额,剩余金额转交_next_chain继续处理,形成责任链;can_dispense同样沿链递归判断面额组合能否凑出目标金额;- 三个具体节点以子类形式定义:
NoteDispenser100(10)、NoteDispenser50(20)、NoteDispenser20(30)(见 atm.py),即 10 张 $100、20 张 $50、30 张 $20。
6. ATM 门面 + 单例
atm.py 中的 ATM 是用户操作的门面,同时实现了线程安全的单例:
class ATM:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
双重检查加锁(Double-Checked Locking)确保多线程环境下只创建一个 ATM 实例。__init__ 中完成状态机、银行服务、出钞链的一次性组装。
取款流程(withdraw_cash)展示了"先预检、再扣账、失败回滚"的事务语义:
def withdraw_cash(self, amount: int):
if not self._cash_dispenser.can_dispense_cash(amount):
raise RuntimeError("Insufficient cash available in the ATM.")
self._bank_service.withdraw_money(self._current_card, amount)
try:
self._cash_dispenser.dispense_cash(amount)
except Exception as e:
self._bank_service.deposit_money(self._current_card, amount) # 出钞失败则回存
raise e
7. ATMState —— 状态模式约束交互
atm_state.py 定义了抽象基类与三个具体状态:
IdleState:等待插卡。插卡时按卡号查找卡片,找不到直接退卡,找到则进入HasCardState;HasCardState:等待输 PIN。PIN 正确进入AuthenticatedState,错误则退卡回到空闲;AuthenticatedState:执行操作。select_operation按OperationType分发到余额查询/取款/存款,完成一次交易后自动退卡回到IdleState。
非法操作(如未插卡就输 PIN、重复插卡、金额为负)在各状态中被拦截并打印明确错误,这正是文档"用户友好交互"需求的体现。状态转移调用 atm.change_state(...) 完成。
三、运行方式与完整演示
演示入口 atm_demo.py 与文档中的 ATMDriver 角色对应,依次执行:
cd solutions/python/atm
python atm_demo.py
演示脚本覆盖了主要路径与异常分支(使用默认账户 1234-5678-9012-3456 / PIN 1234,初始余额 $1000):
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 插卡 + PIN + 查余额 | 打印余额 $1000.00 |
| 2 | 插卡 + PIN + 取款 $570 | 出钞 5×$100 + 1×$50 + 1×$20,余额变 $430 |
| 3 | 插卡 + PIN + 存款 $200 | 余额变 $630 |
| 4 | 再查余额 | 打印余额 $630.00 |
| 5 | 取款 $700(超过余额) | 打印 "Insufficient balance" |
| 6 | 输入错误 PIN "3425" | 认证失败并退卡 |
从输出可以看到责任链的实际出钞分配:$570 由 $100 面额链节点先出 5 张,余 $70 交给 $50 节点出 1 张,最后 $20 节点出 1 张,剩余为 0 时完成。
四、设计模式与并发要点的归纳
这套 Python 实现是"状态模式 + 责任链模式 + 单例模式 + 线程安全"的组合样板,文档与源码的对应关系如下:
| 设计要点 | 模式/机制 | 源码位置 |
|---|---|---|
| 插卡 → 输 PIN → 操作 的流程状态机 | State Pattern | atm_state.py |
| 大额 → 小额递进出钞 | Chain of Responsibility | dispense_chain.py + note_dispenser.py |
| 全局唯一 ATM | Singleton(双重检查加锁) | atm.py |
| 余额与出钞并发安全 | threading.Lock 互斥 |
account.py / cash_dispenser.py |
| 事务失败回滚 | 先扣账、出钞失败回存 | atm.py |
五、与其他语言实现的横向对照
同一套需求在仓库中还有多种实现,适合对比学习"同样的设计如何在不同语言中表达":
- Java:完整拆分为实体、状态、责任链三个子包,见 solutions/java/src/atm;
- C#:按 DispenserChain / Enums / Models / Service / State 目录组织,见 solutions/csharp/atm;
- C++:ATM/Account 头文件与实现分离,见 solutions/cpp/atm;
- Go:以
transaction.go显式建模 Transaction 抽象,见 solutions/golang/atm。
各实现的类图可参考仓库 class-diagrams/atm-class-diagram.png,它直观展示了 Card、Account、BankService、CashDispenser、ATM 与状态类之间的静态关系。
六、可延伸的改进方向
基于现有代码结构,可推断出以下增强点(均为合理的工程推演,非仓库已实现功能):
- PIN 加密与失败次数限制:目前 PIN 明文比较、可无限重试;可引入哈希存储与"三次错误锁卡"策略;
- Transaction 抽象层:若需记录交易流水、做持久化,可参照 Go 版把取款/存款收敛为
Transaction子类,便于审计与回滚; - 更细粒度的并发控制:当前用粗粒度锁保护整个出钞链,高并发场景可改为按面额分锁;
- 真实银行后端对接:
BankService目前是进程内模拟,可替换为 RPC/HTTP 客户端而不改动ATM门面。
这些方向都建立在文档"并发访问与数据一致性"与"与银行后端交互"两条需求的延伸之上。
参考路径速查
- 文档主体:solutions/python/atm/README.md
- 问题与类图说明:problems/atm.md
- Python 源码目录:solutions/python/atm/
- 类图:class-diagrams/atm-class-diagram.png