你好,C++(39)6.4.4 依葫蘆畫瓢:用C++表達設計結果(下)

6.4.4  依葫蘆畫瓢:用C++表達設計結果

完成上面的分析與設計以後,小陳感受已經胸有成竹勝利在望了。他知道,只要完成了程序中的類以及類之間關係的分析和設計,整個程序就至關於已經完成了一大半。接下來的工做,不過就是依葫蘆畫瓢,用C++這種開發語言將以前的設計結果表達出來,造成具體的程序而已。ios

按照以前的設計結果,小陳決定首先實現最基礎的Employee類:程序員

// SalarySys.cpp
#include <ctime>      // 使用其中的時間函數
#include <string>    // 使用字符串對象

using namespace std;
 
// 枚舉員工的級別
enum EmpLevel
{
    enumOfficer = 1,     // 高級員工
    enumStaff = 2        // 通常員工
};
 
// 員工類
class Employee
{
public:
    // 構造函數,根據員工的姓名和入職年份構造對象
    Employee(string strName,int nY)
    :m_strName(strName),m_nYear(nY) // 設定員工的姓名和入職年份
    {}
// Employee類的行爲,這些行爲都是供外界調用的接口,
// 因此將其訪問級別設定爲public
public:
    // 得到員工姓名
    string GetName() const
    {
        return m_strName;
    }
    // 得到員工入職年份
    int GetYear() const
    {
        return m_nYear;
    }
    // 得到員工級別
    EmpLevel GetLevel() const
    {
         return m_nLevel;
    }
    // 得到員工工資,由於這個行爲同具體的員工類相關,
    // 不一樣的派生類有不一樣的行爲(計算方法),因此在基類Employee中只是
    // 用純虛函數表示接口,具體行爲由其派生類實現
    virtual int GetSalary() = 0;

    // GetWorkTime()只是供自身和本身的派生相似用,因此將其
    // 訪問級別設定爲protected
protected:  
    // 得到在職時間,也就是如今年份減去入職年份
    int GetWorkTime() const
    {
        // 得到如今的年份
         time_t t = time(0);
        struct tm* now = localtime(&t);
        // time()函數得到的時間是以1900年爲起點,因此這裏須要
         // 加上1900。同時,不滿一年按照一年計算,因此最後要加1
         return now->tm_year + 1900 - m_nYear + 1;
    }

// Employee類的屬性
// 由於這些屬性也一樣應當是其派生類具備的,須要由基類遺傳給
// 它的派生類,因此這裏使用protected訪問級別,容許其派生類繼承這些屬性
protected:
    string m_strName;    // 姓名
    int m_nYear;         // 入職年份
    EmpLevel m_nLevel;   // 級別
};

完成Employee類的實現後,就比如造房子打好了地基,小陳接着在其基礎上,派生出具體的員工類Officer和Staff,分別完成具體的工資計算:數據庫

//// 高級員工類
// 由於高級員工也是員工的「一種」,因此它能夠從Employee類採用public派生
class Officer : public Employee
{
public:
    // 構造函數
    // 直接調用基類Employee的構造函數,完成相同部分屬性的構建
    Officer(string strName, int nY)
    :Employee(strName,nY)
    {
        // 進行派生類獨有的構建工做,設定員工的特定級別
         m_nLevel = enumOfficer;
    }
public:
    // 對基類的純虛函數進行重寫,具體實現員工計算工資的行爲
    virtual int GetSalary() override
    {
        // 對於高級員工,每一年漲5000元工資
        return GetWorkTime()*5000;
    }
};

// 普通員工類
class Staff : public Employee
{
public:
    Staff(string strName, int nY)
    :Employee(strName,nY)
    {
        m_nLevel = enumStaff;
    }
public:
    // 不一樣的派生類對相同的行爲有不一樣的實現,
    // 這就是類的多態機制的體現
    virtual int GetSalary() override
    {
        // 普通員工,每一年漲1000元工資
        return GetWorkTime()*1000;
    }
};

