1022 Quiz#2: 簡化的迷你銀行

C++ 實習測試: 簡化的迷你銀行設計與製作
時間: 120分鐘 (12:05 上傳時間截止)

在這個測試裡, 我們嘗試模型化一個功能簡化的 迷你銀行, 提供下列功能

  • 這個銀行 (Bank) 有一個行員 (Teller) 提供 開戶 (createAccount), 存款 (deposit), 提款 (withdraw), 帳戶查詢 (inquire), 轉帳 (transfer) 的功能

  • 有很多個客戶, 每一個客戶有唯一的身分証號碼 customerID, 都可以開設一個帳戶, 每一個帳戶有唯一的帳戶號碼 accountID

  • 客戶存款與提款僅限於現金存款與現金提款, 提款金額不能大於帳戶餘額 (balance)

  • 客戶轉帳時僅限於本銀行內的帳戶互轉, 轉出金額不能大於帳戶餘額

  • 銀行行員早上有 200,000 的現金在抽屜裡, 每天營業結束時需要清點帳戶總價值以及現金餘額, 確定兩者是一致的
因為考試時間有限, 所以我們需要相當程度地簡化我們的模型, 只需要考慮上面的要求, 其他功能就算實際的銀行運作裡是需要的, 請暫時不要考慮, 否則在有限的時間裡恐怕很難完成 (考試完了以後, 你可以參考最後面的描述來檢驗這個簡化過的設計和實際功能之間的落差)

  1. 首先, 由題目的需求敘述裡我們找到幾個可能的類別: 銀行, 行員, 帳戶, 客戶

  2. 開戶, 存款, 提款, 帳戶查詢, 轉帳, 以及清點結帳, 每一個功能都可以看成是一個使用案例 (usecase)

  3. 簡化起見, 我們先不要分成四個類別, 試看看可不可以用 銀行帳戶 兩個類別來完成上面這六個使用案例 (請注意純粹是因為考試的時間有限, 所以我們才做這樣的簡化, 正常狀況下其實四個類別應該算是少的, 少任何一個類別都會導致一些看起來很合理的功能無法順利完成) , 我們先簡單地寫出測試的程式碼
    void main()
    {
        Bank bank("CS Minibank");
    
        string aliceName("Alice"), aliceID("C123456789");
        string bobName("Bob"), bobID("D987654321");
        string carolName("Carol"), carolID("A123454321");
        int aliceAccID=0, bobAccID, carolAccID;
    
        assert(!bank.deposit(aliceAccID, 5000)); // 還沒有開戶的情況下
        assert(!bank.withdraw(aliceAccID, 5000));// aliceAccID = 0, 
        assert(bank.inquire(aliceAccID) < 0);    // 這些動作都不會成功
    
        assert(aliceAccID = bank.createAccount(aliceName, aliceID));
        assert(bank.inquire(aliceAccID) == 0);
        assert(bank.deposit(aliceAccID, 10000));
        assert(bank.inquire(aliceAccID) == 10000);
    
        assert(bobAccID = bank.createAccount(bobName, bobID));
        assert(bank.deposit(bobAccID, 23400));
        assert(bank.inquire(bobAccID) == 23400);
    
        assert(carolAccID = bank.createAccount(carolName, carolID));
        assert(bank.deposit(carolAccID, 53500));
        assert(bank.inquire(carolAccID) == 53500);
    
        assert(bank.withdraw(bobAccID, 3400));
        assert(bank.inquire(bobAccID) == 20000);
    
        assert(bank.transfer(carolAccID, aliceAccID, 13500));
        assert(bank.inquire(carolAccID) == 40000);
        assert(bank.inquire(aliceAccID) == 23500);
    
        assert(!bank.transfer(carolAccID, bobAccID, 50000)); // 失敗
        assert(!bank.withdraw(aliceAccID, 33500)); // 失敗
    
        assert(bank.withdraw(aliceAccID, 8500));
        assert(bank.inquire(aliceAccID) == 15000);
    
        assert(bank.dailyAudit());
    
        system("pause");
    }

  4. 所以這個 Bank 類別基本上就提供 (你也可以乾脆想像說是一個自動提款機 ATM)
    int createAccount(string& customerName, string& customerID)
    {
        vector<Account*>::iterator iter;
        for (iter=m_accounts.begin(); iter<m_accounts.end(); iter++)
            if ((*iter)->isCustomerID(customerID))
                return 0;
    
        Account *ptr;
        m_accounts.push_back(ptr=new Account(customerName, customerID));
        return ptr->getAccountID();
    }
    createAccount 時先檢查這個顧客是不是已經有開戶, 如果沒有則運用 customerName, customerID 建構一個 Account 物件, 把指標記錄在 Bank 物件中的容器資料成員 m_accounts 裡, 配合上面這段程式碼你應該可以看出 m_accounts 的型態, 新帳戶中帳戶餘額應該初始化為 0

    在 Account 建構元裡應該要產生一個唯一的整數序號, 並且運用 getAccountID() 介面傳出


    bool deposit(int accountID, int amount)
    {
        vector<Account*>::iterator iter;
        for (iter=m_accounts.begin(); iter<m_accounts.end(); iter++)
            if ((*iter)->isAccountID(accountID))
            {
                (*iter)->add(amount);
                m_cash += amount;
                return true;
            }
        return false;
    }
    

    int inquire(int accountID)
    {
        vector<Account*>::iterator iter;
        for (iter=m_accounts.begin(); iter<m_accounts.end(); iter++)
            if ((*iter)->isAccountID(accountID))
                return (*iter)->getBalance();
        return -1;
    }

    withdraw()
    ...
    transfer()
    ...
    dailyAudit()
    ...
    這六個功能, Bank 類別也需要有適當的 建構元 和 解構元 (請記得刪除所有的帳號物件)

    Bank 裡面需要記住 1. 銀行的名稱, 2. 早上開始營業時的現金金額 m_cashStart (200,000), 3. 還有目前的現金金額 m_cash, 4. 上面 m_accounts 是一個 vector 容器物件, 記錄不同客戶的帳戶物件的指標

  5. 請完成上面的這個 Bank 類別的宣告以及成員函式的定義, 其中 withdraw, transfer 和 deposit, inquire 一樣, 都需要找到指定的帳戶, 檢查帳戶的餘額, 修改帳戶的餘額, 如果有牽涉現金流入流出, 需要修改銀行的現金數量

  6. dailyAudit 需要計算所有帳戶的淨資產, 根據早上開始營業時的現金金額, 核對目前的現金金額是否正確

  7. 配合上面的描述, 請設計帳戶 (Account) 類別, 每一個帳戶至少應該記錄, 顧客姓名 (customerName), 顧客身分證號 (custumerID), 顧客帳號 (customerAccountID), 帳戶餘額 (balance), 上面 Bank 類別的六個功能可以隨時去存取所紀錄的帳戶物件; 帳戶物件的介面至少應該有 isAccountID(int accountID), isCustomerID(int customerID), getAccountID(void), getBalance(void), add(int amount) 等等, 請逐一實作出來

    Account 類別也需要有適當的建構元 (記得產生唯一的帳號) 和 解構元

  8. 請測試到上面的 main() 程式能夠正確執行 (當然正確運作的部份越多越好, 如果你有沒有辦法達成的功能, 請先把 main() 裡面對應的測試註解掉)

