SALES TAXES
Basic sales tax is applicable at a rate of 10% on all goods, except books, food, and medical products that are exempt. Import duty is an additional sales tax applicable on all imported goods at a rate of 5%, with no exemptions.
When I purchase items I receive a receipt which lists the name of all the items and their price (including tax), finishing with the total cost of the items, and the total amounts of sales taxes paid. The rounding rules for sales tax are that for a tax rate of n%, a shelf price of p contains (np/100 rounded up to the nearest 0.05) amount of sales tax.
Write an application that prints out the receipt details for these shopping baskets...
INPUT:
Input 1:
1 book at 12.49
1 music CD at 14.99
1 chocolate bar at 0.85
Input 2:
1 imported box of chocolates at 10.00
1 imported bottle of perfume at 47.50
Input 3:
1 imported bottle of perfume at 27.99
1 bottle of perfume at 18.99
1 packet of headache pills at 9.75
1 box of imported chocolates at 11.25
OUTPUT
Output 1:
1 book : 12.49
1 music CD: 16.49
1 chocolate bar: 0.85
Sales Taxes: 1.50
Total: 29.83
Output 2:
1 imported box of chocolates: 10.50
1 imported bottle of perfume: 54.65
Sales Taxes: 7.65
Total: 65.15
Output 3:
1 imported bottle of perfume: 32.19
1 bottle of perfume: 20.89
1 packet of headache pills: 9.75
1 imported box of chocolates: 11.85
Sales Taxes: 6.70
Total: 74.68
首先我们看,税种这里有三种:
1. 免消费税;
2. 消费税;
3. 进口税。
则税种组合有以下两种:
1.免消费税+进口税
2.消费税+进口税
因此,我们可以设计一个Tax接口,里头包含一个方法名叫getTax,这个getTax方法作用就是返回纳税值,但是我们需要有哪些类来实现这个接口呢?
1.FixedRateTax,它拥有一个成员变量叫做rate,因此针对不同的实例,它拥有不同的rate,每一个实例对应某一种固定税率,因此其getTax传入的参数便为对应商品价格,返回值即为在此税率之下的相应税收。
2.CompositeTax,它拥有一个成员函数叫做addTax,因此它的作用就是整合某一商品它所包含的所有税收类型,因此其getTax同样传入的对应商品价格作为参数,返回值为某商品其对应的全部税收,事实上了就是一个对于税率类型的遍历,每一种税率类型对应一个FixedRateTax的实例,然后调用FixedRateTax的getTax方法获得此税率之下的相应税收加入当前税收总和之中,最后返回当前价格下的全部税收总和。
UML图如下:
对应的代码实现如下:
Tax.java
package main.java.com.thoughtworks.demo.taxes;
import java.math.BigDecimal;
/**
* This interface provides unified access to different taxation kinds.
*
*/
public interface Tax {
/**
* Calculates the tax value for the given price.
* @param price the price
* @return the tax value
*/
BigDecimal getTax(BigDecimal price);
}
FixedRateTax.java
package main.java.com.thoughtworks.demo.taxes.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import main.java.com.thoughtworks.demo.taxes.Tax;
/**
* A fixed rate implementation of the {@link Tax} interface. It rounds taxes up
* to the nearest 0.05.
*
*/
public class FixedRateTax implements Tax {
private static final BigDecimal TICK_SIZE = new BigDecimal("0.05");
private final BigDecimal rate;
/**
* Creates a tax with the specified rate.
* @param rate the tax rate
*/
public FixedRateTax(BigDecimal rate) {
this.rate = rate;
}
@Override
public BigDecimal getTax(BigDecimal price) {
return roundOff(price.multiply(rate));
}
private static BigDecimal roundOff(BigDecimal value) {
return value.divide(TICK_SIZE).setScale(0, RoundingMode.UP).multiply(TICK_SIZE);
}
}
CompositeTax.java
package main.java.com.thoughtworks.demo.taxes.impl;
import java.math.BigDecimal;
import java.util.LinkedList;
import java.util.List;
import main.java.com.thoughtworks.demo.taxes.Tax;
/**
* An implementation of the {@link Tax} interface that works as a superposition
* of multiple taxes.
*
*/
public class CompositeTax implements Tax {
private final List taxes = new LinkedList();
/**
* Adds another tax to this composite.
* @param tax the tax to add
*/
public void addTax(Tax tax) {
taxes.add(tax);
}
@Override
public BigDecimal getTax(BigDecimal price) {
BigDecimal result = BigDecimal.ZERO;
for (Tax tax : taxes) {
result = result.add(tax.getTax(price));
}
return result;
}
}
很显然如上输入所示,不同产品种类的货物对应不同的税率,因此首先我们要想到我们必须创建一个对应不同产品种类的类,使得这个类能够承载不同的产品类别及其对应的税率,这就是ProductCategory的由来。
有了ProductCategory之后,接下来我们需要考虑的就是针对每一件商品,我们都必须给予它一个条目详情,该条目详情包含四个属性,分别为商品ID,商品条目名称,商品价格,商品是否进口以及商品的类别(即为ProductCategory),通过这样的方式,我们就对每一条商品进行了登记,我们称之为CatalogItem。
有了CatalogItem还不够,因为CatalogItem只是对商品的总体信息进行了登记,并没有记录用户实际买卖的信息,因此对于用户实际的某一条购买记录,我们创建一个类叫做OrderItem,显然每一条这个记录对应于某一个CatalogItem,而且OrderItem之中包含有四个特别的属性分别为,税务信息,购买数量,总支出,总税收。
显然对于每个客户,他都有若干条购买记录,所有这些购买记录构成一个新的实例,我们称之为order,这个order包含如下三条信息:票据编码,总消费税和总支出;
那么讲了这么多我们也大致将商品和清单之间的关系也理清楚了,其UML图如下:
对应的代码实现如下:
ProductCategory.java
package main.java.com.thoughtworks.demo.taxes;
import java.math.BigDecimal;
/**
* Enumeration of product categories. Every category encapsulates its sales tax
* rate.
*
*/
public enum ProductCategory {
/**
* Books category. No sales tax.
*/
BOOK ("0"),
/**
* Food category. No sales tax.
*/
FOOD ("0"),
/**
* Medical products category. No sales tax.
*/
MEDICINE ("0"),
/**
* Music products category.
*/
MUSIC,
/**
* Perfume products category.
*/
PERFUME,
/**
* All other goods.
*/
OTHER;
private final BigDecimal salesTax;
/**
* Default constructor for product categories those have basic sales tax rate.
*/
private ProductCategory() {
this("0.1");
}
/**
* This constructor defines sales tax rate for a product category.
* @param salesTax the sales tax rate
*/
private ProductCategory(String salesTax) {
this.salesTax = new BigDecimal(salesTax);
}
/**
* Returns sales tax rate of this product category.
* @return sales tax rate value
*/
public BigDecimal getSalesTax() {
return salesTax;
}
}
package main.java.com.thoughtworks.demo.taxes;
import java.math.BigDecimal;
/**
* Catalog item abstraction. Contains base properties of an item in the catalog.
* Immutable.
*
*/
public class CatalogItem {
private final long id;
private final String title;
private final BigDecimal price;
private final ProductCategory category;
private final boolean imported;
/**
* Constructor initializing all fields.
* @param id the item id
* @param title the item title
* @param price the item price
* @param category the product category
* @param imported the the flag defining if the item is imported or not
*/
public CatalogItem(long id, String title, BigDecimal price, ProductCategory category, boolean imported) {
super();
this.id = id;
this.title = title;
this.price = price;
this.category = category;
this.imported = imported;
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public BigDecimal getPrice() {
return price;
}
public boolean isImported() {
return imported;
}
public ProductCategory getCategory() {
return category;
}
}
OrderItem.java
package main.java.com.thoughtworks.demo.taxes;
import java.math.BigDecimal;
/**
* Order item abstraction. Contains a {@link CatalogItem} and an applied tax
* value. Immutable.
*
*/
public class OrderItem {
private final CatalogItem catalogItem;
private final BigDecimal tax;
private final int quantity;
private final BigDecimal total;
private final BigDecimal totalTax;
/**
* Constructor.
* @param catalogItem the catalog item
* @param tax the value of applied tax
* @param quantity the number of catalog items (positive number)
*/
public OrderItem(CatalogItem catalogItem, BigDecimal tax, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity shall be positive");
}
this.catalogItem = catalogItem;
this.tax = tax;
this.quantity = quantity;
this.totalTax = tax.multiply(new BigDecimal(quantity));
this.total = catalogItem.getPrice().multiply(new BigDecimal(quantity)).add(totalTax);
}
public CatalogItem getCatalogItem() {
return catalogItem;
}
public BigDecimal getTax() {
return tax;
}
public int getQuantity() {
return quantity;
}
public BigDecimal getTotal() {
return total;
}
public BigDecimal getTotalTax() {
return totalTax;
}
@Override
public String toString() {
return quantity + " " + catalogItem.getTitle() + " : " + total;
}
}
Order.java
package main.java.com.thoughtworks.demo.taxes;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* A abstraction of an order. Contains ordered items. Immutable.
*
*/
public class Order {
private final long id;
private final List orderItems = new LinkedList();
private final BigDecimal salesTaxes;
private final BigDecimal total;
/**
* Constructor.
* @param id the order's id
* @param orderItems the list oder's items
*/
public Order(long id, List orderItems) {
this.id = id;
this.orderItems.addAll(orderItems);
// calculate taxes and total
BigDecimal salesTaxes = BigDecimal.ZERO;
BigDecimal total = BigDecimal.ZERO;
for (OrderItem orderItem : orderItems) {
salesTaxes = salesTaxes.add(orderItem.getTotalTax());
total = total.add(orderItem.getTotal());
}
this.salesTaxes = salesTaxes;
this.total = total;
}
public long getId() {
return id;
}
public List getOrderItems() {
return Collections.unmodifiableList(orderItems);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Order ").append(id).append(":\n");
for (OrderItem orderItem : orderItems) {
sb.append(orderItem).append('\n');
}
sb.append("Sales taxes: ").append(salesTaxes).append('\n');
sb.append("Total: ").append(total).append('\n');
return sb.toString();
}
}
那么除此以外,所谓的OrderManager在这里的作用就是负责创建清单,取消清单,添加购买记录以及结账。当然我们不会忘记,既然对商品要进行管理,同时对税收我们也要进行对应的管理,因此我们还需要一个接口叫做TaxManager。这个TaxManager主要就完成一件事情,就是getTax收税,因为此前我们对于税务实现了两个类,那么这些类之间的关系和操作就由这个TaxManager来实现。
注意到CatalogManager需要实现删除的功能,显然要删除一个CatalogItem需要知道其对应的ID,但是对于OrderManager这边,注意到我们有结账的功能,考虑结账的时候需要知道相应商品的税率,这些税率来自ProductCategory,而ProductCategory不独立出现,ProductCategory是来自于CatalogItem,每一个CatalogItem也具有独立的ID,因此我们决定创立一个接口Catalog,里头就一个接口函数getItem,分别由OrderManager以及CatalogManager的实现类共同继承实现。
那么既然讲到OrderManager以及CatalogManager的实现类,那么它们各自有何特征呢?
对于CatalogManager,我们实现一个类叫做TrivialCatalogImpl,那么这个类实现了Catalog和CatalogManager接口,重点进行商品种类的管理。
那么对于TrivialOrderManagerImpl,同样我们也实现了OrderManager的接口,但同时我们这里采用了一些IoC的手段来注入TaxManager和CatalogManager,理由是显然TrivialOrderManagerImpl和TaxManager,CatalogManager不存在一种继承关系,但是TrivialOrderManagerImpl当中的结账需要用到TaxManager来帮忙,自然TaxManager中的汇率信息必须由CatalogManager来提供,因此就形成了这样的一种关系。所以,这些类之间的UML关系图就如下所示:
CatalogManager.java
package main.java.com.thoughtworks.demo.taxes;
import java.math.BigDecimal;
/**
* This service provides methods for catalog modification.
*
*/
public interface CatalogManager {
/**
* Adds a new item.
* @param title the item's title
* @param price the item's price
* @param category the product category
* @param imported the flag defining if the item is imported or not
* @return the added item's id
*/
long addItem(String title, BigDecimal price, ProductCategory category, boolean imported);
/**
* Removes the specified item.
* @param id the item's id
* @throws NoSuchItemException if there is no catalog item with the given id
*/
void removeItem(long id);
}
OrderManager.java
package main.java.com.thoughtworks.demo.taxes;
/**
* This service is used to manage orders: create order, add item to the order,
* checkout, etc.
*
*/
public interface OrderManager {
/**
* Creates a new order.
* @return the order id
*/
long createOrder();
/**
* Cancels the specified order.
* @param orderId the order id
*/
void cancelOrder(long orderId);
/**
* Adds the specified catalog item to the specified order.
* @param orderId the order id
* @param itemId the catalog item id
* @param quantity the number of items to add
* @throws NoSuchItemException if there is no order or catalog item found for the specified ids
*/
void addItem(long orderId, long itemId, int quantity);
/**
* Checkouts the specified order. The order is no longer available for updates after this operation.
* @param orderId the order id
* @return the order
* @throws NoSuchItemException if there is no order found for the specified id
*/
Order checkout(long orderId);
}
TaxManager.java
package main.java.com.thoughtworks.demo.taxes;
/**
* Defines tax rates for catalog items.
*
*/
public interface TaxManager {
/**
* Calculates a tax rate for the specified item's attributes.
* @param category the product category
* @param isImported the flag defining if the product was imported
* @return the {@link Tax} object to be applied
*/
Tax getTax(ProductCategory category, boolean isImported);
}
Catalog.java
package main.java.com.thoughtworks.demo.taxes;
/**
* This service is intended for read only catalog access.
*
*/
public interface Catalog {
/**
* Returns an item by its id.
* @param id the item's id
* @return the item
* @throws NoSuchItemException if there is no catalog item with the given id
*/
CatalogItem getItem(long id);
}
TaxManagerImpl.java
package main.java.com.thoughtworks.demo.taxes.impl;
import java.math.BigDecimal;
import main.java.com.thoughtworks.demo.taxes.ProductCategory;
import main.java.com.thoughtworks.demo.taxes.Tax;
import main.java.com.thoughtworks.demo.taxes.TaxManager;
/**
* An implementation of the {@link TaxManager} interface that is able to manage
* base sales taxes and import duties.
*
*/
public class TaxManagerImpl implements TaxManager {
private static final BigDecimal IMPORT_DUTY_RATE = new BigDecimal("0.05");
@Override
public Tax getTax(ProductCategory category, boolean isImported) {
CompositeTax result = new CompositeTax();
result.addTax(new FixedRateTax(category.getSalesTax()));
if (isImported) {
result.addTax(new FixedRateTax(IMPORT_DUTY_RATE));
}
return result;
}
}
TrivialCatalogImpl.java
package main.java.com.thoughtworks.demo.taxes.impl;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import main.java.com.thoughtworks.demo.taxes.Catalog;
import main.java.com.thoughtworks.demo.taxes.CatalogItem;
import main.java.com.thoughtworks.demo.taxes.CatalogManager;
import main.java.com.thoughtworks.demo.taxes.NoSuchItemException;
import main.java.com.thoughtworks.demo.taxes.ProductCategory;
/**
* Trivial in-memory implementation of the {@link Catalog} interface. It holds a
* map {@link CatalogItem} objects using their ids as keys.
*
*/
public class TrivialCatalogImpl implements Catalog, CatalogManager {
private final Map items = new HashMap();
private final IdGenerator idGenerator = new IdGenerator();
@Override
public synchronized long addItem(String title, BigDecimal price, ProductCategory category, boolean imported) {
long id = idGenerator.getId();
CatalogItem item = new CatalogItem(id, title, price, category, imported);
items.put(id, item);
return id;
}
@Override
public synchronized void removeItem(long id) {
if (items.remove(id) == null) {
throw new NoSuchItemException("There is no catalog item: " + id);
}
}
@Override
public synchronized CatalogItem getItem(long id) {
CatalogItem item = items.get(id);
if (item == null) {
throw new NoSuchItemException("There is no catalog item: " + id);
}
return item;
}
}
TrivialOrderManagerImpl.java
package main.java.com.thoughtworks.demo.taxes.impl;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import main.java.com.thoughtworks.demo.taxes.Catalog;
import main.java.com.thoughtworks.demo.taxes.CatalogItem;
import main.java.com.thoughtworks.demo.taxes.NoSuchItemException;
import main.java.com.thoughtworks.demo.taxes.Order;
import main.java.com.thoughtworks.demo.taxes.OrderItem;
import main.java.com.thoughtworks.demo.taxes.OrderManager;
import main.java.com.thoughtworks.demo.taxes.Tax;
import main.java.com.thoughtworks.demo.taxes.TaxManager;
/**
* Trivial in-memory implementation of the {@link OrderManager} interface. It
* holds a map {@link Order} objects using their ids as keys.
*
*/
public class TrivialOrderManagerImpl implements OrderManager {
private final HashMap> orders = new HashMap>();
private final IdGenerator idGenerator = new IdGenerator();
private Catalog catalog;
private TaxManager taxManager;
public void setCatalog(Catalog catalog) {
this.catalog = catalog;
}
public void setTaxManager(TaxManager taxManager) {
this.taxManager = taxManager;
}
@Override
public synchronized long createOrder() {
long id = idGenerator.getId();
List orderItems = new LinkedList();
orders.put(id, orderItems);
return id;
}
@Override
public synchronized void cancelOrder(long orderId) {
List orderItems = orders.remove(orderId);
if (orderItems == null) {
throw new NoSuchItemException("There is no order: " + orderId);
}
}
@Override
public synchronized void addItem(long orderId, long itemId, int quantity) {
List orderItems = orders.get(orderId);
if (orderItems == null) {
throw new NoSuchItemException("There is no order: " + orderId);
}
// create order item
CatalogItem catalogItem = catalog.getItem(itemId);
Tax tax = taxManager.getTax(catalogItem.getCategory(), catalogItem.isImported());
OrderItem orderItem = new OrderItem(catalogItem, tax.getTax(catalogItem.getPrice()),
quantity);
orderItems.add(orderItem);
}
@Override
public synchronized Order checkout(long orderId) {
List orderItems = orders.remove(orderId);
if (orderItems == null) {
throw new NoSuchItemException("There is no order: " + orderId);
}
return new Order(orderId, orderItems);
}
}
最后,我们需要一些工具类便于程序中的一些操作,例如产生
ID
以及抛出异常,如下所示:
IdGenerator.java
package main.java.com.thoughtworks.demo.taxes.impl;
import java.util.concurrent.atomic.AtomicLong;
/**
* Trivial unique id generator. It just holds an id value and increments it each
* time.
*
*/
class IdGenerator {
private final AtomicLong nextId = new AtomicLong(1);
/**
* Returns the next id value.
* @return the id
*/
public long getId() {
return nextId.getAndIncrement();
}
}
NoSuchItemException.java
package main.java.com.thoughtworks.demo.taxes;
/**
* This exception is thrown when an item is not found in a repository.
*
*/
public class NoSuchItemException extends RuntimeException {
/**
* Constructor.
* @param message the message describing this exception
*/
public NoSuchItemException(String message) {
super(message);
}
}
总的UML图如下:
main函数测试程序如下:
package main.java.com.thoughtworks.demo;
import java.math.BigDecimal;
import main.java.com.thoughtworks.demo.taxes.CatalogManager;
import main.java.com.thoughtworks.demo.taxes.Order;
import main.java.com.thoughtworks.demo.taxes.OrderManager;
import main.java.com.thoughtworks.demo.taxes.ProductCategory;
import main.java.com.thoughtworks.demo.taxes.impl.TaxManagerImpl;
import main.java.com.thoughtworks.demo.taxes.impl.TrivialCatalogImpl;
import main.java.com.thoughtworks.demo.taxes.impl.TrivialOrderManagerImpl;
/**
* Demo program illustrating basic concepts of "sales taxes" problem solution.
*
*/
public class TaxesDemo {
private CatalogManager catalogManager;
private OrderManager orderManager;
public void setCatalogManager(CatalogManager catalogManager) {
this.catalogManager = catalogManager;
}
public void setOrderManager(OrderManager orderManager) {
this.orderManager = orderManager;
}
public void demoCase1() {
// prepare all needed goods
long bookId = catalogManager.addItem("book", new BigDecimal("12.49"), ProductCategory.BOOK, false);
long cdId = catalogManager.addItem("music CD", new BigDecimal("14.99"), ProductCategory.MUSIC, false);
long chocolateId = catalogManager.addItem("chocolate bar", new BigDecimal("0.85"), ProductCategory.FOOD, false);
// order these goods
long orderId = orderManager.createOrder();
orderManager.addItem(orderId, bookId, 1);
orderManager.addItem(orderId, cdId, 1);
orderManager.addItem(orderId, chocolateId, 1);
Order order = orderManager.checkout(orderId);
// print receipt
System.out.println(order);
}
public void demoCase2() {
// prepare all needed goods
long chocolateId = catalogManager.addItem("imported box of chocolates", new BigDecimal("10.00"), ProductCategory.FOOD, true);
long perfumeId = catalogManager.addItem("imported bottle of perfume", new BigDecimal("47.50"), ProductCategory.PERFUME, true);
// order these goods
long orderId = orderManager.createOrder();
orderManager.addItem(orderId, chocolateId, 1);
orderManager.addItem(orderId, perfumeId, 1);
Order order = orderManager.checkout(orderId);
// print receipt
System.out.println(order);
}
public void demoCase3() {
// prepare all needed goods
long perfumeId = catalogManager.addItem("imported bottle of perfume", new BigDecimal("27.99"), ProductCategory.PERFUME, true);
long anotherPerfumeId = catalogManager.addItem("bottle of perfume", new BigDecimal("18.99"), ProductCategory.PERFUME, false);
long pillsId = catalogManager.addItem("packet of headache pills", new BigDecimal("9.75"), ProductCategory.MEDICINE, false);
long chocolateId = catalogManager.addItem("imported box of chocolates", new BigDecimal("11.25"), ProductCategory.FOOD, true);
// order these goods
long orderId = orderManager.createOrder();
orderManager.addItem(orderId, perfumeId, 1);
orderManager.addItem(orderId, anotherPerfumeId, 1);
orderManager.addItem(orderId, pillsId, 1);
orderManager.addItem(orderId, chocolateId, 1);
Order order = orderManager.checkout(orderId);
// print receipt
System.out.println(order);
}
public static void main(String[] args) {
// create services and wire them
TaxManagerImpl taxManagerImpl = new TaxManagerImpl();
TrivialCatalogImpl catalogImpl = new TrivialCatalogImpl();
TrivialOrderManagerImpl orderManagerImpl = new TrivialOrderManagerImpl();
orderManagerImpl.setTaxManager(taxManagerImpl);
orderManagerImpl.setCatalog(catalogImpl);
// create demo
TaxesDemo demo = new TaxesDemo();
demo.setCatalogManager(catalogImpl);
demo.setOrderManager(orderManagerImpl);
// run demo cases
demo.demoCase1();
demo.demoCase2();
demo.demoCase3();
}
}
根据main程序的流程,我们可以总结出以下的工作流程: