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;
}

各部件接口

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

image.png

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

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();
    }
}

结构框图

image.png

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容