1012 C++ 程式作業二 (due 102/05/30(四) 23:59):

修車廠管理系統 -- 程式撰寫指引 - 第二階段

你看到這個說明文件的原因可能有兩個:

  1. 因為程式有很嚴重的 bug, 你還沒有交作業, 但是繳交時間已經過了, 你不想放棄這個作業所要傳遞的概念, 想要繼續下一個作業, 必須完成這個作業
  2. 你的作業已經繳交, 但是自己覺得不太能夠掌握設計的方法, 所以來看看別人的設計, 驗證一下子自己的設計方法與步驟
物件導向的設計是漸進式的, 並不是在紙上設計完所有的機制然後在進入 "程式碼實作 (coding)" 階段的, 越有經驗的設計者在 物件導向分析和設計的階段會做得比較多, 會透過 UML 中的 use case, class diagram, sequence diagram, state diagram, collaboration diagram 得到一個比較接近最後軟體的系統架構設計, 然後進入 coding 的階段, 不過目前你還在學習 C++ 的語法, 如果先設計好了架構, 也不知道是不是真的可以實作出來, 所以下面使用的方法其實就是基本的 bottom-up 設計, 由比較基礎比較簡單的物件開始設計, 慢慢地組合一些物件、調整一些物件的功能來得到最後需要的功能, 在過程中逐步修改各個物件的設計。

 




1. 怎樣運用C++函式庫中現成的類別 vector, deque, istream, ostream

2. 怎樣建立單一的類別 (建構元, 解構元, 拷貝建構元, 設定運算子, 檔案序列化, ...)

3. 怎樣結合多個類別的物件來模擬實際世界中系統的運作, 提供自動化的輔助系統

4. assert 應用

5. unitTest()

後續可以提供你練習物件導向技術的基礎平台, 練習繼承與多型的設計方法, 進一步結合資料庫系統或是圖形化介面系統開發應用程式

 
說明

通常客戶開車進到修車廠中, 或是車子被拖入修車廠中, 資深的技師很快地幫你評估需要做怎樣的維修或是哪些項目的保養, 然後根據目前在等候維修的車輛、可用的設備狀況、以及今天上班的技師的數量, 給你一個預估的完修時間以及初步的報價, 由於現在物料控管以及物流系統都很發達, 所以各種車款的各種維修零件都建立了電腦系統, 可以查詢到所需要的物料的價格, 修車廠中對於各種車款的故障狀況以及保養程序也在電腦中建立了標準流程, 所以技師只要鍵入基本的客戶車輛資料, 選擇維修的項目, 程式就可以自動推算出預估的取車時間以及初步的報價, 這個作業就是嘗試建立這樣的一套系統 。

在這個作業裡稍微做一些簡化, 以便你更容易實作需要的類別:

1.

物料的價格、品名、與系統內部編號我們簡化如下表:

物料編號 品名 售價 維修時是否需舉升車體 保固期 (月)
a24 頭燈燈泡 2000 0 12
a25 前燈罩 600 0 12
a26 空氣濾清器 1200 0 12
a31f 前門中控鎖 2600 0 24
...        
c01 輪胎 2200 1 6
..        

上述這些資料我們先假設是固定的, 由資料檔案裡讀出, 檔案內容如下:

          14            本檔案中物料個數
          a24           物料編號
          頭燈燈泡      品名
          2000          售價
          0             維修時是否需舉升車體
          12            保固期 (月)
          a25
          前燈罩
          600
          0
          12
          a26
          ... 

其中第一列為檔案中資料的筆數, 其餘資料分別為每一 "料件" 的 物料編號, 品名, 售價, 維修時是否需要舉升車體, 保固期

測試資料檔 Parts.txt

這個資料檔案裡面的每一筆資料是獨立的, 我們設計一個 Part 類別來存放每一筆物料, 這個類別裡需要依據資料的格式來設計資料成員如下

    string m_id;
    string m_name;
    int  m_price;
    int  m_warrantyMonths;
    bool m_liftedRequired;

照理說物料的資料應該還要有供應商的名稱, 物料的進價等等, 不過這裡簡化掉了

接下來你應該要設計 "序列化 (Serialization)" 的建構程式碼 Part(istream &is), 由資料檔案中讀出一筆一筆的資料, 建構出一個一個 Part 物件, 並且把這些物件記錄在一個容器裡面

你也應該順便寫一個列印 Part 物件裡資料內容的 print(ostream &os) 函式, 一個簡單的單元測試函式 unitTest 來讀取檔案並且列印所有讀到的資料, 在 main() 裡面呼叫單元測試確定這一段程式的正確性

