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 */
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); }; //影片類 class Movie { private: string title; //片名 Price* price; //價格類 public: Movie(string title, int priceCode); ~Movie(); string getTitle(); void setTitle(string value); int getPriceCode(); void setPriceCode(int priceCode); double getCharge(int daysRented); int getFrequentRenterPoints(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; //*******************************************價格類************************************** //普通片:用子類取代類型碼 int RegularPrice::getPriceCode() { return REGULAR; } //使用pushDownMethod(Field)方法將與特定類型碼相關的函數推到子類來實現 double RegularPrice::getCharge(int daysRented) { double result = 2 ; if(daysRented > 2) //超過2天的每天加1.5元 result +=(daysRented - 2 ) * 1.5; return result; } //兒童片 int ChildrensPrice::getPriceCode() { return CHILDRENS; } //使用pushDownMethod(Field)方法將與特定類型碼相關的函數推到子類來實現 double ChildrensPrice::getCharge(int daysRented) { double result = 1.5;//兒童片基本租金為1.5元 if(daysRented > 3) //超過3天的每天加1.5元 result +=(daysRented - 3 ) * 1.5; return result; } //新片 int ReleasePrice::getPriceCode() { return NEW_RELEASE; } //使用pushDownMethod(Field)方法將與特定類型碼相關的函數推到子類來實現 double ReleasePrice::getCharge(int daysRented) { return daysRented * 3; //新片的租金 } int ReleasePrice::getFrequentRenterPoints(int daysRented) { return (daysRented > 1) ? 2: 1; } //*********************************************影片類************************************* Movie::Movie(string title, int priceCode):price(NULL) { this->title = title; setPriceCode(priceCode); } string Movie::getTitle() { return title; } void Movie::setTitle(string value) { title = value; } int Movie::getPriceCode() { return price->getPriceCode(); } void Movie::setPriceCode(int priceCode) { if (price != NULL) delete price; switch(priceCode) { case REGULAR: price = new RegularPrice(); break; case CHILDRENS: price = new ChildrensPrice(); break; case NEW_RELEASE: price = new ReleasePrice(); break; } } double Movie::getCharge(int daysRented) { double ret = 0; if (price != NULL) ret = price->getCharge(daysRented); return ret; } int Movie::getFrequentRenterPoints(int daysRented) { return price->getFrequentRenterPoints(daysRented); } Movie::~Movie() { delete price; } //********************************租賃類(表示某個顧客租了一部影片)********************************** 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 = new Movie("倚天屠龍記", REGULAR); mv->setPriceCode(NEW_RELEASE); //重新改變影片的類型為NEW_RELEASE //這在上個例子是不可能的! Rental* rt = new Rental(*mv, 2); customer.addRental(rt); mv = new Movie("新水滸傳", NEW_RELEASE); rt = new Rental(*mv, 3); customer.addRental(rt); mv = new 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 倚天屠龍記 6 新水滸傳 9 喜羊羊與灰太狼 4.5 Amount owed is 19.5 You earned 5 */