在員工類及其派生類的實現中,全面體現了面向對象的三大特徵。首先,咱們將全部員工,包括高級員工和普通員工的共有屬性和行爲封裝成員工類Employee這個基類,這裏體現的是類對屬性和行爲的封裝;而後使用面向對象的繼承機制從員工類Employee中派生出高級員工類Officer和普通員工類Staff,這樣使得這兩個派生類能夠複用基類的代碼,例如員工的姓名和入職時間等共有屬性,以及供外界訪問的GetName()等接口函數,派生類無須重複定義而經過繼承就直接擁有了。派生類所要作的,只是實現本身特有的屬性和行爲。例如,兩個派生類各自對工資的計算方式不一樣,因此利用面向對象的多態機制,它們對基類提供的用於計算工資的GetSalary()純虛函數進行重寫,各自完成了本身特殊的工資計算方式。設計模式

完成了具體的員工類的實現,接下來就是用它們建立具體的員工對象並交由最核心的SalarySys類對其進行管理。按照前面的設計,小陳用一個數組來保存這些員工對象的指針,同時又分別實現了SalarySys類的其餘行爲,完成對這些員工對象的輸入、查詢和輸出:數組

// 引入須要的頭文件
#include <iostream>  // 屏幕輸入輸出
#include <fstream>   // 文件輸入輸出
#include <climits>   // 引入INT_MAX

//// 定義SalarySys中數組的最大數據量,
// 也就是SalarySys最多能處理多少個員工數據
const int MAX = 100000;

// 工資管理類SalarySys
class SalarySys
{
public:
    // 構造函數,對屬性進行初始化
    SalarySys()
        :m_nCount(0), // 設定當前數據量爲0
        m_strFileName("SalaryData.txt") // 設定員工數據文件名
    {
        // 對數組進行初始化,使得數組中都是nullptr
         for(long i = 0; i < MAX; ++i)
        {
             m_arrEmp[i] = nullptr;
        }

        // 讀取員工數據文件
         Read();
    }

    // 析構函數,完成清理工做
    ~SalarySys()
    {
        // 將員工數據寫入文件,以備下次讀取
         Write();
        // 釋放數組中已經建立的員工對象
         for(long i = 0; i < m_nCount; ++i)
        {
             delete m_arrEmp[i];  // 釋放對象
             m_arrEmp[i] = nullptr;   // 將指針設置爲nullptr
        }
    }
// SalarySys的公有行爲
public:
    // 從員工數據文件讀取已經輸入的數據
    int Read()
    {
        // 用於文件讀取的中間臨時變量
         string strName = "";
        int nLevel = 0;
        int nYear = 0;

        // 讀取的數據個數
         int i = 0;
        // 打開數據文件
         ifstream in(m_strFileName);
        if(in.is_open()) // 判斷是否成功打開
        {
             // 若是打開文件成功,構造無限循環進行讀取
             while(true)
             {
                 // 分別讀取姓名、級別和入職年份
                 in>>strName>>nLevel>>nYear;

                 // 判斷是否讀取正確,若是讀取錯誤,
                 // 例如讀取到達文件末尾,則結束讀取
                 if(!in)
                     break; // 跳出讀取循環

                 // 根據讀取的員工級別,分別建立不一樣的員工對象,
                 // 並保存到m_arrEmp數組進行管理
                 if( enumOfficer == nLevel)
                 {
                     // 根據員工姓名和入職年份,建立高級員工對象
                     m_arrEmp[i] = new Officer(strName,nYear);
                     ++i; // 記錄已經讀取的數據數量
                 }
                 else if ( enumStaff == nLevel)
                 {
                     m_arrEmp[i] = new Staff(strName,nYear);
                     ++i; // 記錄已經讀取的數據數量
                 }
                
                 // 若是讀取的數量大於數組容量,則結束讀取,不然繼續下一次讀取
                 if(i >= MAX)                    
                     break;
             }
             // 讀取完畢,關閉文件
             in.close();
        }
        // 輸出讀取結果並返回讀取的數據個數
         cout<<"已讀取"<<i<<"個員工數據"<<endl;
        m_nCount = i;  // 記錄數組中有效數據的個數

         return i;
    }
   