Part 類別的其他功能性介面目前都還沒有設計到, 稍後我們看到可以針對需要的功能再來設計

在目前的設計裡, 這個表格是整個修車廠只需要維護一份的, 裡面的 Part 物件應該也是只有一份的, 不應該會拷貝很多份, 不過實際狀況裡, 這個表格卻是會變動的, 可能會增加項目, 可能會刪除項目, 可能價格會更動, 也許在變動的過程裡需要要求每一個物料有唯一的編號, 並且需要不斷地修改檔案... 不過目前暫不考量這些細節

每一個物料都是一個物件? 整個物料表格是一個物件? 或者都是物件? 從物件模型化分析的角度來看, 當然都是物件, 但是究竟在你的作業裡要不要把每一種看到的物件包裝成一個類別來實作? 還是可以暫時先不包裝, 需要的時候再修改? 其實是有彈性的, 在後續開發的過程中其實也都可以調整的, 不封裝成物件的話, 就只是存放資料的地方而已, 所有的功能就都散落在各處需要使用這種資料的地方, 就像第一週課程中所談到, 資料除了型態有限制之外, 數值變成沒有任何限制的, 出錯的機率變大, 如果封裝成類別的話, 每一個物件是有適當責任的, 負責管理適當的資料, 管理由這些資料所提供的服務。但是你正在學怎樣寫出物件化的程式, 正在熟悉這些語法, 也許你覺得多寫一個類別是一種負擔, 那其實也不要太過煩惱, 就先按照你看到的來實作, 每一種設計都有優點和缺點, 重點是你看到那些優缺點, 這裡沒有絕對正確的答案, 你需要先嘗試其中一種以後才知道有什麼好處, 有什麼壞處。

稍後在步驟 3 裡你可以看到目前是把 "物料" 和 "物料表格" 都當成物件的, 只是實作上設計 Part 類別, 但是卻省去 PartTable 類別而用標準函式庫提供的 vector 類別來簡化這個程式 (當然後續你很可能會發現這樣子簡化造成的缺點)

2.

接下來是 "維修或是保養項目" 的設計, 我們簡化如下表:

維修項目代號 需更換物料 維修時間 (分鐘) 工資
1001 a31f a32 b01 20 700
...      
2001 c01 c02 c03 d02 60 2500
...      
       

上述這些資料我們先假設是固定的, 由資料檔案裡讀出, 檔案內容如下:

          10               本檔案中維修項目總個數
          1001             維修項目代號
          3                維修物料個數
          a31f             需更換料號1
          a32              需更換料號2
          b01              需更換料號3
          20               標準維修時間 (分鐘)
          700              工資  
          1002
          2
          a31r
          b02
          30
          900
          ...
其中第一列為檔案中資料的筆數, 其餘資料分別為每一 "維修項目" 的 維修項目代號, 維修物料個數 n, 需更換料號1, 需更換料號2, ...,需更換料號 n, 標準維修時間 (分鐘), 工資

測試資料檔 MaintenanceItems.txt

這個資料檔案裡面的每一筆 "維修項目" 資料是獨立的, 我們設計一個 MaintenanceItem 類別來存放每一筆維修項目, 這個類別裡需要依據資料的格式來設計資料成員如下:

        int m_id;
        vector<char *> m_parts;
        int m_duration;
        int m_laborCharge;

在這個設計裡, m_parts vector 記錄的是所需要更換物料的 ID, 這也是一個簡化的實作, 以後每次要透過 MaintenanceItem 物件來存取物料時, 都要在物料的表格中搜尋一次, 比較有效率的方法應該是只記錄對應物料物件的指標

接下來你應該要設計 "序列化 (Serialization)" 的建構程式碼 MaintenanceItem(istream &is), 由資料檔案中讀出一筆一筆的資料, 建構出一個一個 MaintenanceItem 物件, 並且把這些物件記錄在一個容器裡面

你也應該順便寫一個列印 MaintenanceItem 物件裡資料內容的 print(ostream &os) 函式, 一個簡單的單元測試函式 unitTest 來讀取檔案並且列印所有讀到的資料, 在 main() 裡面呼叫單元測試確定這一段程式的正確性

和前一步驟的 Part 類別一樣, MaintenanceItem 類別的其他功能性介面目前都還沒有設計到, 稍後我們看到可以針對需要的功能再來設計

3.

修車廠

