由于原书是基于java,这里勇哥改为C#版本。
先引入第一章的开篇例子。
需求如下:
需求: 影片出租,计算每一位顾客的消费金额并打印详单。 操作得告诉程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型计算出费用。 影片分为三类:普通片、儿童片和新片。 除了计算费用,还要为常客计算积分,积分会根据影片种类是否新片而不同。 Movie 影片类 Rental 租赁类 Customer 顾客类
priceCode:int 表示影片的种类,普通片、儿童片和新片
daysRented:int 表示出租天数
statement() 结算清单方法
源码如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication1 { /* 代码需要重构的理由? 虽然编译器不区分代码的美丑,但是当我们打算修改代码的时候,就涉及到了人。 差劲的系统很难修改的原因是:很难找到修改点。 如果很难找到修改点,程序员就可能犯错,从而引入bug 这个程序的问题: 1. 如果希望以html格式输出消费单,则Statement()完全不支持,你得弄一个HtmlStatement() 这样只能复制粘贴Statement()进行修改 2. 如果计费标准发生变,则Statement()和HtmlStatement()都得一起修改。 这里复制粘贴带来的问题就浮现出来,因为要确保两处的一致性,以后这种修改会越来越复杂。 原则:如果你发现需要为程序添加一个特性,而代码结构使你无法很方便做到, 那就先重构那个程序,使特征的添加比较容易进行,然后再添加特性 */ public partial class Form1 : Form { Customer lxy = new Customer("xaioyong"); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { lxy.AddRental(new Rental(new Movie("星球大战1", Movie.NEW_RELEASE), 3)); lxy.AddRental(new Rental(new Movie("勇敢者的游戏2", Movie.NEW_RELEASE), 4)); RtbMsg.Text = lxy.Statement(); } } /// <summary> /// 影片类 /// </summary> public class Movie { /// <summary> /// 儿童片 /// </summary> public const int CHILDRENS = 2; /// <summary> /// 普通片 /// </summary> public const int REGULAR = 0; /// <summary> /// 新片 /// </summary> public const int NEW_RELEASE = 1; private string _title; private int _priceCode; public Movie(string title, int priceCode) { _title = title; _priceCode = priceCode; } public int GetPriceCode() { return _priceCode; } public void SetPriceCode(int arg) { _priceCode = arg; } public string GetTitle() { return _title; } } /// <summary> /// 租赁,表示某个顾客租了一部影片 /// </summary> public class Rental { private Movie _movie; private int _dayRented; public Rental(Movie movie, int days) { _movie = movie; _dayRented = days; } public int GetDaysRented() { return _dayRented; } public Movie GetMovie() { return _movie; } } /// <summary> /// 顾客类 /// </summary> public class Customer { private string _name; private List<Rental> _rentals = new List<Rental>(); public Customer(string name) { _name = name; } public void AddRental(Rental arg) { _rentals.Add(arg); } public string GetName() { return _name; } /// <summary> /// 生成顾客消费单 /// </summary> /// <returns></returns> public string Statement() { double totalAmount = 0; int fPoints = 0; var result = new StringBuilder(); result.Append($"Rental Record for {GetName()} \n"); foreach(var m in _rentals) { double thisAmount = 0; switch(m.GetMovie().GetPriceCode()) { case Movie.REGULAR: thisAmount += 2; if (m.GetDaysRented() > 2) thisAmount += (m.GetDaysRented() - 2) * 1.5; break; case Movie.NEW_RELEASE: thisAmount += m.GetDaysRented() * 3; break; case Movie.CHILDRENS: thisAmount += 1.5; if (m.GetDaysRented() > 3) thisAmount += (m.GetDaysRented() - 3)*1.5; break; } fPoints++; if (m.GetMovie().GetPriceCode() == Movie.NEW_RELEASE && m.GetDaysRented() > 1) fPoints++; result.Append($"\t {m.GetMovie().GetTitle()}\t{thisAmount}\n"); totalAmount += thisAmount; } result.Append($"Amount owed is {totalAmount}\n"); result.Append($"You earned {fPoints} frequent renter points"); return result.ToString(); } } }
(一)拆分长函数,并把较小块的代码移至更适合的类。
这样做的目的是降低代码重复量。
首先我们对长函数Statement() 那个switch动手。
public string Statement() { double totalAmount = 0; int fPoints = 0; var result = new StringBuilder(); result.Append($"Rental Record for {GetName()} \n"); foreach (var m in _rentals) { double thisAmount = amountFor(m); //这里switch变成了函数调用。 fPoints++; if (m.GetMovie().GetPriceCode() == Movie.NEW_RELEASE && m.GetDaysRented() > 1) fPoints++; result.Append($"\t {m.GetMovie().GetTitle()}\t{thisAmount}\n"); totalAmount += thisAmount; } result.Append($"Amount owed is {totalAmount}\n"); result.Append($"You earned {fPoints} frequent renter points"); return result.ToString(); } //函数变量还进行了重命名,这是值得的。 //任何一下傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码, //才是优秀的程序员 private double amountFor(Rental aRental) { double result = 0; switch (aRental.GetMovie().GetPriceCode()) { case Movie.REGULAR: result += 2; if (aRental.GetDaysRented() > 2) result += (aRental.GetDaysRented() - 2) * 1.5; break; case Movie.NEW_RELEASE: result += aRental.GetDaysRented() * 3; break; case Movie.CHILDRENS: result += 1.5; if (aRental.GetDaysRented() > 3) result += (aRental.GetDaysRented() - 3) * 1.5; break; } return result; }
(二)函数功能放错位置
注意上面的amountFor() 函数,它没有用到顾客类Customer的信息,只是使用了Rental类的信息。
因此它放错了位置。
绝大多数情况下,函数应该放在它所用到数据的所属对象内。
//此函数形参不变,内容变一下。 private double amountFor(Rental aRental) { return aRental.GetCharge(); } //下面函数放到Rental类中去。 public double GetCharge() { double result = 0; switch (GetMovie().GetPriceCode()) { case Movie.REGULAR: result += 2; if (GetDaysRented() > 2) result += (GetDaysRented() - 2) * 1.5; break; case Movie.NEW_RELEASE: result += GetDaysRented() * 3; break; case Movie.CHILDRENS: result += 1.5; if (GetDaysRented() > 3) result += (GetDaysRented() - 3) * 1.5; break; } return result; }
(三)更新一下新函数的调用方式
一般来讲,尽量去除临时变量,它会导致大量参数被传来传去。
为了这个目的,下面调用了2次m.GetCharge(),浪费了一点性能。
其它对于去掉临时变量这一点,勇哥有一点不一样的观点。
有时候,临时变量的存在,是为了方便调试代码。因为你可以鼠标悬停在临时变量上面查阅结果,但是你无法这样查看函数的返回结果。
所以勇哥觉得,如果是简单变量,且需要返回结果需要被关注时,还是可以用临时变量。
public string Statement() { double totalAmount = 0; int fPoints = 0; var result = new StringBuilder(); result.Append($"Rental Record for {GetName()} \n"); foreach (var m in _rentals) { fPoints++; if (m.GetMovie().GetPriceCode() == Movie.NEW_RELEASE && m.GetDaysRented() > 1) fPoints++; result.Append($"\t {m.GetMovie().GetTitle()}\t{m.GetCharge()}\n"); totalAmount += m.GetCharge(); } result.Append($"Amount owed is {totalAmount}\n"); result.Append($"You earned {fPoints} frequent renter points"); return result.ToString(); }
(四)提炼“常客积分计算”的代码。
常客积分就是那个fPoints变量。
此积分计算的责任应该放在Rental类上面。
下面计算常客积分的函数放在Rental类中去。
public int GetfPoints() { if (GetMovie().GetPriceCode() == Movie.NEW_RELEASE && GetDaysRented() > 1) return 2; else return 1; }
改一下Statement函数的调用。
public string Statement() { double totalAmount = 0; int fPoints = 0; var result = new StringBuilder(); result.Append($"Rental Record for {GetName()} \n"); foreach (var m in _rentals) { fPoints += m.GetfPoints(); //fPoints++; //if (m.GetMovie().GetPriceCode() == Movie.NEW_RELEASE && // m.GetDaysRented() > 1) // fPoints++; result.Append($"\t {m.GetMovie().GetTitle()}\t{m.GetCharge()}\n"); totalAmount += m.GetCharge(); } result.Append($"Amount owed is {totalAmount}\n"); result.Append($"You earned {fPoints} frequent renter points"); return result.ToString(); }
(五)

