ATM机设计归纳

源代码见ATM源码

需求

一个ATM是一台机器,包含读卡设备显示屏吐钞口存钞口键盘打印机
当机器空闲时,会显示一个欢迎消息,此时键盘和存钞口都是不活动的,直到一张银行卡被插入,读卡器会尝试读取这张卡,如果不可读,会提示用户并且弹出卡片
如果卡片可读,读卡设备会读取账号,然后要求用户输入密码, 用户的输入应该显示为星号,而不是真正输入的数字。
如果用户输入的密码正确,则显示主菜单; 如果不正确,再给用户两次输入机会,如果第三次依然失败,ATM就吞卡

主菜单提示用户可以

  1. 存款
    如果选择了存款交易,ATM要求用户输入存款的金额,然后在存钞口放入钞票
  2. 取款
    如果选择了取款交易,ATM要求用户输入提取的金额,如果账户余额足够,并且ATM的现金足够,从吐钞口吐出相应的钞票。
  3. 转账
    如果选择了转账,ATM要求用户输入转入的账号,如果余额足够, 就会执行转账交易
  4. 查看余额
    如果选择了查询余额,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;  // 键盘
}

下面举几个例子:

  1. 调用显示屏输出交易类型选择框,用户按键盘选择,获得某个具体的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("输入有误,请重新输入");
        }
    }
}
  1. 用户输入密码,显示屏展示*号
/**
    * 提示用户输入密码,待用户输入完成后,显示星号
    * 并把明文密码返回(为了简单起见)
    * @return
    */
public String getPassword() {
    System.out.println("请输入密码:");
    String input = keyBoard.input();
    display.outputEncryptedText(input);
    return input;
}

各部件接口

为了代码的可扩展性,我将大部分成员变量设计为了接口形式:

ATM机设计归纳_第1张图片
image.png

每个接口都有具体的类来实现:

ATM机设计归纳_第2张图片
image.png

举个吐钞口的例子:

/**
 * 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);
}

它的实现类很简单,补全了verifyprocess方法无非是让客户端把消息发到银行做密码账号验证,或进行交易处理,而其中process(Transaction transaction)方法会把封装的交易发到银行那边做处理,因此我们下面看一看Transaction类。

Transaction abstract class

一共有4种交易类型,存款,取款,转账,余额查询,它们的共同特点是把交易信息传输给银行,如果用一个类来统一管理比较方便。但是它们又各有特点,比如取款之前先要校验atm机余额是否足够,取款后要把钱吐出来;存款前则要把钱塞到atm机的存款口。
因此我设计了一个抽象类,给出了交易前动作preProcess,交易后动作postProcess的抽象方法,让子类负责实现,其中要传入atm对象,用来调用atm的其他部件的功能;而交易本身上面已经讲过,由BankProxyprocess方法交给银行来处理。

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机设计归纳_第3张图片
image.png

总结

ATM是运用组合而非继承的一个很好的例子,代码本身不复杂,但是设计得精妙很难,而且写惯了增删改查的同学,如果写写这类有相关行为的代码,是很好的练习。

你可能感兴趣的:(ATM机设计归纳)