設計完上面兩個類別以後, 你也看到了應該要有兩個容器物件來代表 物料的表格以及維修項目的表格, 這兩個容器應該設計在哪裡? Global? (當然不是) main() (應該也不是, 如果所有東西都放在 main 函式裡, 程式功能就很難增加了), 可以設計一個 "修車廠" 的物件, 用這個物件的資料成員來管理上面的表格, 同時這個物件也會提供使用者介面讓使用者可以輸入維修的車輛的資料, 查詢維修車輛的狀況

將來如果程式功能持續擴大下去, 也許一個程式可以管理好幾個分廠, 所以不要把修車廠的所有功能直接放在 main() 裡面, 獨立出一個類別來, 究竟你在問題描述裡是有看到 "修車廠" 這個物件的; 如果將來程式需要管理好多個修車場, 修車場之間物料, 維修設備, 以及技師也許可以分享; 也還要設計上述表格的維護介面以及修改過的存檔功能, 甚至也還需要設計供應商的物件...

4.

日期與時間

下面是範例執行程式在執行時的輸入資料範例,
          2013 5 1
          S
          9 0
          N
          9 20 
          8934-NX 
          2 
          1002 1004
          ...
其中紅字是日期的輸入, 藍字是時間的輸入 (本來像這樣的應用程式應該是不需要輸入日期和時間的, 應該是讀取當時的系統日期和時間就可以的, 不過在設計與測試這樣的程式時, 你沒有辦法在實際運作時測試, 否則你真的要執行一整天才能測試 20 筆維修, 測試的資料恐怕太少了, 會有很多的狀況測試不到)

日期和時間的物件在這個程式裡的作用是什麼呢? 一是每一筆維修單裡面一定都有記錄日期和時間, 如果單單考慮預估完修時間的程式要求的話, 好像不需要記錄日期, 不過如果你考慮到也許以後會加上過去維修資料查詢的功能, 一個客戶的車輛進廠維修了很多次, 如果不記錄日期的話, 如何分辨是哪一次的維修, 維修了哪些項目, 如何去查詢各個更換零件的保固期?

因為需要預估完修時間, 所以你的時間類別需要支援加法以及比較大小, 目前的測試資料裡並沒有需要好多天的維修項目, 所以你的日期類別目前還不需要支援加法, 不過如果你考慮到將來可能會有多天的維修, 也許你可以把日期和時間合併在一個類別裡面, 加法的進位會比較簡單

另外日期和時間一定要可以顯示出來的, 所以列印的功能可以先設計好

5.

維修單

客戶把車輛開到保養廠時, 技師評估過以後應該會填寫一張維修單, 載明維修的車輛, 維修的項目, 預估的取車時間, 以及報價

在範例執行程式執行時的輸入資料範例中, 可以看到目前簡化的資料需要包括哪些欄位:

          2013 5 1
          S
          9 0
          N
          9 20 
          8934-NX
          2
          1002 1004
           ...
其中紅字部份包括 車號, 維修項目個數m, 維修項目1, 維修項目2, ..., 維修項目m

這些是 MaintenanceOrder 類別需要包括的資料成員, 另外當然也需要有 "進廠時間", "完修時間", "維修費用" 等欄位, 例如:

    int m_id;
    string m_licensePlate;
    vector<int> m_workIDs;
    int m_totalCharge;
    DateTime m_timeRequested;
    DateTime m_timeCompleted;

其中 m_id 是一個唯一的編號, 永遠不會重複, 所以你可以運用 static member variable 來設計

每當客戶的車輛進廠維修時就會產生一個 MaintenanceOrder 的物件, 這些物件當然也需要記錄在一個容器裡, 如此一天下來使用者可以隨時查詢某一張維修單的狀況, 車廠也可以計算每天的營收, 如車廠有客戶的資料庫的話, 也應該把維修的資料記錄在客戶資料庫裡

這個程式要求在客戶車輛進廠時就計算下列資料提供客戶:

  1. 預估維修金額
  2. 預估完修時間 (取車時間)
接下來的步驟就是要完成這兩個要求
6.

預估維修金額

首先在維修單類別應該要提供計算總價的功能 , 總價包括每一個維修項目所需要的物料的價格以及維修的工資, 所以維修單類別應該要能夠存取 步驟 2 中記錄所有 維修項目的表格, 如此才能夠知道每一個維修項目需要更換哪些物料, 需要多少工資, 另外也需要能夠存取 步驟 1 中記錄所有物料的表格, 才能知道每一個物料的價格, 在上一個步驟裡設計的 MaintenanceOrder 類別目前沒有辦法直接存取到這兩個容器

