面试题之如何用Java设计一个自动售货机
如何用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 * @author Javin Paul */ 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://javarevisited.blogspot.com/2016/06/design-vending-machine-in-java.html
Recent Comments