如何用Java设计一个自动售货机程序是一个非常好的Java面试题。大多数情况会在面试比较senior的Java开发者的时候出现。在一个典型的代码面试中,你需要在一定的时间内根据对应的条件完成相关的代码。通常2到3小时内(面试哪有这么多时间,哈哈),你需要产生设计文档,可以工作的代码已经单元测试。这样的Java面试的好处就是你能够一次性检测面试者的很多能力。为了能够完成代码的设计,编码以及单元测试,面试者需要在这三个方面都比较精通。
另外,这种真实的问题可以提升你面向对象分析和设计能力的技能,假如你想成为一个很好的应用开发者,那么这个技能就很重要。
要想用Java或者别的面向对象的语言来设计一个自动售货机,你不仅仅需要了解最基本的东西,比如封装(Encapsulation),多态(Polymorphism)或者继承(Inheritance),你还需要理解如何使用抽象类和接口的细节,这样才能解决问题或者设计一个好的应用。
通常这种问题,还会给你一个使用设计模式的机会,因为在这个问题中你可以使用工厂模式去创建不同的售货机。我在20个Java软件开发的问题一文中曾今讨论过这个问题,那之后,我收到了很多反馈关于解决这个问题的方案。
这篇文章,我们将会提供一个自动售货机问题的解决方案。顺便说一下,其实这个问题有很多种解决的方案,你应当在看本文之前自己先尝试一下。你也需要先复习一下SOLID和OOPS的设计原则,我们会在代码中使用到他们。当你设计自动售货机的时候,你会发现我们会用到其中很多的相关内容。
另外,假如你对设计模式和原则感兴趣,我推荐你看看Udemy的“Java设计模式”这门课。这门课包括SOLID的设计模式,比如开闭原则(open closed)以及里氏替代(Liskov Substitution),当然也包括所有的面向对象的设计模式,比如装饰,观察,责任链等等。
问题陈述
你需要设计一个这样的自动售货机:
- 能够接收1分钱,5分钱,10分钱以及25分钱等等。
- 允许客户来选择产品,比如可乐(25), 百事(35),苏打(45)
- 允许用户取消请求来退钱
- 返回选择的产品,并且找零(假如需要的话)
- 允许售货机的提供商做reset的操作。
需求部分是这个问题最重要的部分。你需要仔细地阅读这个部分,然后对这个问题有一个高层的理解,然后思考如何来解决它。通常来说,需求部分都是不明确的,你需要通过阅读问题的陈述来列出一系列的你自己理解的需求。
我们喜欢指出基本的需求,因为他们很容易来跟踪。一些需求是很隐式的,我们最好把他们在你的列表中显式列出来。比如在这个问题中,假如售货机没有足够的零钱来找回,那么他就不应该接收相应的请求。
很不幸,没有什么课本或者课程来告诉你这些,你只有在真实的环境中做过这些才能知道。当然,有两本书曾帮助我改进我的面向对象分析和设计的能力,他们是《深入浅出面向对象分析和设计》 (Head First Object Oriented Design and Analysis),假如你没有相关的面向对象编程的经验,那么这本书非常值得推荐。
另外一本书是UML for Java Progrmmers,它是一本开发应用和系统设计方面非常好的书,值得推荐。它的做着是Robert C. Martin。我已经读过他的很多本书,比如Clean Code, Clean Coder以及一本关于使用Agile进行软件开发的书。他在OOP方面的教学大概是最好的。
这本书有一个类似的问题:设计一个咖啡机。因此,假如你想有更多的实践,或者希望提升你的面向对象的设计能力,你可以参考那个问题。那个问题也是一个很好的学习的作业。
方案和代码
我的关于售货机的Java实现包括以下的类和接口:
VendingMachine
它定义了售货机的所有公用API,通常所有的高级功能应该都在这个类里面。
VendingMachineImpl
售货机的示例实现
VendingMachineFactory
这是一个工厂类,用来创建不同的售货机
Item
Java的Enum关于售货机服务的项目
Inventory
这个类用来展示库存,用来创建售货机中的案例和物品清单。
Coin
一个Java Enum用来表示支持的货币。
Bucket
一个用于容纳两个对象的参数化类。
NotFullPaidException
这是一个exception,主要用来表示一个用户选择了一个项目,但是没有付足够的钱。
NotSufficientChangeException
这个Exception用来表示售货机没有钱的用来找零。
SoldOutException
当用户选择一个已经卖完了的产品时,会抛出这个exception
怎样在Java中设计售货机
下面就是完整的代码,你可以测试一下这个代码,如果有什么问题告诉我。
VendingMachine.java
它定义了售货机的所有公用API,通常所有的高级功能应该都在这个类里面。
package vending;
import java.util.List;
/**
* Decleare public API for Vending Machine
*/
public interface VendingMachine {
public long selectItemAndGetPrice(Item item);
public void insertCoin(Coin coin);
public List<Coin> refund();
public Bucket<Item, List<Coin>> collectItemAndChange();
public void reset();
}
VendingMachineImpl.java
一个VendingMachine接口实现示例,你可以在你的办公室,公交车站,火车站以及公共的地方看到他
package vending;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Sample implementation of Vending Machine in Java
* @author Javin Paul
*/
public class VendingMachineImpl implements VendingMachine {
private Inventory<Coin> cashInventory = new Inventory<Coin>();
private Inventory<Item> itemInventory = new Inventory<Item>();
private long totalSales;
private Item currentItem;
private long currentBalance;
public VendingMachineImpl(){
initialize();
}
private void initialize(){
//initialize machine with 5 coins of each denomination
//and 5 cans of each Item
for(Coin c : Coin.values()){
cashInventory.put(c, 5);
}
for(Item i : Item.values()){
itemInventory.put(i, 5);
}
}
@Override
public long selectItemAndGetPrice(Item item) {
if(itemInventory.hasItem(item)){
currentItem = item;
return currentItem.getPrice();
}
throw new SoldOutException("Sold Out, Please buy another item");
}
@Override
public void insertCoin(Coin coin) {
currentBalance = currentBalance + coin.getDenomination();
cashInventory.add(coin);
}
@Override
public Bucket<Item, List<Coin>> collectItemAndChange() {
Item item = collectItem();
totalSales = totalSales + currentItem.getPrice();
List<Coin> change = collectChange();
return new Bucket<Item, List<Coin>>(item, change);
}
private Item collectItem() throws NotSufficientChangeException,
NotFullPaidException{
if(isFullPaid()){
if(hasSufficientChange()){
itemInventory.deduct(currentItem);
return currentItem;
}
throw new NotSufficientChangeException("Not Sufficient change in
Inventory");
}
long remainingBalance = currentItem.getPrice() - currentBalance;
throw new NotFullPaidException("Price not full paid, remaining : ",
remainingBalance);
}
private List<Coin> collectChange() {
long changeAmount = currentBalance - currentItem.getPrice();
List<Coin> change = getChange(changeAmount);
updateCashInventory(change);
currentBalance = 0;
currentItem = null;
return change;
}
@Override
public List<Coin> refund(){
List<Coin> refund = getChange(currentBalance);
updateCashInventory(refund);
currentBalance = 0;
currentItem = null;
return refund;
}
private boolean isFullPaid() {
if(currentBalance >= currentItem.getPrice()){
return true;
}
return false;
}
private List<Coin> getChange(long amount) throws NotSufficientChangeException{
List<Coin> changes = Collections.EMPTY_LIST;
if(amount > 0){
changes = new ArrayList<Coin>();
long balance = amount;
while(balance > 0){
if(balance >= Coin.QUARTER.getDenomination()
&& cashInventory.hasItem(Coin.QUARTER)){
changes.add(Coin.QUARTER);
balance = balance - Coin.QUARTER.getDenomination();
continue;
}else if(balance >= Coin.DIME.getDenomination()
&& cashInventory.hasItem(Coin.DIME)) {
changes.add(Coin.DIME);
balance = balance - Coin.DIME.getDenomination();
continue;
}else if(balance >= Coin.NICKLE.getDenomination()
&& cashInventory.hasItem(Coin.NICKLE)) {
changes.add(Coin.NICKLE);
balance = balance - Coin.NICKLE.getDenomination();
continue;
}else if(balance >= Coin.PENNY.getDenomination()
&& cashInventory.hasItem(Coin.PENNY)) {
changes.add(Coin.PENNY);
balance = balance - Coin.PENNY.getDenomination();
continue;
}else{
throw new NotSufficientChangeException("NotSufficientChange,
Please try another product");
}
}
}
return changes;
}
@Override
public void reset(){
cashInventory.clear();
itemInventory.clear();
totalSales = 0;
currentItem = null;
currentBalance = 0;
}
public void printStats(){
System.out.println("Total Sales : " + totalSales);
System.out.println("Current Item Inventory : " + itemInventory);
System.out.println("Current Cash Inventory : " + cashInventory);
}
private boolean hasSufficientChange(){
return hasSufficientChangeForAmount(currentBalance - currentItem.getPrice());
}
private boolean hasSufficientChangeForAmount(long amount){
boolean hasChange = true;
try{
getChange(amount);
}catch(NotSufficientChangeException nsce){
return hasChange = false;
}
return hasChange;
}
private void updateCashInventory(List change) {
for(Coin c : change){
cashInventory.deduct(c);
}
}
public long getTotalSales(){
return totalSales;
}
}
VendingMachineFactory.java
一个工厂类,用来创建不同的售货机
package vending;
/**
* Factory class to create instance of Vending Machine, this can be extended to create instance of * different types of vending machines.
* @author Javin Paul
*/
public class VendingMachineFactory
{
public static VendingMachine createVendingMachine()
{
return new VendingMachineImpl();
}
}
Coin.java
一个Java的enum用来表示售货机支持的货币
package vending;
/**
* Coins supported by Vending Machine.
* @author Javin Paul
*/
public enum Coin {
PENNY(1), NICKLE(5), DIME(10), QUARTER(25);
private int denomination;
private Coin(int denomination){
this.denomination = denomination;
}
public int getDenomination(){
return denomination;
}
}
Inventory.java
一个用来表示库存的类,用来创建售货机中的案例和物品清单。
package vending;
import java.util.HashMap;
import java.util.Map;
/**
* An Adapter over Map to create Inventory to hold cash and
* Items inside Vending Machine
* @author Javin Paul
*/
public class Inventory<T> {
private Map<T, Integer> inventory = new HashMap<T, Integer>();
public int getQuantity(T item)
{
Integer value = inventory.get(item);
return value == null? 0 : value ;
}
public void add(T item){
int count = inventory.get(item);
inventory.put(item, count+1);
}
public void deduct(T item) {
if (hasItem(item)) {
int count = inventory.get(item);
inventory.put(item, count - 1);
}
}
public boolean hasItem(T item){
return getQuantity(item) > 0;
}
public void clear(){
inventory.clear();
}
public void put(T item, int quantity) {
inventory.put(item, quantity);
}
}
Bucket.java
一个带参数的工具类,可以产生两个对象
package vending;
/**
* A parameterized utility class to hold two different object.
* @author Javin Paul
*/
public class Bucket<E1, E2> {
private E1 first;
private E2 second;
public Bucket(E1 first, E2 second){
this.first = first;
this.second = second;
}
public E1 getFirst(){
return first;
}
public E2 getSecond(){
return second;
}
}
NotFullPaidException.java
这是一个exception,主要用来表示一个用户选择了一个项目,但是没有付足够的钱。
package vending;
public class NotFullPaidException extends RuntimeException {
private String message;
private long remaining;
public NotFullPaidException(String message, long remaining) {
this.message = message;
this.remaining = remaining;
}
public long getRemaining(){
return remaining;
}
@Override
public String getMessage(){
return message + remaining;
}
}
NotSufficientChangeException.java
这个Exception用来表示售货机没有钱的用来找零。
package vending;
public class NotSufficientChangeException extends RuntimeException {
private String message;
public NotSufficientChangeException(String string) {
this.message = string;
}
@Override
public String getMessage(){
return message;
}
}
SoldOutException.java
当用户选择一个已经卖完了的产品时,会抛出这个exception
package vending;
public class SoldOutException extends RuntimeException {
private String message;
public SoldOutException(String string) {
this.message = string;
}
@Override
public String getMessage(){
return message;
}
}
关于设计售货机的第一部分就到这里结束了。在这个部分中,我们通过创建所有的类,以及写相关的代码解决了这个问题。但是单元测试和设计文档并没有做,关于这一部分你可以关注我们的第二部分。
假如你愿意的话,你可以为这个问题创建单元测试,或者在一个thread中运行他,然后再建一个thread来调用它,这样就类似模拟一个用户。你也可以阅读UML For Java Programmers中的相关内容。
进一步阅读:
From 0 to 1: Design Patterns – 24 That Matter – In Java
Java Design Patterns – The Complete Masterclass
更多原创,敬请关注微信公众号,每日更新业界最新资讯:
欢迎访问个人小站: https://donggeitnote.com/2020/07/06/javavendingmachine/