第1章 重构,第一个案例:运用多态取代switch

Posted 浅墨浓香

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第1章 重构,第一个案例:运用多态取代switch相关的知识,希望对你有一定的参考价值。

3. 运用多态取代与价格相关的条件逻辑

3.1 switch和“常客积分”代码的再次搬迁

(1)switch:最好不要在另一个对象的属性上运用switch语句

switch(getMovie().getPriceCode())   //在movie对象的priceCode属性上运用switch
{                                   //这意味着可以将getCharge函数从Rental类移动到Movie类去
                                    //选择在Movie类中封装计算费用功能,还有一个
                                    //原因,就是可以控制因影片类型变化导致的计算
                                    //方式变化,从而对其它对象产生影响。
}

(2)常客积分:getFrequentRenterPoints函数的再次搬迁。用跟处理getCharge相同的手法处理常客积分,将因影片类型变化而变化的所有东西都放到Movie类中去处理。Rental类中只需调用Movie相应的方法即可。

【实例分析】影片出租1.3.1

 

//第1章:重构,第1个案例
//场景:影片出租,计算每一位顾客的消费金额
/*
说明:
1. 影片分3类:普通片、儿童片和新片。
2. 每种影片计算租金的方式。
   A.普通片:基本租金为2元,超过2天的部分每天加1.5元
   B.新片:租期*3
   C.儿童片:基本租金为1.5元,超过3天的部分每天加1.5元
3. 积分的计算:每借1片,积分加1,如果是新片且租期1天以上的额外赠送1分。
*/
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
using namespace std;

//影片类
class Movie
{
private:
    string title; //片名
    int priceCode; //价格

public:
    enum MovieType{
        REGULAR = 0, //普通片
        NEW_RELEASE, //新片
        CHILDRENS    //儿童片
    };

    Movie(string title, int priceCode)
    {
        this->title = title;
        this->priceCode = priceCode;
    }

    string getTitle(){return title;}
    void setTitle(string value)
    {
        title = value;
    }

    int getPriceCode(){return priceCode;}
    void setPriceCode(int value)
    {
        this->priceCode = value;
    }

    //将原来Rental类中的getCharge移到该类中,并将租期作为参数传入
    //搬到这里来的原因是
    //1.switch语句中getPriceCode为本类对象的属性
    //2.封装影片类型的变化导致计算方式变化于该类中,从而降低对其他类的影响
    double getCharge(int daysRented)
    {
        double result = 0 ;//相当于statement中的thisamount;

        switch(getPriceCode())
        {
        case Movie::REGULAR:
            result += 2;    //普通片基本租金为2元
            if(daysRented > 2)  //超过2天的每天加1.5元
                result +=(daysRented - 2 ) * 1.5;
            break;
        case Movie::NEW_RELEASE:
            result += daysRented * 3;    //新片的租金
            break;
        case Movie::CHILDRENS:
            result += 1.5;    //儿童片基本租金为1.5元
            if(daysRented > 3)  //超过3天的每天加1.5元
                result +=(daysRented - 3 ) * 1.5;
            break;
        }

       return result;
    }

    //将原Rental类中常客积分搬到该类中
    //原因是常客积分的计费方式与影片类型有关,也是为了控制当
    //影片类型变化时,由于计算方式变化对其他类的影响
    int getFrequentRenterPoints(int daysRented)
    {
        //如果是新片且租期超过1天以上,则额外送1分积分
        if ((getPriceCode() == Movie::NEW_RELEASE) &&
            daysRented > 1 )  return 2;
        else  return 1;
    }
};

//租赁类(表示某个顾客租了一部影片)
class Rental
{
private:
    Movie& movie;   //所租的影片
    int daysRented; //租期
public:
    Rental(Movie& movie, int daysRented):movie(movie)
    {
        this->daysRented = daysRented;
    }

    int getDaysRented(){return daysRented;}

    Movie& getMovie()
    {
        return movie;
    }

    double getCharge()
    {
        return movie.getCharge(daysRented);
    }

