设计 ATM 系统(ATM System):面向面试的低层设计(LLD)完整实战指南

原创2026-09-30 19:54:311,228 阅读
文章标签:示例工程

设计 ATM 系统(ATM System):面向面试的低层设计(LLD)完整实战指南

导读

本文以 problems/atm.md 为骨架,完整拆解 ATM 系统的低层设计(Low Level Design,LLD)题目:从需求分析、UML 类图到类/接口/枚举划分,并深入 awesome-low-level-design 仓库中 Java、C++、Go、C# 等多语言实现源码,讲解认证、余额查询、取款、存款、并发安全与现金分发的具体落地方式。读完本文,你将掌握 ATM 系统的对象建模方法,并能在面试中围绕"状态管理 + 并发控制 + 责任链出钞"展开有依据的设计论述。


一、需求分析:先界定系统的边界

在动手画类图之前,先明确系统的功能范围与约束。原题给出了 6 条核心需求,它们共同定义了 ATM 系统的最小可行能力:

  1. 基础操作:ATM 系统应支持余额查询(balance inquiry)、取款(cash withdrawal)和存款(cash deposit)。
  2. 身份认证:用户应能通过**银行卡(Card)+ 个人识别码(PIN)**完成身份认证。
  3. 银行后端交互:系统应与银行的后端系统交互,用于校验用户账户并执行交易。
  4. 现金出钞:ATM 应配备**现金分发器(Cash Dispenser)**向用户吐钞。
  5. 并发与一致性:系统应处理并发访问并保证数据一致性。
  6. 友好交互:ATM 应提供用户友好的界面供用户交互。

从面试角度看,这 6 条需求分别对应了设计的几个关注点:核心领域模型(账户/卡片/交易)、安全认证流程、外部系统集成边界、硬件抽象(出钞机)、并发安全(多 ATM 或多线程同时操作)、以及交互流程(状态机)。后续的所有类设计都应能追溯到这些需求,这是 LLD 面试中"需求驱动设计"的基本功。


二、整体架构与 UML 类图

原文档配套的类图位于 class-diagrams/atm-class-diagram.png,它勾勒了系统的静态结构:ATM 作为门面对外提供操作入口,内部协作 BankingService(银行业务)、CashDispenser(出钞硬件),并由 Card、Account、Transaction 及其子类构成领域模型。

ATM 系统 UML 类图,展示 ATM、BankingService、CashDispenser、Card、Account 与 Transaction 等核心类的关系

三、核心类、接口与枚举:逐一定义

原文档明确了系统的类清单,下面逐类展开,并结合仓库实现说明其职责。

3.1 Card(银行卡)

Card 类代表一张 ATM 卡,包含卡号(card number)和 PIN。

在 Java 实现 entities/Card.java 中,Card 只持有 cardNumber 与 pin 两个字段;而 BankService.java 通过 createCard 创建卡片,并通过 linkCardToAccount 将卡片与账户建立映射。关键设计决策:卡片本身不校验 PIN,认证校验委托给银行服务(见 3.4),这保持了"卡是凭据、银行是权威"的职责划分。

3.2 Account(银行账户)

Account 类代表银行账户,包含账号(account number)与余额(balance),并提供**借记(debit)/ 贷记(credit)**余额的方法,即取款扣减、存款增加。

在 C++ 实现 Account.hpp 中,账户同时持有 accountNumber、pin、balance 三个私有字段,构造时默认余额为 0.0:

Account(std::string accountNumber, std::string pin, double initialBalance = 0.0);
bool deposit(double amount);
bool withdraw(double amount);
double getBalance() const;

在 Java 实现中,账户由 BankService.createAccount 创建(见 BankService.java),初始余额作为构造参数传入,例如 createAccount("1234567890", 1000.0)。

3.3 Transaction 抽象基类与子类(WithdrawalTransaction / DepositTransaction)

Transaction 是不同交易类型(取款、存款)的抽象基类,由 WithdrawalTransaction 和 DepositTransaction 两个子类扩展。这是典型的策略/模板模式的雏形:所有交易共享"交易 ID + 目标账户 + 金额"的结构,而具体的记账语义由子类实现。

在 Go 实现 transaction.go、withdrawal_transaction.go、deposit_transaction.go 中,交易被建模为携带金额与账户的领域对象,由 BankingService.ProcessTransaction 统一执行;Go 版还单独维护了 errors.go 统一错误语义(如余额不足、账户不存在)。交易 ID 由 atm.go 中的 generateTransactionID 生成:用 atomic.AddInt64 递增计数器并结合时间戳,格式为 TXN<yyyyMMddHHmmss><10位序号>,从源码结构看这保证了交易号在高并发下的唯一性与可追溯性。

3.4 BankingService(银行服务)