時間: 120分鐘 (12:05 上傳時間截止)

請在 Visual Studio 中製作一個新方案 SimplifiedMiniBank, 請依照上面的說明撰寫類別, 並且測試, 確保功能正確

將所完成的程式方案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 以 zip/rar/7zip 程式將整個資料匣壓縮起來, 選擇 Labtest2上傳
範例程式碼 完整操作過程 (android com.adobe.flashplayer.apk)

上面描述的這個模型實在有點太簡化了, 只有稍微意思到了而已, 和實際上銀行運作差異很大, 後續你可以思考下面的問題, 嘗試設計功能比較完整的系統:

  1. 這個銀行只有一個行員, 實際上每一個銀行在運作時都有很多個行員, 前面把 開戶, 存款, 提款, 查詢, 轉帳 的功能以及客戶的帳戶都放在 Bank 類別裡, 就會使得銀行裡永遠只有單一的一個行員, 所以應該要有分離出來的銀行行員類別 (Teller, Clerk), 如此 銀行行員類別負責 開戶, 存款, 提款, 查詢, 轉帳 的功能, 而銀行類別負責記錄所有客戶的帳戶資料, 當然有了銀行行員類別以後, 這些行員也需要記錄姓名, ID 等等基本資料。

  2. 每一個客戶只能有一個單一的帳戶嗎? 應該不見得吧, 帳戶有不同類型的帳戶, 利率和利息的計算方式不見得相同, 可以有支票嗎? 不同種類的帳戶需要有獨立管理的類別嗎? 如果某一類型的帳戶需要有特別的處理方式, 也許就需要多設計一個類別來記錄那個類型的帳戶, 而不能只用一般功能的容器 (例如 vector), 另外現金有區分各種鈔票/銅板面額的數量嗎?

  3. 上面這個運作模型中客戶是沒有辦法調閱過去一段時間內所有帳戶的活動細節的, 例如如果有人轉帳給你你就不知道什麼時候轉的, 多少錢入帳了, 因為帳戶裡只記錄目前的餘額。 更糟糕的是, 銀行不太容易計算出正確的利息給客戶, 因為根本不知道帳戶中餘額的細部變化。

    要解決這樣的問題, 應該要設計記錄所有流水帳 (ledger)的機制, 一種方式是把每一筆交易記錄在每一個帳戶裡, 就好像你的存簿裡記錄的資料一樣, 不過如果只記錄在存簿裡, 銀行稽核查帳的時候不就要把所有的存簿收回去? 實際上這樣子做一定是不夠的, 第二種方式是在銀行端存下所有顧客完整的交易記錄, 這個交易記錄不見得屬於個別帳戶的, 比方說轉帳的交易記錄就屬於兩個不同的帳戶

    所以在設計上可以 把每一筆交易 (Transaction) 獨立出來, 由銀行類別統一管理, 也方便每一筆交易產生出來以後可以記錄在檔案或是資料庫裡。當然為了實作這樣的交易記錄機制, 也需要有獨立的時間和日期的類別, 需要處理一些時間或是日期的比較。

  4. 每一個帳戶其實只要結清了, 就可以關掉, 當然結清之前銀行其實是要把利息都算出來的。 由於過去交易的記錄都會保留相當長的一段時間, 客戶或是主管單位或是警政單位都還有可能來查詢, 所以帳戶關掉不能直接就把帳戶刪除, 應該是要把帳戶的狀態設成 "關閉", 有別於 "正常", "透支", "靜止", "凍結", 等等狀態。

  5. 前面簡化的程序中, 銀行行員每天結帳時需要清點帳戶總價值以及現金餘額, 確定兩者是一致的, 由於我們把銀行和行員類別分開了, 功能和責任也就分開來了, 現在需要修正為由銀行指派人員清點所有帳戶的總價值, 由銀行的金庫管理人員負責銀行的現金總數量, 銀行行員每天早上需要由金庫領取一定額度的現金, 結帳時需要查閱所有他經手的流水帳, 確定現金餘額和流水帳現金流出/流入是一致的, 結帳後需要繳交現金回到金庫 , 銀行端需要指派人員在所有行員結帳後確定帳戶的總價值改變和金庫現金的總價值改變是一致的。

  6. 上面的測試程式裡我們假設只有一個銀行, 可是由於銀行是一個獨立的類別, 照理說如果我們在類別裡設計銀行的名稱, 銀行的 ID, 其實是可以有很多銀行的, 如以轉帳功能其實也就可以跨越不同的銀行來轉帳 (如果你假設銀行之間是有連線的, 轉帳的結果立刻出現在轉入端的帳戶裡, 不過因為要有實體現金的移轉, 也許這種跨行轉帳和行內的轉帳需要適當的區分, 在現金還沒有入帳之前, 目標帳戶中所轉入的金額是沒有辦法進行現金提款的)。

  7. 上面的功能描述裡沒有提到票據的作業以及代收付帳款的作業, 沒有談到除了現金之外其他的有價資產, 更沒有描述到銀行真正賺錢的核心功能 - 放款與投資, 也還沒有談到利息的計算... 為了簡化也沒有實作物件內資料的序列化 (存檔或是存放在資料庫裡)。

這些都是設計上需要修改的, 你可以試著修改類別的設計, 增加一些功能。

上面所說的我有寫程式實作部份的功能, 如果你有嘗試去寫的話, 歡迎你過來討論。

C++ 物件導向程式設計課程 首頁

製作日期: 03/05/2014 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程學系 Lagoon