    //将原Customer类的statement中计算常客积分的代码移到Rental类
    int getFrequentRenterPoints()
    {
        return movie.getFrequentRenterPoints(daysRented);
    }
};

//顾客类(用来表示顾客)
class Customer
{
private:
    string name; //顾客姓名
    vector<Rental*> rentals; //每个租赁记录

     //获得总消费
     double getTotalCharge()
     {
        double result = 0;
        vector<Rental*>::iterator iter = rentals.begin();
        while( iter != rentals.end())
        {
            Rental& each = *(*iter);

            result += each.getCharge();

            ++iter;
        }
        return result;
     }

     //获得总积分
    int getTotalFrequentRenterPointers()
    {
        int result = 0;

        vector<Rental*>::iterator iter = rentals.begin();
        while( iter != rentals.end())
        {
            Rental& each = *(*iter);

            result += each.getFrequentRenterPoints();

            ++iter;
         }

         return result;
     }

     void cleanUp()
     {
         vector<Rental*>::iterator iter = rentals.begin();
         while( iter != rentals.end())
         {
             delete(*iter);
            ++iter;
         }
         rentals.clear();
     }

     template <typename T>
     string numToString(T num)
     {
         stringstream ss;
         ss << num;
         return ss.str();
     }

public:
    Customer(string name)
    {
        this->name = name;
    }

    void addRental(Rental* value)
    {
        rentals.push_back(value);
    }

    string getName(){return name;}

    //statement(报表),生成租赁的详单
    string statement()
    {
        string ret = "Rental Record for " + name + "\\n";

        vector<Rental*>::iterator iter = rentals.begin();
        while( iter != rentals.end())
        {
            Rental& each = *(*iter);

            //显示每个租赁记录

            ret += "\\t" + each.getMovie().getTitle() + "\\t" +
                 numToString(each.getCharge())+ "\\n";

            ++iter;
        }

        //增加页脚注释
        ret += "Amount owed is " + numToString(getTotalCharge()) + "\\n";//用getTotalCharge代替totalAmount
        ret += "You earned " + numToString(getTotalFrequentRenterPointers()) +"\\n";

        return ret;
    }

    ~Customer()
    {
        cleanUp();
    }
};

void init(Customer& customer)
{
    Movie* mv = new Movie("倚天屠龙记",Movie::REGULAR);
    Rental* rt = new Rental(*mv, 2);
    customer.addRental(rt);

    mv = new Movie("新水浒传",Movie::NEW_RELEASE);
    rt = new Rental(*mv, 3);
    customer.addRental(rt);

    mv = new Movie("喜羊羊与灰太狼",Movie::CHILDRENS);
    rt = new Rental(*mv, 5);
    customer.addRental(rt);
}

int main()
{
    Customer customer("SantaClaus");
    init(customer);

    cout << customer.statement() <<endl;

    return 0;
}
/*输出结果
Rental Record for SantaClaus
        倚天屠龙记      2
        新水浒传        9
        喜羊羊与灰太狼  4.5
Amount owed is 15.5
You earned 4
*/
View Code

3.2 运用子类取代类型码

 

(1)使用继承子类的方式,可以利用多态来取代switch语句

(2)重构的步骤

  ①使用“自我封装字段”的方法将类型码通过get/set函数封装起来(如Movie类的getPriceCode函数)。如果类型码被传递给构造函数,就需要将构造函数换成工厂方法(如createMovie函数)

  ②以类型码的宿主类为基类,为类型码的每一个数值建立一个相应的子类。在每个子类中覆写类型码的取值函数,使其返回相应的类型码值。(见Movie子类getPriceCode)

  ③从父类中删除保存类型码的字段(即旧Movie类的priceCode字段),将类型码访问函数声明为抽象函数(如Movie中的getPriceCode)

  ④使用pushDownMethod/Field方法将与特定类型码相关的函数推到子类来实现(如本例中的getCharge函数)