BankingService 管理银行账户并处理交易。原文档特别强调它使用线程安全的 ConcurrentHashMap 存储与检索账户信息——这是针对需求 5(并发访问 + 数据一致性)的直接回应。

Java 实现 entities/BankService.java 使用三个 ConcurrentHashMap 分别维护 accounts(账号 → 账户)、cards(卡号 → 卡片)、cardAccountMap(卡片 → 账户)三个映射:

private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private final Map<String, Card> cards = new ConcurrentHashMap<>();
private final Map<Card, Account> cardAccountMap = new ConcurrentHashMap<>();

它对外提供 createAccount、createCard、linkCardToAccount、authenticate、getBalance、withdrawMoney、depositMoney 等方法。值得注意的设计点:

  • 认证逻辑在这里:authenticate(Card card, String pin) 直接比较 card.getPin().equals(pin),即银行服务才是 PIN 校验的权威方。
  • 卡片与账户解耦:通过 cardAccountMap 做多对一映射(一个账户可绑定多张卡),这种设计比在 Account 里硬编码单张卡更贴近真实银行体系。

Go 实现对应文件为 banking_service.go,职责与 Java 版对齐。

3.5 CashDispenser(现金分发器)

CashDispenser 代表 ATM 的出钞硬件,负责分发现金。原文档要求它使用同步机制保证出钞的线程安全(需求 5)。

Java 实现 entities/CashDispenser.java 用 synchronized 修饰 dispenseCash 与 canDispenseCash,并且**引入责任链模式(Chain of Responsibility)**来完成面额组合:

public synchronized void dispenseCash(int amount) {
    chain.dispense(amount);
}

public synchronized boolean canDispenseCash(int amount) {
    if (amount % 10 != 0) {
        return false;
    }
    return chain.canDispense(amount);
}

出钞链在 ATMSystem.java 的构造器中组装:100 美元 ×10 张 → 50 美元 ×20 张 → 20 美元 ×30 张(NoteDispenser100/NoteDispenser50/NoteDispenser20,见 chainofresponsibility 目录)。canDispenseCash 首先校验金额必须是 10 的倍数(因为最小面额为 $20,链条按 $10 粒度约束),再沿链询问各面额能否凑出。

3.6 ATM 门面类

ATM 类是 ATM 操作的主入口(门面 Facade),负责协调 BankingService 与 CashDispenser,向用户暴露认证、余额查询、取款、存款四类操作。它屏蔽了底层银行服务与出钞硬件的复杂度。

  • C++ 版 ATM.hpp 将 ATM 建模为持有账户列表、当前账户与会话状态的类,提供 authenticate / logout / deposit / withdraw / checkBalance / start。
  • Java 版 ATMSystem.java 采用单例 + 状态模式:ATMSystem 通过 getInstance() 保证全局唯一实例,内部维护 ATMState currentState,将插卡、输 PIN、选操作都委托给当前状态对象处理。
  • Go 版 atm.go 同样聚合 BankingService 与 CashDispenser,且把"取款 = 记账 + 出钞"拆成两步:先 bankingService.ProcessTransaction,成功后再 cashDispenser.DispenseCash,保证账务与现金不会出现"扣了钱没出钞"的割裂。

3.7 ATMDriver / ATMDemo(驱动演示类)

ATMDriver 类用于演示系统用法:创建示例账户并执行 ATM 操作。它同时也是面试中的"自测入口"。

  • C++ 版 ATMDemo.cpp 创建两个测试账户并启动交互式菜单:
ATM atm;
Account* account1 = new Account("1234", "5678", 1000.0);
Account* account2 = new Account("4321", "8765", 2000.0);
atm.addAccount(account1);
atm.addAccount(account2);
atm.start();
  • Java 版 ATMDemo.java 走完整用例:插卡 → 输 PIN → 查余额 → 取款 → 存款 → 退卡。

四、多语言实现对照与运行方式

原文档给出了 5 个语言实现入口,全部存在于本仓库中:

语言 源码路径 特点
Java solutions/java/src/atm/ 单例 + 状态模式 + 责任链出钞,结构最完整
Python solutions/python/atm/ 轻量示例(含说明文档)
C++ solutions/cpp/atm/ 类层次简洁,交互式控制台程序
C# solutions/csharp/atm/ .NET 风格实现
Go solutions/golang/atm/ 交易对象 + 统一错误处理

以 C++ 版为例,编译运行方式(仓库为只读,仅演示查看与本地运行):

cd solutions/cpp/atm
g++ ATM.cpp Account.cpp ATMDemo.cpp -o atm
./atm

运行后按提示输入账号 1234、PIN 5678 即可进入菜单,依次体验查余额(1)、存款(2)、取款(3)、登出(4)、退出(5)流程。C++ 版 start() 的交互循环见 ATM.cpp 的 displayMenu/start:每个操作前都会检查 isAuthenticated 与 currentAccount,未认证时只允许输入账号 PIN 完成认证,体现了"先认证、后操作"的会话控制。