    // 將員工數據寫入文件
    void Write()
    {
        // 打開數據文件做爲輸出
         ofstream o(m_strFileName);
        if(o.is_open())
        {
             // 若是成功打開文件,則利用for循環逐個輸出數組中保存的數據
             for(int i = 0;i < m_nCount; ++i)
             {
                 Employee* p = m_arrEmp[i];
                 // 輸出各個員工的各項屬性,以Tab間隔
                 o<<p->GetName()<<"\t"    // 名字
                     <<p->GetLevel()<<"\t"    //級別
                     <<p->GetYear()<<endl;    // 入職年份
             }

            // 輸出完畢,關閉文件
             o.close();
        }
    }  


    // 手工輸入員工數據
    int Input()
    {
        // 提示輸入
        cout<<"請輸入員工信息(名字 級別(1-通常員工,2-高級員工) 入職年份),例如:Wanggang 1 1982"<<endl;
         cout<<"-1表示輸入結束"<<endl;

        // 新輸入的數據保存在數組已有數據以後,
         // 因此這裏將已有數據個數m_nCount做爲輸入起點
        // 又由於i在for循環以後還須要用到,因此定義在for循環以前

         int i = m_nCount;
        for(; i < MAX; ++i) // 初始化語句留空
        {
             // 利用for循環逐個輸入
             cout<<"請輸入"<<i<<"號員工的信息:"<<endl;
             // 根據輸入的數據建立具體的員工對象,並保存到數組
             string strName = "";
             int nL = 0;
             int nY = 0;

             // 獲取用戶輸入
             cin>>strName>>nL>>nY;

             // 對輸入狀況進行判斷處理
             if(!cin) // 若是輸入錯誤,則從新輸入
             {
                 cout<<"輸入錯誤,請從新輸入"<<endl;
                 cin.clear(); // 清理輸入標誌位
                 cin.sync();  // 清空鍵盤緩衝區
                 --i; // 本次輸入做廢,不計算在內
                 continue; // 直接開始下一次輸入循環
             }
             else // 輸入正確
             {
                 // 檢查是否輸入結束
                 if("-1" == strName)
                 {
                     break; // 結束輸入循環
                 }
                 // 根據輸入的數據,建立具體的員工對象並保存到數組
                 if(enumOfficer == nL)
                     m_arrEmp[i] = new Officer(strName,nY);
                 else if(enumStaff == nL)
                     m_arrEmp[i] = new Staff(strName,nY);
                 else  // 員工級別輸入錯誤
                 {
                     cout<<"錯誤的員工級別,請從新輸入"<<endl;
                      --i;
                     cin.clear(); // 清理輸入標誌位
                     cin.sync();  // 清空鍵盤緩衝區
                      continue;
                  }
             }
        }

        // 輸入完畢,調整當前數組中的數據量
         m_nCount = i;

        // 返回本次輸入完成後的數據個數
         return m_nCount;
    }

    // 得到最高工資的員工對象
    Employee* GetMax()
    {
        // 表示結果的指針,初始值爲nullptr
         Employee* pMax = nullptr;
        // 設定一個假想的當前最大值,也就是最小的int類型數據值
         int nMax = INT_MIN;
        // 用for循環遍歷數組中的每個對象
         for(int i = 0;i < m_nCount; ++i)
        {
             // 若是當前對象的工資高於當前最大值nMax,則將當前對象的工資
             // 做爲新的當前最大值,並將當前對象的指針做爲結果保存
             // 這裏使用的是基類Employeed 的指針調用GetSalry()虛函數來得到
             // 當前對象的工資,而實際上,它將動態地調用這個指針所指向的實際對象的
             // 相應函數來完成工資的計算。換言之,若是這個指針指向的是Officer對象,
             // 就會調用Officer類的GetSalary()函數,若是指向的是Staff對象,
             // 就會調用Staff類的GetSalary()函數。這樣就實現了不一樣等級
             // 的員工,不一樣的工資計算方式,使用統一的調用方式。
             if(m_arrEmp[i]->GetSalary() > nMax)
             {
                 // 則將當前對象記錄爲結果對象
                 pMax = m_arrEmp[i];
                 // 並將當前對象的工資記錄爲當前最大值
                 nMax = pMax->GetSalary();
             }
        }

         // 返回指向擁有最高工資的員工對象的指針
        return pMax;
    }