第一種修改上面類別的方法是在 "修車廠" 類別中增加取得這兩個表格的介面, 需要計算總價時就跟修車廠物件要這兩個表格的物件參考, 不過就算這樣, 維修單類別還是需要修改, 需要記錄 "修車廠" 物件的參考, 如此才能夠跟修車廠物件要這兩個表格的物件參考: 需要在 "維修單" 類別中增加一個修車廠物件的參考或是指標, 在建構每一個維修單時需要做為建構元的參數, 例如:

...

第二種修改的方法是直接在 "維修單" 類別中增加 "維修項目" 表格物件的參考或是指標, 修改 "維修單" 類別的建構元, 在建構每一個維修單時需要把 "維修項目" 表格物件的參考 做為建構元的參數; 另外也修改 "維修項目" 類別, 在維修項目物件中增加一個記錄 "物料" 表格物件的參考或是指標, 修改 "維修項目" 類別的建構元, 在建構每一個維修項目物件時需要把 "物料" 表格物件的參考或是指標 做為建構元的參數, 例如:

    class MaintenanceOrder
    {
    public:
        ...
        MaintenanceOrder::MaintenanceOrder(..., vector<MaintenanceItem> *ptrMITable, ...);
        ...
    private:
        ...
        vector<MaintenanceItem> *m_ptrMITable;
        ...
    };
    
    MaintenanceOrder::MaintenanceOrder(..., vector<MaintenanceItem> *ptrMITable, ...)
        : ..., m_ptrMITable(ptrMITable),...
    {
       ...
    }

    class MaintenanceItem  
    {
    public:
        ...
        MaintenanceItem::MaintenanceItem(..., vector<Part> *ptrPartsTable, ...);
        ...
    private:
        ...
        vector *const m_ptrPartsTable;
        ...
    };

    MaintenanceItem::MaintenanceItem(..., vector<Part> *ptrPartsTable, ...)
        : ..., m_ptrPartsTable(ptrPartsTable),...
    {
       ...
    }
7.

預估完修時間

這一次的作業裡你可以先假設只有一個技師, 一套維修的設備, 所以所有進廠的車輛都需要排在待修隊伍中一輛一輛地順序維修 (後續你可以進一步設計比較複雜的維修策略, 多種不同的維修項目可以有多組技師同時進行...), 假設給機具和維修技師 10 分鐘的休息時間, 假設實際維修時間是標準維修時間的 80%-120%

  1. 如步驟 4 所說明, 系統目前的時間請以 "新客戶到達時輸入的時間" 或是 "查詢維修狀態時輸入的時間" 為準
  2. 請注意系統運作時上述兩種輸入的時間值是遞增的 (你可以參考下面的命令列測試資料)
  3. 每當系統時間改變時, 請 修正/重新估計 在待修隊伍中還沒有維修完的車輛的 "完修時間", 已經修完的車輛的完修時間已經記錄下來, 不會再更改了
  4. 每一張維修單的 "預估取車時間" 可以用最多的時間 (120% 標準維修時間) 來估計, 如此實際的取車時間會比預估的時間早

要完成上述的功能, 在 "修車廠" 類別中應該要增加一個 "待修隊伍" 的容器物件, "維修單" 物件一產生出來以後應該就要放進這個 "待修隊伍" 物件中, 已經修完的車輛應該要修改維修單中的 完修時間欄位以後由 "待修隊伍" 中移除, 把 "待修隊伍" 中下一個維修單的 開始維修時間設為前一輛車完修時間加上十分鐘, 這個 "待修隊伍" 物件不見得要自己設計 (雖然也很簡單), 不過可以多練習使用 C++ 標準函式庫中的 queue 物件或是 deque 物件, 由於我們在程式進行中需要常常順序把 "待修隊伍" 中的維修單看過一遍, 所以其實 queue 類別是不符合需要的, 因為它不提供 iterator
8.

待修隊伍

你可以運用下面的 iterable_queue 類別, 這個類別擴充標準函式庫中的 queue 類別如下

    #include <queue>
    #include <deque>
    #include <iostream>

    template<typename T, typename Container=std::deque<T> >
    class iterable_queue : public std::queue<T,Container>
    {
    public:
        typedef typename Container::iterator iterator;
        typedef typename Container::const_iterator const_iterator;

        iterator begin() { return this->c.begin(); }
        iterator end() { return this->c.end(); }
        const_iterator begin() const { return this->c.begin(); }
        const_iterator end() const { return this->c.end(); }
    };