(3)缺点:

  ①对于某个具体对象,在其生命周期中其状态(或本例中类型码)是不可改变(如代表Movie子类型的priceCode是不能更改的),所以当创建了一部影片出来以后,就不能修改其类型了。如,现在某影片是“新片”类型,但即使随着时间的推移,也不能更改为“普通片”或“儿童片”了。

  ②我们总是在避免使用switch语句,虽然利用了多态将各个case语句的代码分解到相应的子类中去实现。但在Movie类的createMovie函数中仍然要出现switch语句。幸运的是,仅此一处用到switch,并且只用于决定创建何种对象而没有其他的业务逻辑,所以这样的switch语句是可以接受的

【实例分析】影片出租1.3.2

//types.h

#include <vector>
#include <string>
#include <sstream>
using namespace std;

//影片类
class Movie
{
private:
    string title; //片名
    //int priceCode; //价格,注意,这里被注释了

public:
    enum MovieType{
        REGULAR = 0, //普通片
        NEW_RELEASE, //新片
        CHILDRENS    //儿童片
    };

    Movie(string title);

    static Movie* createMovie(string title, int priceCode);

    string getTitle();
    void setTitle(string value);

    //类型码的“自我封装”(提供取值函数)
    virtual int getPriceCode() = 0;

    //将原来Rental类中的getCharge移到该类中,并将租期作为参数传入
    //搬到这里来的原因是
    //1.switch语句中getPriceCode为本类对象的属性
    //2.封装影片类型的变化导致计算方式变化于该类中,从而降低对其他类的影响
    virtual double getCharge(int daysRented);

    int getFrequentRenterPoints(int daysRented);
};

//普通片:用子类取代类型码
class RegularMovie: public Movie
{
public:
    RegularMovie(string title);
    int getPriceCode();
    double getCharge(int daysRented);
};

//儿童片:
class ChildrensMovie: public Movie
{

public:
    ChildrensMovie(string title);
    int getPriceCode();
    double getCharge(int daysRented);
};

//新片
class ReleaseMovie: public Movie
{
public:
    ReleaseMovie(string title);
    int getPriceCode();
    double getCharge(int daysRented);
};


//租赁类(表示某个顾客租了一部影片)
class Rental
{
private:
    Movie& movie;   //所租的影片
    int daysRented; //租期
public:
    Rental(Movie& movie, int daysRented);

    int getDaysRented();

    Movie& getMovie();

    double getCharge();

    //将原Customer类的statement中计算常客积分的代码移到Rental类
    int getFrequentRenterPoints();
};

//顾客类(用来表示顾客)
class Customer
{
private:
    string name; //顾客姓名
    vector<Rental*> rentals; //每个租赁记录

     //获得总消费
     double getTotalCharge();

     //获得总积分
    int getTotalFrequentRenterPointers();

     void cleanUp();

     template <typename T>
     string numToString(T num)
     {
        stringstream ss;
        ss << num;
        return ss.str();
    }

public:
    Customer(string name);

    void addRental(Rental* value);
    string getName();

    //statement(报表),生成租赁的详单
    string statement();

    ~Customer();
};

//main.cpp

//第1章:重构,第1个案例
//场景:影片出租,计算每一位顾客的消费金额
/*
说明:
1. 影片分3类:普通片、儿童片和新片。
2. 每种影片计算租金的方式。
   A.普通片:基本租金为2元,超过2天的部分每天加1.5元
   B.新片:租期*3
   C.儿童片:基本租金为1.5元,超过3天的部分每天加1.5元
3. 积分的计算:每借1片,积分加1,如果是新片且租期1天以上的额外赠送1分。
*/
#include <iostream>
#include "types.h"
using namespace std;

//*********************************************影片类*************************************
Movie::Movie(string title)
{
    this->title = title;
}
//提供创建子类实例的静态函数(也可以使用工厂方法)
Movie* Movie::createMovie(string title, int priceCode)
{
    Movie* ret = NULL;
    //利用子类替代switch的分支。我们总是在避免使用switch语句。但这里
    //只有一处用到switch,并且只用于决定创建何种对象而没有其他的业务逻辑
    //所以这样的switch语句是可以接受的。
    switch(priceCode)
    {
        case Movie::REGULAR:
            ret = new RegularMovie(title);
            break;
        case Movie::CHILDRENS:
            ret = new ChildrensMovie(title);
            break;
        case Movie::NEW_RELEASE:
            ret = new ReleaseMovie(title);
            break;
    }

    return ret;
}

