源代码见ATM源码
需求
一个ATM是一台机器,包含读卡设备,显示屏,吐钞口,存钞口,键盘,打印机
当机器空闲时,会显示一个欢迎消息,此时键盘和存钞口都是不活动的,直到一张银行卡被插入,读卡器会尝试读取这张卡,如果不可读,会提示用户并且弹出卡片
如果卡片可读,读卡设备会读取账号,然后要求用户输入密码, 用户的输入应该显示为星号,而不是真正输入的数字。
如果用户输入的密码正确,则显示主菜单; 如果不正确,再给用户两次输入机会,如果第三次依然失败,ATM就吞卡
主菜单提示用户可以
- 存款
如果选择了存款交易,ATM要求用户输入存款的金额,然后在存钞口放入钞票 - 取款
如果选择了取款交易,ATM要求用户输入提取的金额,如果账户余额足够,并且ATM的现金足够,从吐钞口吐出相应的钞票。 - 转账
如果选择了转账,ATM要求用户输入转入的账号,如果余额足够, 就会执行转账交易 - 查看余额
如果选择了查询余额,ATM则显示账号的的余额所有的交易都是ATM和银行服务器合作
用户可以选定交易,提供相关信息,交易完成后,返回主菜单
所有的交易都是ATM和银行服务器合作完成的,银行保留了账户信息,必须在合适的时候向银行查询这些信息
这里其实是两个系统, 一个运行在ATM上,另外一个在银行端
设计
ATM class
首先设计ATM机主体,它包含了所有的设备,将他们作为ATM的成员变量
public class ATM {
private CardReader cardReader; // 读卡设备
private SuperKeyPad superKeyPad; // 超级键盘
private CashDispenser cashDispenser; // 吐钞口
private DepositSlot depositSlot; // 存钞口
private Printer printer; // 打印机
private BankProxy bankProxy; // 银行代理
}
SuperKeyPad class
上面的大多数成员变量一眼就看明白了,但是superKeyPad
比较难理解,其实它整合了显示屏和键盘的功能,这是因为显示器和键盘之间要经常做交互,比如键盘打了密码,显示屏显示星号;显示屏展示交易类型,键盘选择哪一种交易类型等:
public class SuperKeyPad {
private Display display; // 显示屏
private KeyBoard keyBoard; // 键盘
}
下面举几个例子:
- 调用显示屏输出交易类型选择框,用户按键盘选择,获得某个具体的Transaction
public Transaction getTransaction(String account, String password) {
display.outputPlainText("请输入交易类型:");
display.outputPlainText("W: 取款, ");
display.outputPlainText("D: 存款, ");
display.outputPlainText("T: 转账, ");
display.outputPlainText("Q: 查询余额");
while(true) {
String input = keyBoard.input();
switch (input) {
case "W":
return getWithdrawTrx(account, password);
case "D":
return getDepositTrx(account, password);
case "T":
return getTransferTrx(account, password);
case "Q":
return getQueryTrx(account, password);
default:
System.out.printf("输入有误,请重新输入");
}
}
}
- 用户输入密码,显示屏展示*号
/**
* 提示用户输入密码,待用户输入完成后,显示星号
* 并把明文密码返回(为了简单起见)
* @return
*/
public String getPassword() {
System.out.println("请输入密码:");
String input = keyBoard.input();
display.outputEncryptedText(input);
return input;
}
各部件接口
为了代码的可扩展性,我将大部分成员变量设计为了接口形式:
每个接口都有具体的类来实现:
举个吐钞口的例子:
/**
* Created by thomas_young on 30/7/2017.
* 吐钞口/取款口
* 判断金额是否足够(我们用它来负责管理atm的金额,不再专门搞一个储钱箱了)
*/
public interface CashDispenser {
/**
* 取款amount,判断余额是否足够
* @param amount
* @return
*/
boolean hasEnoughMoney(Double amount);
/**
* 吐出amount的钞票
* @param amount
*/
void dispenseMoney(Double amount);
}
public class CashDispenserImpl implements CashDispenser {
// 方便起见,假设atm机子里有这么多前,而且不会变 todo magic number!
private static Double totalAmount = 10000d;
@Override
public boolean hasEnoughMoney(Double amount) {
if (amount <= totalAmount)
return true;
System.out.println("atm没钱啦");
return false;
}
@Override
public void dispenseMoney(Double amount) {
System.out.println("取款:吐出"+amount+"元");
}
}
BankProxy abstract class
由于ATM机需要和银行服务区做交互,进行取款存款的交易,因此封装了一个银行代理类,其中包含了一个客户端以及若干方法:
public abstract class BankProxy {
protected NetworkClient networkClient; // 客户端
public abstract boolean verify(String account, String password);
public abstract boolean process(Transaction transaction);
}
它的实现类很简单,补全了verify
和process
方法无非是让客户端把消息发到银行做密码账号验证,或进行交易处理,而其中process(Transaction transaction)
方法会把封装的交易发到银行那边做处理,因此我们下面看一看Transaction类。
Transaction abstract class
一共有4种交易类型,存款,取款,转账,余额查询,它们的共同特点是把交易信息传输给银行,如果用一个类来统一管理比较方便。但是它们又各有特点,比如取款之前先要校验atm机余额是否足够,取款后要把钱吐出来;存款前则要把钱塞到atm机的存款口。
因此我设计了一个抽象类,给出了交易前动作preProcess
,交易后动作postProcess
的抽象方法,让子类负责实现,其中要传入atm对象,用来调用atm的其他部件的功能;而交易本身上面已经讲过,由BankProxy
的process
方法交给银行来处理。
public abstract class Transaction {
private String account;
private String password;
public abstract boolean preProcess(ATM atm);
public abstract boolean postProcess(ATM atm);
}
// 存款交易类
public class DepositTransaction extends Transaction {
// 该取款交易的存款金额
private Double amount;
private Double actualAmount;
@Override
public boolean preProcess(ATM atm) {
actualAmount = atm.retrieveMoney(amount);
System.out.println("实际存款"+actualAmount+"元");
return true;
}
@Override
public boolean postProcess(ATM atm) {
// TODO: 30/7/2017 只是为了调试,建议删除
System.out.println("存款postProcess: 什么都不做");
return true;
}
}
使用方法:
boolean valid = transaction.preProcess(this);
if (!valid) {
cardReader.ejectCard();
return;
}
boolean success = bankProxy.process(transaction);
if (!success) {
System.out.printf("银行处理出错");
} else {
System.out.println("银行已经处理完成");
transaction.postProcess(this);
}
ATM主流程
atm的几个方法,就是调用各个子类做一些事,供Transaction的前后处理使用,此外还有个start()
方法,是主流程
public boolean hasEnoughMoney(Double amount) {
return cashDispenser.hasEnoughMoney(amount);
}
/**
* 取款
* @param amount
*/
public void dispenseMoney(Double amount) {
cashDispenser.dispenseMoney(amount);
}
/**
* 存款
* @return
*/
public Double retrieveMoney(Double amount) {
return depositSlot.saveMoney(amount);
}
/**
* 主流程
*/
public void start() {
// step1. 读卡
String account;
while(true) {
account = cardReader.getAccount();
if(account != null) {
System.out.println("读卡成功,卡号是"+account);
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
// step2. 读取密码,并做校验
int failedCount = 0;
boolean verified = false;
String password = null;
while (failedCount < 3) {
password = superKeyPad.getPassword();
verified = bankProxy.verify(account, password);
if (verified) {
break;
}
failedCount++;
}
if (!verified) {
System.out.println("输错密码超过3次");
cardReader.eatCard();
return;
}
// step3. 输入交易类型,进行交易
Transaction transaction = superKeyPad.getTransaction(account, password);
boolean valid = transaction.preProcess(this);
if (!valid) {
cardReader.ejectCard();
return;
}
boolean success = bankProxy.process(transaction);
if (!success) {
System.out.printf("银行处理出错");
} else {
System.out.println("银行已经处理完成");
transaction.postProcess(this);
}
// last step. 最后记得把卡吐出
cardReader.ejectCard();
}
测试程序
要想让代码跑起来,需要给atm实例化,并给它装上各组件,驱动代码如下:
public class ATMTest {
private static ATM atm;
static {
atm = new ATM();
CardReader cardReader = new CardReaderImpl();
SuperKeyPad superKeyPad = new SuperKeyPad();
KeyBoard keyBoard = new KeyBoardImpl();
Display display = new DisplayImpl();
superKeyPad.setDisplay(display);
superKeyPad.setKeyBoard(keyBoard);
CashDispenser cashDispenser = new CashDispenserImpl();
DepositSlot depositSlot = new DepositSlotImpl();
Printer printer; // 没有用到
BankProxy bankProxy = new BankProxyImpl();
NetworkClient networkClient = new NetworkClient();
bankProxy.setNetworkClient(networkClient);
atm.setBankProxy(bankProxy);
atm.setCardReader(cardReader);
atm.setCashDispenser(cashDispenser);
atm.setDepositSlot(depositSlot);
atm.setSuperKeyPad(superKeyPad);
}
public static void main(String[] args) {
atm.start();
}
}
结构框图
总结
ATM是运用组合而非继承的一个很好的例子,代码本身不复杂,但是设计得精妙很难,而且写惯了增删改查的同学,如果写写这类有相关行为的代码,是很好的练习。