把上面這段程式放在 iterable_queue.h 中, 使用範例如下:
    #include "iterable_queue.h"
    #include <iostream>
    ...
    int i;
    iterable_queue<int> int_queue;
    for(i=0; i<10; ++i)
        int_queue.push(i);

    iterable_queue<int>::iterator it;
    for(it=int_queue.begin(); it!=int_queue.end(); ++it)
        std::cout << *it << "\n";

    while (!int_queue.empty())
    {
        std::cout << int_queue.front() << endl;
        int_queue.pop();
    }
或是直接運用底層雙向的 deque 類別, 簡單的使用範例如下:
    #include <iostream>
    #include <deque>
    using namespace std;
    ...
    deque<int> int_queue;
    int myint;

    cout << "Please enter some integers (enter 0 to end):\n";

    do 
    {
        cin >> myint;
        int_queue.push_back(myint);
    } 
    while (myint);

    cout << "int_queue contains: ";
    while (!int_queue.empty())
    {
        cout << " " << int_queue.front();
        int_queue.pop_front();
    }

    deque<int>::iterator it;
    for(it=int_queue.begin(); it!=int_queue.end(); ++it)
        std::cout << *it << "\n";
9.

使用者介面:

下面為使用者操作這個應用程式的範例:

D:\assign2>garage

Enter today's date: (year, month, day) 2013 5 1

[S]tatus update/[N]ew request? S
Current time? (hour, minute) 9 0

There is no car currently being maintained!

[S]tatus update/[N]ew request? N
Service request time: (hour, minute) 9 20 
License Plate: 8934-NX 
Number of Service Items: 2 
Service Items: 1002 1004
Expected finish time: 2013/05/01 10:44

[S]tatus update/[N]ew request? S
Current time? (hour, minute) 9 30

[0000] 8934-NX, Time requested:  2013/05/01 09:20 ,
                Service requested: 1002, 1004,
                Service started on 2013/05/01 09:20 ,
                Charge=NT7900
   ===> Expected finish time:  2013/05/01 10:44

[S]tatus update/[N]ew request? N
Service request time: (hour, minute) 9 50
License Plate:  1234-AB 
Number of Service Items: 2 
Service Items: 1003 2001
Expected finish time: 2013/05/01 12:30

[S]tatus update/[N]ew request? S
Current time? (hour, minute) 10 10

[0000] 8934-NX, Time requested:  2013/05/01 09:20 ,
                Service requested: 1002, 1004,
                Service started on 2013/05/01 09:20 ,
                Charge=NT7900
   ===> Expected finish time:  2013/05/01 10:44
[0001] 1234-AB, Time requested:  2013/05/01 09:50 ,
                Service requested: 1003, 2001,
                Service not yet started.,
                Charge=NT15600
   ===> Expected finish time:  2013/05/01 12:30

[S]tatus update/[N]ew request? N
Service request time: (hour, minute) 17 20 
License Plate: 4444-RR 
Number of Service Items: 2 
Service Items: 2003 2004
Expected finish time: 2013/05/01 19:14
Sorry, it's too late to accept this request! [S]tatus update/[N]ew request? S Current time? (hour, minute) 17 30 [0000] 8934-NX, Time requested: 2013/05/01 09:20 , Service requested: 1002, 1004, Service started on 2013/05/01 09:20 , Service finished on 2013/05/01 10:39 , Charge=NT7900 [0001] 1234-AB, Time requested: 2013/05/01 09:50 , Service requested: 1003, 2001, Service started on 2013/05/01 10:49 , Service finished on 2013/05/01 12:20 , Charge=NT15600 There is no car currently being maintained! [S]tatus update/[N]ew request? N Service request time: (hour, minute) 17 31

上面紅字部份為使用者輸入的資料, 其他為程式的輸出

系統啟動時先輸入當天的日期, 例如: 2013 5 1

接下來使用者有兩種選擇:

  1. S 代表希望知道目前 修車廠中 已完修維修單以及 "待修隊伍" 中維修單的資料
  2. N 代表有一輛車進廠維修, 需要產生維修單

如果使用者在產生新維修單時輸入的資料, 得到的預估完修時間超過 17:30, 系統不會接受這一份維修單

如果使用者輸入一個超過 5:30 的時間, 這時車廠已經準備關門, 如果 "待修隊伍" 中沒有維修單了 (正常狀況下應該是沒有了, 因為在產生維修單的時候我們用最多的時間來估計它的完修時間, 所以實際完修時間一定在 17:30 以前), 系統就結束。

範例執行程式
 

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

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