☰
从零设计自动售货机(Vending Machine):需求、类设计与 C++ 实现深度解析
2026/10/1 1:58:55 网站建设 项目流程
  • 示例工程

【免费下载链接】awesome-low-level-design

Learn Low Level Design (LLD) and prepare for interviews using free resources.

项目地址:https://gitcode.com/GitHub_Trending/aw/awesome-low-level-design
点击查看免费下载

本文以 awesome-low-level-design 仓库中的 自动售货机 C++ 解决方案 为主线,系统拆解低层设计(LLD)面试中经典的 Vending Machine 问题:从需求澄清、核心类与枚举划分,到 C++ 源码级别的库存管理、交易流程、异常处理与演示程序。读完本文,你将掌握如何用领域建模 + 状态机思想(State Pattern)+ 单例约束(Singleton Pattern)组织一个多商品、多面额、支持并发与补货的售货机系统,并能够在面试中清晰复述其设计取舍。

一、需求分析:把模糊问题转化为可验证的功能清单

原文档开篇即给出 7 条需求,这是整个设计的"验收标准"。在做 LLD 面试题时,第一步永远是澄清需求边界,而非直接写代码。这 7 条可以归纳为四类核心关注点:

  1. 商品管理:支持多种商品,且不同商品具有不同价格与库存数量。
  2. 支付与找零:接受不同面额的硬币(Coin)与纸币(Note),商品售出后如付款有盈余需返回找零。
  3. 交易与一致性:记录可用商品与数量变化,支持并发交易并保证数据一致性。
  4. 运维接口与异常场景:提供补货(restock)与收款(collect money)接口;对资金不足(insufficient funds)、商品缺货(out-of-stock)等异常场景有明确处理。

对照仓库中的 C++ 实现 VendingMachine.cpp,这些需求被逐一落地为方法签名:

  • 商品管理 →addProduct(name, price, quantity)、removeProduct(productId)、restockProduct(productId, quantity)、updatePrice(productId, price);
  • 支付与找零 →purchaseProduct(productId, quantity, payment)(付款额与总价比较后多退少补由调用方结算);
  • 运维 →addCash(amount)、withdrawCash(amount)、setOperational(status);
  • 异常场景 →purchaseProduct对"不可用商品""库存不足""付款不足"均返回nullptr,由调用方感知失败。

二、核心类、接口与枚举的设计骨架

原文档第 12 行起定义了设计骨架,共 8 个核心构件。理解这套命名对后续阅读任何一种语言实现都至关重要:

构件职责
Product表示一件商品,属性含名称、价格、数量等
Coin / Note 枚举描述售货机接受的不同硬币与纸币面额
Inventory管理商品与库存数量,使用并发哈希表(concurrent hash map)保证线程安全
VendingMachineState 接口定义 idle、ready、dispense 等状态下机器的行为
IdleState / ReadyState / DispenseState状态接口的具体实现,封装各状态下的行为差异
VendingMachine主类,遵循 Singleton 模式保证全系统唯一实例
VendingMachineDemo演示程序:加商品、选商品、投币、出货、找零全流程

需要特别说明的是:原文档描述的是"理想化设计"(State Pattern + Singleton + 并发 Inventory),而本仓库的 C++ 版实现是一套"教学简化版"——它以Product、Transaction、VendingMachine三个类直接落地,未引入枚举与状态类。但仓库中 Go 版实现(含coin.go、note.go、inventory.go、state.go)、C# 版实现(含Enum/Coin.cs、States/IdleState.cs、States/HasMoneyState.cs、States/DispensingState.cs)、以及 Java 版 vendingmachine 和 TypeScript 版 VendingMachine 则完整实现了文档描述的状态机与枚举设计。因此下文以 C++ 实现为主干做逐类分析,并交叉对照文档骨架与多语言实现,帮助读者理解"文档设计"与"具体落地"的差异。

三、Product 类:商品模型的领域边界

Product.hpp 将商品建模为五个私有字段与一组操作:

class Product { private: std::string productId; std::string name; double price; int quantity; bool available; public: Product(std::string productId, std::string name, double price, int quantity = 0); std::string getProductId() const; std::string getName() const; double getPrice() const; int getQuantity() const; bool isAvailable() const; void setPrice(double price); void setQuantity(int quantity); void setAvailable(bool status); void addQuantity(int amount); bool removeQuantity(int amount); void displayInfo() const; };

关键设计点:

  • isAvailable()是派生状态而非独立事实。在 Product.cpp 中,它的实现是available && quantity > 0,即"上架标志 + 库存大于零"同时成立才算可售。这避免了available布尔值与quantity相互矛盾的数据不一致问题。
  • removeQuantity(int amount)带前置校验:if (amount <= quantity)才扣减并返回true,否则返回false不动库存。这是对"out-of-stock 异常场景"的第一道防线。
  • displayInfo()输出标准化:使用<iomanip>的std::fixed << std::setprecision(2)将价格格式化为两位小数,避免浮点输出的杂乱,属于工程细节上的加分项。

