面试题之如何用Java设计一个自动售货机

如何用Java设计一个自动售货机程序是一个非常好的Java面试题。大多数情况会在面试比较senior的Java开发者的时候出现。在一个典型的代码面试中,你需要在一定的时间内根据对应的条件完成相关的代码。通常2到3小时内(面试哪有这么多时间,哈哈),你需要产生设计文档,可以工作的代码已经单元测试。这样的Java面试的好处就是你能够一次性检测面试者的很多能力。为了能够完成代码的设计,编码以及单元测试,面试者需要在这三个方面都比较精通。

另外,这种真实的问题可以提升你面向对象分析和设计能力的技能,假如你想成为一个很好的应用开发者,那么这个技能就很重要。

要想用Java或者别的面向对象的语言来设计一个自动售货机,你不仅仅需要了解最基本的东西,比如封装(Encapsulation),多态(Polymorphism)或者继承(Inheritance),你还需要理解如何使用抽象类和接口的细节,这样才能解决问题或者设计一个好的应用。

通常这种问题,还会给你一个使用设计模式的机会,因为在这个问题中你可以使用工厂模式去创建不同的售货机。我在20个Java软件开发的问题一文中曾今讨论过这个问题,那之后,我收到了很多反馈关于解决这个问题的方案。

这篇文章,我们将会提供一个自动售货机问题的解决方案。顺便说一下,其实这个问题有很多种解决的方案,你应当在看本文之前自己先尝试一下。你也需要先复习一下SOLID和OOPS的设计原则,我们会在代码中使用到他们。当你设计自动售货机的时候,你会发现我们会用到其中很多的相关内容。

另外,假如你对设计模式和原则感兴趣,我推荐你看看Udemy的“Java设计模式”这门课。这门课包括SOLID的设计模式,比如开闭原则(open closed)以及里氏替代(Liskov Substitution),当然也包括所有的面向对象的设计模式,比如装饰,观察,责任链等等。

问题陈述

你需要设计一个这样的自动售货机:

  1. 能够接收1分钱,5分钱,10分钱以及25分钱等等。
  2. 允许客户来选择产品,比如可乐(25), 百事(35),苏打(45)
  3. 允许用户取消请求来退钱
  4. 返回选择的产品,并且找零(假如需要的话)
  5. 允许售货机的提供商做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中的相关内容。

进一步阅读:

Design Pattern Library

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

You may also like...

Leave a Reply

Your email address will not be published.