    // 查詢員工工資
    void Find()
    {
        // 構造無限循環進行查詢
         while(true)
        {
             // 查詢的姓名
             string strName = "";
             // 輸入提示
             cout<<"請輸入要查詢的員工名字(-1表示結束查詢):"<<endl;
             // 獲取用戶輸入的員工姓名
             cin>>strName;

             // 對用戶輸入進行檢查
             if(!cin) // 若是輸入錯誤,提示從新輸入
             {
                 cout<<"輸入錯誤,請從新輸入"<<endl;
                 cin.clear();
                 cin.sync();
                 continue;  // 開始下一次查詢
             }
             else if("-1" == strName) // 若是查詢結束
             {
                 // 查詢結束,用break結束查詢循環
                 cout<<"查詢完畢,感謝使用!"<<endl;
                 break;
             }
             // 記錄是否找到查詢的員工
             bool bFind = false;
             // 用for循環遍歷全部員工對象,逐個進行比對查找
             for(int i = 0;i < m_nCount;++i)
             {
                 // 得到指向當前對象的指針
                 Employee* p = m_arrEmp[i];
                 // 判斷當前對象的名字是否與查詢條件相同
                 if(strName == p->GetName())
                 {
                     // 輸出符合查詢條件的員工信息
                     cout<<"員工姓名:"<<p->GetName()<<endl;
                     cout<<"員工工資:"<<p->GetSalary()<<endl;
                     bFind = true; // 記錄本次查詢成功
                     break;  // 跳出for循環結束查詢     // 結束循環
                 }
             }

             // 若是本次沒有找到,則提示用戶從新輸入
             if(!bFind)
             {
                 cout<<"沒法找到名字爲"<<strName<<"的員工。"<<endl;
                  cout<<"請覈對姓名,從新輸入"<<endl;
             }  
        }
    }

// SlarySys類的屬性
// 由於這些屬性都只是供SalarySys類訪問,
// 因此其訪問級別設定爲private
private:
    // 數據文件名,爲了防止被錯誤修改,因此使用const關鍵字修飾
    // 使用const修飾的成員變量,必須在類構造函數的初始化列表中進行初始化
    // 在C++11中,也能夠在定義時直接賦值初始化
    const string m_strFileName;   
    Employee* m_arrEmp[MAX];  // 保存員工對象指針的數組
    int m_nCount; // 數組中已有的員工對象數
};

完成了工資系統類SalarySys以後,實際上就是萬事俱備,只欠東風了。接下來就只須要在主函數中運用上面建立的這些類來完成需求設計中的各個用例,那就大功告成了:ide

//

int main()
{
    // 建立一個SalarySys對象
    // 在構造函數中,它會首先去讀取數據文件中的員工數據,
    // 完成「「從文件讀取」這一用例
    SalarySys sys;
 
    // 讓用戶輸入數據,完成「手工輸入」用例
    sys.Input();

    // 調用SalarySys的GetMax()函數得到工資最高的員工對象,
    // 完成「計算最大值」用例
    Employee* pMax = sys.GetMax();
    if(nullptr != pMax)
    {
        cout<<"工資最高的員工是:"<<endl;
        cout<<"名字:"<<pMax->GetName()<<endl;
        cout<<"工資:"<<pMax->GetSalary()<<endl;
    }

    // 調用SalarySys類的Find()函數,完成「查詢工資」用例
    sys.Find();

    // 最後,當sys對象析構的時候,會調用本身的Write()函數,
    // 完成「輸出數據到文件」用例
    return 0;
}

有了面向對象思想和類的幫助,短短的幾百行代碼,小陳就完成了一個功能強大的工資程序。從這裏小陳也體會到,用面向對象思想進行分析與設計,更加接近於咱們分析問題、解決問題的思惟習慣,這使得工資程序的設計更加直觀、更加天然,程序結構也更加清晰,實現起來天然也就更加容易了。封裝,可讓函數和它所操做的數據捆綁在一塊兒成爲對象,能夠起到很好的數據保護的做用;繼承,能夠複用共同的屬性和行爲,起到代碼複用的做用。同時還能夠很方便地對其進行擴展,從而支持更多更新的需求;多態,讓咱們能夠以一致的調用方式,實現不一樣的操做行爲。從而使得咱們在設計中考慮得更多的是接口問題,而不用擔憂後面的實現問題。函數