Java 版在 solutions/java 目录下按工程结构编译运行,入口为 atm.ATMDemo。


五、源码级深度剖析:三大设计主题

5.1 并发安全:从容器到出钞的全链路同步

需求 5 的落地体现在三个层次:

  1. 数据容器层:Java 用 ConcurrentHashMap(BankService.java)保证多线程读写账户映射无竞态。
  2. 硬件抽象层:CashDispenser.dispenseCash/canDispenseCash 加 synchronized(CashDispenser.java),保证同一时刻只有一个线程在操作出钞硬件。
  3. 交易号生成层:Go 用 sync/atomic 的 AddInt64 生成全局递增交易号(atm.go),避免多个并发现场产生重复交易 ID。

这组实现清晰地展示了 LLD 中"并发控制要贴着资源边界做"的原则:哪些资源是共享的(账户映射、出钞硬件),就在哪些资源上加锁。

5.2 会话与状态:用状态模式管理 ATM 流程

Java 版把"插卡 → 输 PIN → 选操作 → 退卡"建模为状态机,状态接口与实现位于 state 目录:

  • ATMState(接口):定义 insertCard / enterPin / selectOperation / ejectCard。
  • IdleState(空闲态):插卡后查询卡片,卡片不存在则直接退卡(IdleState.java)。
  • HasCardState(已插卡态):等待用户输入 PIN。
  • AuthenticatedState(已认证态):允许执行余额查询、取款、存款等敏感操作。

ATMSystem.changeState 负责状态切换(ATMSystem.java)。从源码结构看,这种设计的价值是:非法操作在状态层面就被拦截——比如未插卡时调用 enterPin,IdleState 直接打印"Error: Please insert a card first.",而不是进入业务逻辑。这比在 ATM 门面里堆一堆 if (state == ...) 更易扩展(新增"维护模式""吞卡状态"只需新增状态类)。

5.3 出钞面额组合:责任链模式

CashDispenser 内部挂载的出钞链(chainofresponsibility 目录)是责任链模式的经典应用:

  • DispenseChain 定义链节点接口(setNextChain / dispense / canDispense)。
  • NoteDispenser100 / NoteDispenser50 / NoteDispenser20 各自只处理一种面额,处理不了就交给下一个节点。
  • 链的组装顺序即"贪心出钞"策略:优先大面额,剩余金额顺延小面额,如 ATMSystem 构造器中的 100×10 → 50×20 → 20×30。

同时 Java 的 withdrawCash 还做了补偿处理(ATMSystem.java):如果出钞阶段抛异常,会把已扣金额 depositMoney 退回账户,避免"扣账成功但未吐钞"的资金损失。这一细节体现了真实系统中"跨组件操作要具备回滚/补偿能力"的设计意识,是面试中的加分点。

5.4 取款流程的完整调用链(Java 视角)

综合上述分析,一次成功的取款操作在 Java 实现中的调用链为:

  1. ATMSystem.selectOperation(OperationType.WITHDRAW_CASH, amount) 委托给当前状态(AuthenticatedState)。
  2. 状态内部调用 atmSystem.withdrawCash(amount)。
  3. withdrawCash 先调用 cashDispenser.canDispenseCash(amount) 预检金额(面额合法性 + 库存是否足够)。
  4. 通过后调用 bankService.withdrawMoney(currentCard, amount) 完成扣账。
  5. 最后 cashDispenser.dispenseCash(amount) 触发责任链出钞;失败则回滚扣账。

操作类型枚举定义于 enums/OperationType.java。这条链路把本文 3.4 / 3.5 / 3.6 的职责串成闭环,可直接用于面试中的"走一遍流程"环节。


六、总结:从 ATM 题提炼的通用 LLD 方法论

ATM 系统是一道高频 LLD 题,它覆盖了面试官最常考察的几个维度,可迁移到其他系统设计:

  • 领域建模:Card / Account / Transaction(含子类)的划分,回答了"谁持有数据、谁拥有行为"。
  • 外部依赖抽象:BankingService(银行后端)与 CashDispenser(硬件)都被抽象成可替换的服务,便于测试与扩展。
  • 并发与一致性:ConcurrentHashMap、synchronized、原子计数器,分别解决容器、硬件、ID 三类共享资源的竞态。
  • 流程状态管理:状态模式把 ATM 的会话生命周期显式化。
  • 算法与模式的组合:责任链解决面额组合,单例保证系统实例唯一,门面隔离复杂度。

对照 problems/atm.md 的需求清单,你可以按"需求 → 类 → 模式 → 并发 → 演示"的顺序在面试中复述完整设计,并结合本仓库的 Java 实现、C++ 实现、Go 实现 与 C# 实现 进行跨语言对比,展示你对设计的理解深度。

登录后查看全文
awesome-low-level-design