string Movie::getTitle()
{
    return title;
}

void Movie::setTitle(string value)
{
    title = value;
}

double Movie::getCharge(int daysRented)
{
    return Movie::REGULAR;
}

//将原Rental类中常客积分搬到该类中
//原因是常客积分的计费方式与影片类型有关,也是为了控制当
//影片类型变化时,由于计算方式变化对其他类的影响
int Movie::getFrequentRenterPoints(int daysRented)
{
    //如果是新片且租期超过1天以上,则额外送1分积分
    if ((getPriceCode() == Movie::NEW_RELEASE) &&
        daysRented > 1 )  return 2;
    else  return 1;
}

//普通片:用子类取代类型码
RegularMovie::RegularMovie(string title):Movie(title)
{
}

int RegularMovie::getPriceCode()
{
    return Movie::REGULAR;
}

//使用pushDownMethod(Field)方法将与特定类型码相关的函数推到子类来实现
double RegularMovie::getCharge(int daysRented)
{
    double result = 2 ;

    if(daysRented > 2)  //超过2天的每天加1.5元
        result +=(daysRented - 2 ) * 1.5;

   return result;
}

//儿童片
ChildrensMovie::ChildrensMovie(string title):Movie(title)
{
}

int ChildrensMovie::getPriceCode()
{
    return Movie::CHILDRENS;
}

//使用pushDownMethod(Field)方法将与特定类型码相关的函数推到子类来实现
double ChildrensMovie::getCharge(int daysRented)
{
    double result = 1.5;//儿童片基本租金为1.5元

    if(daysRented > 3)  //超过3天的每天加1.5元
        result +=(daysRented - 3 ) * 1.5;

    return result;
}

//新片
ReleaseMovie::ReleaseMovie(string title):Movie(title)
{
}

int ReleaseMovie::getPriceCode()
{
    return Movie::NEW_RELEASE;
}
//使用pushDownMethod(Field)方法将与特定类型码相关的函数推到子类来实现
double ReleaseMovie::getCharge(int daysRented)
{
    return  daysRented * 3;    //新片的租金
}

//********************************租赁类(表示某个顾客租了一部影片)**********************************
Rental::Rental(Movie& movie, int daysRented):movie(movie)
{
    this->daysRented = daysRented;
}

int Rental::getDaysRented(){return daysRented;}

Movie& Rental::getMovie()
{
    return movie;
}

double Rental::getCharge()
{
    return movie.getCharge(daysRented);
}

//将原Customer类的statement中计算常客积分的代码移到Rental类
int Rental::getFrequentRenterPoints()
{
    return movie.getFrequentRenterPoints(daysRented);
}

//*********************************顾客类(用来表示顾客)*************************************
//获得总消费
double Customer::getTotalCharge()
{
    double result = 0;
    vector<Rental*>::iterator iter = rentals.begin();
    while( iter != rentals.end())
    {
        Rental& each = *(*iter);

        result += each.getCharge();

        ++iter;
    }
    return result;
}

 //获得总积分
int Customer::getTotalFrequentRenterPointers()
{
    int result = 0;

    vector<Rental*>::iterator iter = rentals.begin();
    while( iter != rentals.end())
    {
        Rental& each = *(*iter);

        result += each.getFrequentRenterPoints();

        ++iter;
    }

    return result;
}

void Customer::cleanUp()
{
    vector<Rental*>::iterator iter = rentals.begin();
    while( iter != rentals.end())
    {
        delete(*iter);
        ++iter;
    }
    rentals.clear();
}

Customer::Customer(string name)
{
    this->name = name;
}

void Customer::addRental(Rental* value)
{
    rentals.push_back(value);
}

string Customer::getName(){return name;}