當小陳自信滿滿地將改寫後的工資程序拿給老闆使用之後,老闆更是讚不絕口:大數據

「不錯不錯,不只能動態地計算各類員工的工資,而且時間變化之後,工資也會跟着變化。能夠統計最高工資員工的姓名,查詢的時候,也能夠根據名字進行查詢。我想要的功能都很好地實現了嘛,幹得不錯,啊哈哈……,下個月,漲工資,啊哈哈哈……」spa

當再次聽到老闆的「漲工資」時,小陳已經沒有先前那麼激動了,他反問了一句:設計

「真的?」

「固然是真的,」老闆馬上掩飾說,「我何時說話算數啊!」

聽到這話,小陳也不去戳穿老闆的假裝。如今在他看來,學好C++比漲工資更加劇要,如今他已經愈來愈感覺到C++的魅力,已經開始愛上C++了。

 

設計模式:像建築師同樣思考

上面的工資程序是否已經太過複雜,讓你的頭感到有點隱隱做痛?

若是是,那麼你必定須要來一片程序員專用的特效止痛片——設計模式。

設計模式(Design Pattern)是由Erich Gamma等4人在90年代從建築設計領域引入到軟件設計領域的一個概念。他們發現,在建築領域存在這樣一種複用設計方案的方法,那就是在某些外部環境類似,功能需求相同的地方,建築師們所採用的設計方案也是類似的,一個地方的設計方案同時能夠在另一個類似的地方複用。這樣就大大提升了設計的效率節約了成本。他們將這一複用設計的方法從建築領域引入到軟件設計領域,從而提出了設計模式的概念。他們總結了軟件設計領域中最多見的23種模式,使其成爲那些在軟體設計中廣泛存在(反覆出現)的各類問題的解決方案。而且,這些解決方案是通過實踐檢驗的,當咱們在開發中遇到(由於這些問題的廣泛性,咱們也必定會常常遇到)類似的問題時,只要直接採用這些解決方案,複用前人的設計成果就能夠很好地解決今人的問題,這樣能夠節約設計成本,大大提升咱們的開發效率。

那麼,設計模式是如何作到這一點的呢?設計模式並不直接用來完成代碼的編寫,而是描述在各類不一樣狀況下,要怎樣解決問題的一種方案。面向對象設計模式一般以類或對象來描述其中的各個實體之間的關係和相互做用,但不涉及用來完成應用程序的特定類或對象。設計模式能使不穩定依賴於相對穩定、具體依賴於相對抽象,儘可能避免會引發麻煩的緊耦合,以加強軟件設計適應變化的能力。這樣可讓咱們的軟件具備良好的結構,可以適應外部需求的變化,可以避免軟件由於不斷增長新功能而顯得過於臃腫,最後陷入需求變化的深淵。另一方面,設計模式都是前人優秀設計成果的總結,在面對類似問題的時候,直接複用這些通過實踐檢驗的設計方案,不只能夠保證咱們設計的質量,還能夠節省設計時間,提升開發效率。從某種意義上說,設計模式能夠說是程序員們的止痛藥——再也沒有需求變化帶來的痛苦。

爲了讓你們真正地感覺到設計模式的魅力,咱們來看一看衆多設計模式當中最簡單的一個模式——單件模式(Singleton Pattern)。顧名思義,單件模式就是讓某個類在任什麼時候候都只能建立惟一的一個對象。這樣的需求看起來比較特殊,可是有這種需求的場景卻很是普遍,好比,咱們要設計開發一個打印程序,咱們只但願有一個Print Spooler對象,以免兩個打印動做同時輸送至打印機中;在數據庫鏈接中,咱們也一樣但願在程序中只有惟一的一個數據庫鏈接以節省資源;在上面工資程序中的SalarySys類,也一樣須要保證它在整個程序中只有惟一的一個實例對象,要否則每一個人的工資在不一樣的SalarySys對象中就可能會產生衝突;甚至在一個家庭中,咱們都是一個老公只能有一個老婆,若是有多個老婆確定會出問題。單件模式,就是用來保證對象可以被建立而且只可以被建立一次。在程序中,全部客戶使用的對象都是惟一的一個對象。