在文档设计的完整版中,Product 还应与Coin/Note枚举配合:机器只接受枚举中列出的面额,这为"找零可行性"提供了建模基础——真实售货机还需检查硬币箱中是否有足够零钱,这是 C++ 简化版未涉及、但面试中可主动提出的扩展点。

四、Transaction 类:让交易"可审计"

C++ 实现额外引入了一个原文档未单列但极具价值的类——Transaction(Transaction.hpp):

class Transaction { private: std::string transactionId; std::string productId; int quantity; double amount; std::time_t timestamp; bool successful; public: Transaction(std::string transactionId, std::string productId, int quantity, double amount); bool isSuccessful() const; void setSuccessful(bool status); void displayInfo() const; };

它对应需求第 4 条"keep track of the available products and their quantities"的延伸——不仅要追踪库存,还要追踪每一笔买卖。构造时 Transaction.cpp 自动打上std::time(nullptr)时间戳,且successful默认置false,只有真正扣库存成功后才由setSuccessful(true)落定。这种"先记账、后确认"的模式保证了失败交易不会被记为成功。

五、VendingMachine 主类:库存、收银与交易编排

VendingMachine.hpp 是系统的中枢,维护机器状态、商品集合、交易历史与现金余额:

class VendingMachine { private: std::string machineId; std::vector<Product*> products; std::vector<Transaction*> transactions; double cashBalance; bool operational; int productIdCounter; int transactionIdCounter; public: VendingMachine(std::string machineId); ~VendingMachine(); Product* addProduct(const std::string& name, double price, int quantity = 0); void removeProduct(const std::string& productId); bool restockProduct(const std::string& productId, int quantity); bool updatePrice(const std::string& productId, double price); Transaction* purchaseProduct(const std::string& productId, int quantity, double payment); void addCash(double amount); bool withdrawCash(double amount); void setOperational(bool status); // displayInventory / displayTransactions / displayMachineInfo };

5.1 交易主流程purchaseProduct:需求 3 的核心链路

VendingMachine.cpp 中的purchaseProduct是全文最值得精读的 20 行,它一次覆盖了 5 条需求:

Transaction* VendingMachine::purchaseProduct(const std::string& productId, int quantity, double payment) { if (!operational) return nullptr; // 机器停运 → 拒绝 Product* product = findProduct(productId); if (!product || !product->isAvailable() || product->getQuantity() < quantity) return nullptr; // 商品不存在/未上架/库存不足 double totalCost = product->getPrice() * quantity; if (payment < totalCost) return nullptr; // 付款不足(需求 7) std::string transactionId = generateTransactionId(); Transaction* transaction = new Transaction(transactionId, productId, quantity, totalCost); if (product->removeQuantity(quantity)) { // 原子扣库存(需求 4) cashBalance += totalCost; // 收款入账(需求 6) transaction->setSuccessful(true); transactions.push_back(transaction); return transaction; } delete transaction; return nullptr; }

流程可概括为五道防线:停运检查 → 商品存在性与可售性检查 → 库存数量检查 → 付款额检查 → 扣减库存并记账。付款与总价之差即"应找零金额",由调用方读取payment - totalCost结算,对应需求第 3 条"return change if necessary"。

5.2 收银与运维接口(需求 6)

  • addCash(amount):收银员投币/收款时增加现金余额;
  • withdrawCash(amount):带amount <= cashBalance校验的取款,对应"collect money";
  • setOperational(bool):一键切换机器运营/停运状态,配合purchaseProduct第一道防线形成"维护模式"。

5.3 资源管理与 ID 生成

构造函数将cashBalance初始化为 0、operational为true,并让productIdCounter/transactionIdCounter从 1 递增;generateProductId()返回"P" + 序号、generateTransactionId()返回"T" + 序号,保证 ID 在单机范围内唯一且可读。析构函数遍历delete所有堆上的Product*与Transaction*,避免内存泄漏——这是手写 C++ 版本必须交代的细节。

六、演示程序:完整走一遍业务流程

VendingMachineDemo.cpp 演示了从建机到结算的完整闭环,可直接作为面试回答的"运行示例":

VendingMachine machine("VM001"); Product* cola = machine.addProduct("Cola", 2.50, 10); // 可乐 10 罐 Product* chips = machine.addProduct("Chips", 1.50, 15); // 薯片 15 包 Product* candy = machine.addProduct("Candy", 1.00, 20); // 糖果 20 颗 machine.displayMachineInfo(); // 初始状态:ID、运营状态、现金余额、商品数、交易数 machine.displayInventory(); // 逐一打印商品明细 Transaction* t1 = machine.purchaseProduct(cola->getProductId(), 2, 5.00); // 买 2 罐可乐:总价 2.50*2 = 5.00,付款 5.00,正好不用找零 Transaction* t2 = machine.purchaseProduct(chips->getProductId(), 3, 5.00); // 买 3 包薯片:总价 1.50*3 = 4.50,付款 5.00,应找零 0.50 machine.displayTransactions(); // 查看两笔成功交易记录 machine.restockProduct(cola->getProductId(), 5); // 补货 5 罐可乐 machine.updatePrice(candy->getProductId(), 1.25); // 糖果涨价到 1.25 machine.displayMachineInfo(); // 最终状态

运行该文件(g++ VendingMachineDemo.cpp VendingMachine.cpp Product.cpp Transaction.cpp -o demo && ./demo)可观察如下关键行为:两笔交易后cashBalance变为 9.50(5.00 + 4.50)、可乐库存从 10 变为 7(补货后又回到 12)、Candy 价格更新为 1.25——每一步输出都直接对应需求清单中的一条。

七、设计模式与并发考量(文档骨架 vs 简化实现)

原文档明确点名了两个模式,这是 Vending Machine 题目的标准考点:

7.1 State Pattern:从"if-else 泥潭"到状态对象

文档要求的VendingMachineState接口 +IdleState/ReadyState/DispenseState实现,本质是把"投币、选品、出货、找零"过程中的状态流转从if/else判断中解放出来:每个状态类自己实现insertCoin、selectProduct、dispense等行为,机器只持有"当前状态"引用,状态间通过转移方法切换。参考仓库中 C# 版 States 目录(IdleState→HasMoneyState→ItemSelectedState→DispensingState)与 Go 版 state.go,可以看到这一模式的完整形态;C++ 简化版用purchaseProduct内的顺序校验模拟了状态约束(停运→不可售→库存→金额),面试中应指出两者的演进关系。

7.2 Singleton Pattern:机器实例的唯一性

文档第 6 条要求 VendingMachine 遵循 Singleton,保证"一台物理售货机只有一个控制实例"。C++ 简化版通过main中单一对象构造规避了此问题;完整实现可参考仓库 design-patterns 目录 下各语言的 singleton 示例(含 C++ 多线程单例 的 double-checked / thread-safe 版本)。C++ 中推荐 Meyers' Singleton(函数局部静态变量),C++11 起保证线程安全的构造。

7.3 并发一致性:文档与实现之间的"设计债务"

原文档第 5 条要求"handle multiple transactions concurrently and ensure data consistency",并指明 Inventory 使用并发哈希表。这一点在 C++ 简化版中尚未落实——std::vector<Product*>与cashBalance += totalCost并非线程安全,removeQuantity的"检查-扣减"也不是原子操作。面试中应主动补充:用std::mutex或std::shared_mutex保护purchaseProduct临界区,或将库存改为std::atomic<int>配合无锁设计。对照 Go 版 inventory.go 的并发 map 实现,可以直观看到文档设想的生产级做法。

八、多语言对照:同一设计、不同表达

该问题在仓库中有六种语言的完整实现,学习时可以横向对比"同一份设计骨架在不同语言中的形态":

  • C++ 实现:本文主体,Product/Transaction/VendingMachine三类的简化落地;
  • Java 实现:包结构 + 状态类组织最接近文档描述;
  • Python 实现:以 md 文档形式给出思路;
  • C# 实现:完整状态机(4 个 State)+ Coin 枚举 + Inventory 模型;
  • Go 实现:coin.go/note.go/inventory.go/state.go拆分为独立文件,最贴合"并发哈希表"描述;
  • TypeScript 实现:前端/全栈场景下的面向对象写法。

配套的问题描述文档见 problems/vending-machine.md,其中还附有 UML 类图,可作为面试时的架构图速查。

九、面试追问与扩展方向

基于以上源码分析,以下是本题常见追问及参考答案:

  1. 找零逻辑缺失怎么办?简化版只比较payment >= totalCost;生产级需引入Coin/Note面额枚举与"硬币箱余额"模型,用贪心算法判断能否凑出找零,凑不出则拒绝交易并退款。
  2. 如何支持并发?用锁保护交易临界区;或用 Go 的sync.Map管理库存(见 inventory.go);再进一步可引入事务日志保证崩溃恢复。
  3. 商品过期如何建模?为Product增加expiryDate字段并在isAvailable()中联动判断——这正是"派生状态"思想的延伸。
  4. 机器停运期间怎么处理已投入的钱?第一道防线if (!operational) return nullptr直接拒绝交易,投币口在停运时物理上应不可用。

结语

本文从需求清单出发,完整剖析了自动售货机的领域建模:Product的派生可售状态、Transaction的可审计记账、VendingMachine的五道交易防线与收银运维接口,并通过演示程序验证了全流程。与此同时,对照文档骨架指出 State Pattern、Singleton 与并发一致性在简化实现与完整实现间的差异——这正是 LLD 面试最看重的"知道设计目标、也清楚落地差距"的能力。下一步建议打开仓库中的 Go 实现 或 C# 实现,亲手把状态机补全到 C++ 版本中,完成从"读得懂"到"写得对"的跨越。

  • 示例工程

【免费下载链接】awesome-low-level-design

Learn Low Level Design (LLD) and prepare for interviews using free resources.

项目地址:https://gitcode.com/GitHub_Trending/aw/awesome-low-level-design
点击查看免费下载
上一篇:Flink 在 Kubernetes 上的 Standalone 部署完全指南:Session 集群、Application 集群与高可用配置
下一篇:从WebGL到WebGPU:egui渲染引擎升级全解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询