//statement(报表),生成租赁的详单
string Customer::statement()
{
    string ret = "Rental Record for " + name + "\\n";

    vector<Rental*>::iterator iter = rentals.begin();
    while( iter != rentals.end())
    {
        Rental& each = *(*iter);

        //显示每个租赁记录

        ret += "\\t" + each.getMovie().getTitle() + "\\t" +
             numToString(each.getCharge())+ "\\n";

        ++iter;
    }

    //增加页脚注释
    ret += "Amount owed is " + numToString(getTotalCharge()) + "\\n";//用getTotalCharge代替totalAmount
    ret += "You earned " + numToString(getTotalFrequentRenterPointers()) +"\\n";

    return ret;
}

Customer::~Customer()
{
    cleanUp();
}

//************************************************初始化数据********************************************

void init(Customer& customer)
{
    Movie* mv = Movie::createMovie("倚天屠龙记",Movie::REGULAR);
    Rental* rt = new Rental(*mv, 2);
    customer.addRental(rt);

    mv = Movie::createMovie("新水浒传",Movie::NEW_RELEASE);
    rt = new Rental(*mv, 3);
    customer.addRental(rt);

    mv = Movie::createMovie("喜羊羊与灰太狼",Movie::CHILDRENS);
    rt = new Rental(*mv, 5);
    customer.addRental(rt);
}

int main()
{
    Customer customer("SantaClaus");
    init(customer);

    cout << customer.statement() <<endl;

    return 0;
}
/*输出结果
Rental Record for SantaClaus
        倚天屠龙记      2
        新水浒传        9
        喜羊羊与灰太狼  4.5
Amount owed is 15.5
You earned 4
*/

3.3 以state/strategy取代类型码

(1)对象的状态在生命周期内可以变化,可以选用state模式(本例中有3种状态:REGULAR、CHILDREN、NEW_RELEASE)。而前一个例子中,对象与其状态是紧耦合的,本例利用state模式来组合对象与其状态,达到松耦合的目的

(2)重构的步骤

  ①使用SelfEncapsulate Field来封装类型码,确保任何时候都通过取值和设值函数访问类型代码。

  ②新建一个Price类,并在其中提供类型相关的函数。如Price类中加入一个纯虚函数getPriceCode,并在所有子类中加上对应的具体函数。

  ③为Price添加子类,每个子类对应一种类型码。并提供getPriceCode的具体实现。

  ④利用pushDownMethod/Field方法将与类型码相关的函数从Price类推到子类去实现。(如getCharge、getFrequentRenterPoints函数)

  ⑤在Movie类中保存一个Price的引用,用来保存新建的状态对象。同时调整Movie中各个与类型码相关的函数,将动作转发到状态对象上(如Movie的getCharge函数)。

(3)引入state模式的好处

  ①如果要修改任何与价格有关的行为只需修改相应的子类即可。添加新的定价标准也只需扩展新的Price子类即可。

  ②修改影片分类结构或是改变费用的计算规则改变常客积分计算规则都很容易。

【实例分析】影片出租1.3.3

 

//types.h

#include <vector>
#include <string>
#include <sstream>
using namespace std;

enum MovieType{
    REGULAR = 0, //普通片
    NEW_RELEASE, //新片
    CHILDRENS    //儿童片
};

//price类
class Price
{
public:
    virtual int getPriceCode() = 0;
    virtual double getCharge(int daysRented) = 0;
    virtual int getFrequentRenterPoints(int daysRented){return 1;}
    virtual ~Price(){}
};

//普通:
class RegularPrice: public Price
{
public:
    int getPriceCode();
    double getCharge(int daysRented);
};

//儿童片:
class ChildrensPrice: public Price
{
public:
    int getPriceCode();
    double getCharge(int daysRented);
};

//新片
class ReleasePrice: public Price
{
public:
    int getPriceCode();
    double getCharge(int daysRented);
    int getFrequentRenterPoints(int daysRented);
};

//以上是关于第1章 重构,第一个案例:运用多态取代switch的主要内容,如果未能解决你的问题,请参考以下文章

我的重构-初识

面向对象编程导论 An Introduction to Object-Oriented Programming

第2章 基础语法的练习

第3章 控制循环语句

第3章 控制循环语句

<<重构改善既有的代码设计;;第2章