Our program has come a long way since our last article, refactoring, first case study (C++), broke down and reorganized the refactoring in Statement(). But we can’t stop refactoring. Because, pretty soon, customers start muttering about new needs. Let’s see what the customer wants this time.
Look out, the client is going to make a new request!
The client wants to change the classification rules for movies. But they haven’t figured out how to change it themselves. New classification methods may be introduced and the original classification methods may be changed. . In short, they have new ideas, but new ideas are still incubating, and we need to do something to deal with the changes that are coming. As the new classification rules have not been decided, so the corresponding cost calculation and integral calculation method are uncertain. But one thing is for sure, as soon as there is a new category, we will have to add new rules for fees and credits. So if we go into these two calculation functions, it is easy to see that the main one is conditional judgment logic (including switch and if… else… Statement). Obviously, whenever there is a new category, we need to add new conditions. Good, we smell “bad code”. We need to refactor the conditional logic part of the code for price to accommodate future changes.
Let’s go home.
It is best not to use switch statements based on properties of another object. If you have to use it, use it on the object’s own data, not on someone else’s data.
This in our application means that GetCharge and GetFrequentRenterPoints should not be in the Rental class, but in the Movie class. From the above demand analysis, we have made clear that the main possible change point of the program lies in the emergence of new classification rules for films, which may require modification of the calculation of fees and credits. By placing the corresponding calculation function in the Movie class, you can control the change in the Movie class. The code after the move is as follows.
class Movie
{.double GetCharge(int days_rented) {
double result = 0;
switch (GetPriceCode()) {
case PriceCode::REGULAR:
result += 2;
if (days_rented > 2)
result += (days_rented - 2) * 1.5;
break;
case PriceCode::NEW_RELEASE:
result += days_rented * 3;
break;
case PriceCode::CHILDREN:
result += 1.5;
if (days_rented > 3)
result += (days_rented - 3) * 1.5;
break;
}
return result;
}
int GetFrequentRenterPoints(int days_rented) {
if (GetPriceCode() == PriceCode::NEW_RELEASE &&
days_rented > 1)
return 2;
else
return 1; }... };class Rental
{.double GetCharge(a) {
return movie_.GetCharge(days_rented_);
}
int GetFrequentRenterPoints(a) {
returnmovie_.GetFrequentRenterPoints(days_rented_); }... };Copy the code
After moving the calculation function again, our UML class diagram looks like this.
Finally… We come to inheritance
Now let’s start with switch statements, introducing inheritance and using a design pattern to eliminate switch statements. Design patterns may sound complicated to some of you, but some design patterns come naturally when you follow the code refactoring process. First, the type variable in our switch statement is an enumeration of movie types. To eliminate this enumeration value, we need to use inheritance to implement a subclass for each of the types in the original enumeration, which can implement its own calculation methods. Thus we replace the switch statement with polymorphism. The UML class diagram using the inherited movie type is shown below.
However, this design leaves us with a small problem: a film can change its classification over the course of its life. For example, a new film may not be a new film a few days later and fall into a different category. However, our newly written subclass object cannot modify its own class during its lifetime. This is where we need the design pattern — the State pattern. Using the State pattern, the UML class diagram for movie types looks like this.
We added an indirection layer, the Prince object, which we could subclass to change the price at any time. And we can do the same thing with integrals, but because integrals are easier, we’re actually going to simplify it.
Stay close as we implement the refactoring I mentioned above. First, the method we use is Replace Type Code With State/Strategy (Replace Type Code With State/Strategy).
Make sure that type code is accessed at all times through the value and set functions.
This is what the author wants us to do first, in the current program we use most of our access from outside the class using the value function, this doesn’t need to change. In the constructor of the Movie class, we use the variables of the type code directly. So let’s change it to a set function. (this change may be controversial in C++, but the authors recommend using Set and Get functions to access datatype member variables, both inside and outside the class, rather than directly using or modifying their values.)
class Movie
{. Movie(const string& title, intprice_code) { title_ = title; SetPriceCode(price_code); }... };Copy the code
From there, we can add the abstract class Price, add subclasses, and implement concrete functions in those subclasses. The new code is as follows.
class Price
{
public:
virtual ~Price() {}
virtual int GetPriceCode(a) { return - 1; }};class ChildrensPrice : public Price
{
public:
int GetPriceCode(a) {
returnPriceCode::CHILDREN; }};class NewReleasePrice : public Price
{
public:
int GetPriceCode(a) {
returnPriceCode::NEW_RELEASE; }};class RegularPrice : public Price
{
public:
int GetPriceCode(a) {
returnPriceCode::REGULAR; }};Copy the code
From there, we can compile and run (each small refactoring requires a test code run to verify, which I won’t emphasize later). Now we’re going to change price_code_ from the Movie class to the Price class we just defined. Then modify its value, set the value function. The modified code looks like this.
class Movie
{.int GetPriceCode(a) {
int result = - 1;
if (price_.get())
{
result = price_->GetPriceCode();
}
return result;
}
void SetPriceCode(int arg) {
switch (arg) {
case PriceCode::REGULAR:
price_.reset(new RegularPrice());
break;
case PriceCode::NEW_RELEASE:
price_.reset(new NewReleasePrice());
break;
case PriceCode::CHILDREN:
price_.reset(new ChildrensPrice());
break; }}...private:
string title_;
shared_ptr<Price> price_ = nullptr;
};
Copy the code
We are nearing the end of our refactoring, and with the changes above, our application is basically in the shape of State mode, so we need to refine it. First, move the GetCharge function from the Movie class to the Price class. Then, Replace Conditional WIth Polymorophism refactoring eliminates the switch statement and puts its execution logic into the overloaded functions of the corresponding subclass. In the same way, we can integrate the GetFrequentRenterPoints function. Also, because the integral calculation rules are relatively simple, except for the new movie, we simply override the integral calculation function in NewReleasePrice and implement the generic calculation function in the Price class as the default behavior. The code we last modified is as follows.
class Price
{.virtual double GetCharge(int days_rented) { return 0; }
virtual int GetFrequentRenterPoints(int days_rented) { return 1; }};class ChildrensPrice : public Price
{
...
double GetCharge(int days_rented) {
double result = 1.5;
if (days_rented > 3)
result += (days_rented - 3) * 1.5;
returnresult; }};class NewReleasePrice : public Price
{
...
double GetCharge(int days_rented) {
return days_rented * 3;
}
int GetFrequentRenterPoints(int days_rented) {
return (days_rented > 1)?2 : 1; }};class RegularPrice : public Price
{
...
double GetCharge(int days_rented) {
double result = 2;
if (days_rented > 2)
result += (days_rented - 2) * 1.5;
returnresult; }};class Movie
{.double GetCharge(int days_rented) {
double result = 0.0;
if (price_.get())
{
result = price_->GetCharge(days_rented);
}
return result;
}
int GetFrequentRenterPoints(int days_rented) {
int result = 1;
if (price_.get())
{
result = price_->GetFrequentRenterPoints(days_rented);
}
returnresult; }... };Copy the code
After this round of refactoring our UML class diagram and sequence diagram ended up like this.
We end up with a program that works the same way by introducing the State pattern. Is it worth it? Of course it is. Now our program is very easy to deal with whether the customer proposes to modify the film classification structure, or calculate the cost or calculate the rules of points. Here I quote directly from the author’s book summary.
The payoff: If I were to change any behavior related to price, add new pricing criteria, or add other behavior that depends on price, it would be much easier to change the program. The rest of the program doesn’t know I’m using State mode. With the small amount of behavior I currently have, it might not be economical to change any functionality or features, but in a more complex system with a dozen price-related functions, the ease of change can make a big difference.
Thinking summary
- Analyze the program structure to make the function of the class more reasonable.
- Analyze conditional statements in programs, using polymorphisms to replace conditional statements that may change frequently.
- Introducing design patterns makes applications more resilient to change.
Finally, I want to quote the author’s most important summary of this refactoring example.
The biggest takeaway from this example is the pace of refactoring: test, minor change, test, minor change, test, minor change… It is this rhythm that allows refactoring to proceed quickly and safely.
This series of articles
This series of articles
Find the bad smell in the code — Notes from Refactoring (Part 1)
Refactoring, first case (C++ version) – the original program
Refactoring, first case (C++) — breaking down and reorganizing Statement()
Refactoring, first case (C++) — using polymorphism to replace conditional logic with price