你好,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),天然会有人来敲门的。从看似枯燥乏味的程序代码中,咱们也能感悟出人生哲理,真是人生如代码,代码似人生。

相关文章
相关标签/搜索