咱們都知道,對象的建立是經過構造函數來完成的,因此單件模式的實現關鍵是將類的構造函數設定爲private訪問級別,讓外界沒法經過構造函數自由地建立這個類的對象。取而代之的是,它會提供一個公有的靜態的建立函數來負責對象的建立,而在這個建立函數中,咱們就能夠判斷惟一的對象是否已經建立。若是還沒有建立,則調用本身的構造函數建立對象並返回,若是已經建立,則直接返回已經建立的對象。這樣,就保證了這個類的對象的惟一性。例如,咱們能夠用單件模式來改寫上面例子中的SalarySys類,以保證SalarySys對象在程序中的惟一性:

// 使用單件模式實現的SalarySys類
class SalarySys
{
// 省略SalarySys類的其餘屬性和行爲 
//...
// 將構造函數私有化(private)
private:
    SalarySys()
    :m_nCount(0), 
    m_strFileName("SalaryData.txt")
    {
         //
     }
public:
// 提供一個公有的(public,爲了讓客戶可以訪問)靜態的(static,爲了讓
// 客戶能夠在不建立對象的狀況下直接訪問)建立函數,
// 供外界獲取SalarySys的惟一對象
// 在這個函數中,對對象的建立行爲進行控制,以保證對象的惟一性
    static SalarySys* getInstance()
    {
        // 若是惟一的實例對象尚未建立,則建立實例對象
        if ( nullptr == m_pInstance )
            m_pInstance = new SalarySys();
        // 若是已經建立實例對象,則直接返回這個實例對象
        return m_pInstance;
    };
private:
// 靜態的對象指針,指向惟一的實例對象
// 爲靜態的惟一實例對象指針賦初始值,表示對象還沒有建立
    static SalarySys* m_pInstance = nullptr;
};

//
 
int main()
{
// 第一次調用getInstance()函數,惟一的SalarySys對象還沒有建立,
// 則建立相應的對象並返回指向這個對象的指針
    SalarySys* pSalarySys1 = SalarySys::getInstance();

    //// 第二次調用getInstance()函數,這時SalarySys的對象已經建立,
// 則再也不建立新對象而直接返回指向那個已建立對象的指針,保證對象的惟一性
    SalarySys* pSalarySys2 = SalarySys::getInstance();

//// 釋放已建立的對象, pSalarySys1和pSalarySys2指向的是同一個對象,
// 使用pSalarySys1或pSalarySys2釋放這個對象是等效的,並只須要釋放一次
    delete pSalarySys1;
    pSalarySys1 = pSalarySys2 = nullptr;
 
    return 0;
}

通過單件模式的改寫,SalarySys類的構造函數已經變成私有的,在主函數中就不能直接使用new關鍵字來建立一個實例對象,而只能經過它提供的公有的getInstance()函數來得到這個類的惟一實例對象。這裏須要注意的是,爲了實現單件模式,咱們在SalarySys的m_pInstance成員變量和getInstance()成員函數前都加上了static關鍵字對其進行修飾,這表示這個成員變量和成員函數都將是靜態的,咱們能夠經過類做用域符號(「::」)直接訪問類的靜態成員而無需任何類的實例對象。靜態成員的這種特性,爲咱們以私有的構造函數以外的成員函數來建立類的對象提供了可能。同時,在getInstance()函數中咱們能夠對對象的建立行爲進行控制:若是對象還沒有建立,則建立對象;若是對象已經建立完成,則直接返回已經建立完成的對象,這樣就有效地保證了其實例對象的惟一性。

縱觀整個單件模式,它的實現關鍵是將構造函數私有化(用private修飾),這才構成了這個對象只能本身構建本身,防止了外界建立這個類的對象,將建立對象的權利收歸本身全部。經過這樣將本身封閉起來,也就只能孤孤單單一我的了。這個模式對於那些仍在過「光棍節」的朋友一樣有啓發意義,咱們之因此是單件,並非咱們沒法建立對象,只是由於咱們本身把本身封閉(private)起來了,而要想擺脫單件的狀態,只須要把咱們的心敞開(public),天然會有人來敲門的。從看似枯燥乏味的程序代碼中,咱們也能感悟出人生哲理,真是人生如代碼,代碼似人生。

相關文章
相關標籤/搜索