~写?/span>4个测试用例全部通过了,但是如果惛_q代器中每一个元素的计算l果q行验证现有的函数就无法完成要求了?/span>
下面q用重构中的抽取函数分解CustomercM长的方?/span>statement(),q个函数完成的功能有些无所不包了。既计算各个U借的费用和常客积点,又要计算客户需要交U的费用dQ最后还要输出报表的表头和表。另外,现有的快速设计对变化的应对不I客户的电影分cd能会出现变化Q如果去掉儿童片加上U片、故事片怎么办,客户的收费规则发生变化了怎么办。根据前面所_他的味道不是一般的臭?/span>
W一步:抽取函数
我们先把statement计算各个U借的费用的职责分d一个单独的函数中?/span>
private double amountFor(Rental rental){
double thisAmount=0.0;
switch (rental.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount+=2;
if(rental.getDayRented()>2)
thisAmount+=(rental.getDayRented()-2)*1.5;
break;
case Movie.CHILDRENS:
thisAmount+=1.5;
if(rental.getDayRented()>3)
thisAmount+=(rental.getDayRented()-3)*1.5;
break;
case Movie.NEW_RELEASE:
thisAmount+=(rental.getDayRented())*3;
break;
}
return thisAmount;
}
把原来的计算各个U借费用的部分注释hQ把private double thisAmount=0.0; 一句改?/span>double thisAmount=amountFor(each);再次q行试用例Q和W一ơ的l果Ҏ
我们通过人工比对输出l果Q输出结果相同。我们稍后会通过试用例由套件自动比寏V?/span>
如果q一步我们的试fail掉了Q由于我们只C很小的一步,因此我们可以L的退回去。记住,我们始终保持试-〉编码(重构Q?/span>-〉测试的步骤Q小步快走的E_节奏?/span>
试全部通过后,我们可以果断的删除刚才留?/span>statement函数中我们注释过的时代码?/span>
分析我们刚刚抽取出来?/span>amountFor函数Q它只用了rentalcM的成员变量,攑֜Customer
cMq不合适,因此我们l箋重构Q把amountfor函数Ud?/span>RentalcM?/span>
q里我们使用了集成开发环境中的功能:
q里我们看到eclipse自动侦测出目标的cMؓrentalQ我们把新的函数改名?/span>getchargeQƈN在原类型中创徏委托Q可以点击预览按钮察看重构结果,最后的l果如下Q?/span>
Rental cMQ?/span>
double getCharge(){
double thisAmount=0.0;
switch (getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount+=2;
if(getDayRented()>2)
thisAmount+=(getDayRented()-2)*1.5;
break;
case Movie.CHILDRENS:
thisAmount+=1.5;
if(getDayRented()>3)
thisAmount+=(getDayRented()-3)*1.5;
break;
case Movie.NEW_RELEASE:
thisAmount+=(getDayRented())*3;
break;
}
return thisAmount;
}
可以看到?/span>amountfor函数更加化?/span>
CustomercMQ?/span>
private double amountFor(Rental rental){
return rental.getCharge();
}
保留了一?/span>rentalCZ的委托调用?/span>
再次q行试用例Q通过?/span>
我们l箋前进Q?/span>
我们回到CustomercMQ察?/span>Statement函数中的thisAmount变量Q我们发C的结果ƈ没有发生变化Q我们可以通过重构把这个无用的临时变量除去。直接把thisamount=amountFor(each);的语句右侧考到totalAmount+=thisAmount;语句右侧Q编译器会提C?/span>thisamount变量q未使用Q直接双?/span>quick fix 除去q个变量。再ơ运行测试用例,通过?/span>
下一步我们把计算常客U点的职责也?/span>statement函数中分d来:
我们d函数
private int getFrequentCount(Rental rental){
if (rental.getMovie().getPriceCode()==Movie.NEW_RELEASE&&rental.getDayRented()>1)
return 2;
else return 1;
}
再把计算常客U点的部分改写如下:
frequentCount+=getFrequentCount(each);
试后我们发现常客积点的计算和类Rental的关pL密切Q我们再ơ移?/span>getFrequentCount?/span>rentalcM。测试通过后,计算常客U点的部分改写如下:
frequentCount+= each.getFrequentCount();
接下来,我们?/span>statement最后的两个临时变量frequentCount?/span>totalAmount也通过替换为函数查询消除掉?/span>
d函数
private double getTotalCharge(){
double result=0.0;
Enumeration rental=rentals.elements();
while (rental.hasMoreElements()) {
Rental rent = (Rental) rental.nextElement();
result+=rent.getCharge();
}
return result;
}
把时变?/span>totalAmount替换?/span>getTotalCharge()
d函数
private int getTotalFrequentCount(){
int result=0;
Enumeration rental=rentals.elements();
while (rental.hasMoreElements()) {
Rental each = (Rental) rental.nextElement();
result+=each.getFrequentCount();
}
return result;
}
把时变?/span>frequentCount替换?/span>getTotalFrequentCount
相应的,我们在测试中d以下四条试用例Q?/span>
public void testGetCharge(){
Enumeration rentals=tom.getRentals().elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
switch(each.getMovie().getPriceCode()){
case Movie.CHILDRENS:
assertEquals(1.5,each.getCharge(),.1);
break;
case Movie.NEW_RELEASE:
assertEquals(15,each.getCharge(),.1);
break;
case Movie.REGULAR:
assertEquals(6.5,each.getCharge(),.1);
break;
}
}
}
public void testgetFrequentCount(){
Enumeration rentals=tom.getRentals().elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
switch(each.getMovie().getPriceCode()){
case Movie.CHILDRENS:
assertEquals(1,each.getFrequentCount(),.1);
break;
case Movie.NEW_RELEASE:
assertEquals(2,each.getFrequentCount(),.1);
break;
case Movie.REGULAR:
assertEquals(1,each.getFrequentCount(),.1);
break;
}
}
}
public void testGetTotalCharge(){
assertEquals(23,tom.getTotalCharge(),.1);
}
public void testGetTotalFrequentCount(){
assertEquals(4,tom.getTotalFrequentCount(),.1);
}
现在我们可以对q代器中的每个元素的l果q行验证了?/span>
下面通过一个简单的试驱动CZQƈl过重构完成设计的更攏V详l的例子见重构?/span>
q出租店的E序Q计每一位顾客的消费金额q打印报表。操作者告诉程序:֮U用了哪些媄片,U期多长Q程序根据租赁时间和qcdQ普通片Q儿童片和新片)。除了计费用,q要为常客计点敎ͼҎ的计会׃U片U类是否为新片而有所不同?/span>
Ҏ上述描述Q我们画出简单的cd
其中q和租借都是简单的U数据类?br />
package chapter01;
public class Movie {
public static final int CHILDRENS=2;// qcd
public static final int REGULAR=1;
public static final int NEW_RELEASE=0;
private String title;// 名称
private int priceCode;// h代码
public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
}
public class Rental {
private Movie movie;
private int dayRented;
public Rental(Movie movie, int dayRented) {
this.movie = movie;
this.dayRented = dayRented;
}
public int getDayRented() {
return dayRented;
}
public Movie getMovie() {
return movie;
}
}
public class Customer {
private String name;
private Vector rentals=new Vector();
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Vector getRentals() {
return rentals;
}
@SuppressWarnings("unchecked")
public void addRental(Rental rental){
rentals.add(rental);
}
public String statement(){// 计算费用
double totalAmount=0;// d
int frequentCount=0;// 常客U点
Enumeration rental=rentals.elements();
String result="rental record for "+getName()+"\n";
while (rental.hasMoreElements()) {
Rental each = (Rental) rental.nextElement();// 取得一批租借记?/span>
double thisAmount=0;
// ҎcdQ计h?/span>
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount+=2;
if(each.getDayRented()>2)
thisAmount+=(each.getDayRented()-2)*1.5;
break;
case Movie.CHILDRENS:
thisAmount+=1.5;
if(each.getDayRented()>3)
thisAmount+=(each.getDayRented()-3)*1.5;
break;
case Movie.NEW_RELEASE:
thisAmount+=(each.getDayRented())*3;
break;
}
// d常客U点
frequentCount++;
if (each.getMovie().getPriceCode()==Movie.NEW_RELEASE&&each.getDayRented()>1)
frequentCount++;
// 昄此笔数据
result+=
"\t"+each.getMovie().getTitle()+
"\t"+String.valueOf(frequentCount)+"\n";
totalAmount+=thisAmount;
}
// 打印l尾
result+="amount owned "+String.valueOf(totalAmount)+"\n";
result+="you earned "+String.valueOf(frequentCount)+"frequentCount\n";
return result;
}
}
下面是对应的试用例
public class testCustomerStatement extends TestCase {
Movie childrenMovie=new Movie("HARRY POTTY",Movie.CHILDRENS);
Movie regularMovie=new Movie("Titanic",Movie.REGULAR);
Movie newMovie=new Movie("Ice Age 2",Movie.NEW_RELEASE);
Rental childRental=new Rental(childrenMovie,3);
Rental newRental=new Rental(newMovie,5);
Rental regRental=new Rental(regularMovie,5);
Customer tom=new Customer("Tom");
protected void setUp() throws Exception {
super.setUp();
tom.addRental(childRental);
tom.addRental(newRental);
tom.addRental(regRental);
}
public void testCaustomerName(){
assertEquals("Tom",tom.getName());
}
public void testIteratorSize(){
assertEquals(3,tom.getRentals().size());
}
public void testIteratorName(){
Enumeration rentals=tom.getRentals().elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
switch(each.getMovie().getPriceCode()){
case Movie.CHILDRENS:
System.out.println("childrens");
assertEquals("HARRY POTTY",each.getMovie().getTitle());
assertEquals(3,each.getDayRented());
break;
case Movie.NEW_RELEASE:
System.out.println("new");
assertEquals("Ice Age 2",each.getMovie().getTitle());
assertEquals(5,each.getDayRented());
break;
case Movie.REGULAR:
System.out.println("regular");
assertEquals("Titanic",each.getMovie().getTitle());
assertEquals(5,each.getDayRented());
break;
}
}
}
public void testOutput(){
System.out.println(tom.statement());
}
}
软g开发的复杂?/strong> Q?/span>
计算机硬件界的摩定律(每隔 18 个月计算机硬件的q算速度提高一倍,h下降一半)
适用于硬件的发展规律已经过三十q了。h们想当然的认机软g的发展速度和硬件的发展速度相当Q但是不q的是:每次重大的硬件升U之后,随着更大功能更丰富的软g的出玎ͼg的潜能再一ơ被无情的榨取殆。许多开发的软gpȝ不断的遭受进度g期,人员资金和时间等预算无休止的增加QY件质量的不断反复Q开发出来的pȝ对客L新需求响应缓慢,更改困难的噩梦?/span>
q样的现实是pY件的固有复杂性造成的,软g不同于硬件的生q程Q是׃h的智力劳动完成h的需求到机器E序的翻译{换过E。需求可能不清晰Q对需求可能出C人理解上的差异,选择实现Ҏ的差异,需求的不断变化Q具体实现语aq_的差异,软g生中采用的q程Q具体实Ch员的变动{等都会Ҏl的产品产生影响。想象一下,如果一U变化的因素只有两种可能Q那么可以用简单的 0 Q?/span> 1 表示Q只?/span> 10 个变化因素的l合已l达C 2BB10=1024 U可能性,而实际开发中变化的因素轻易就过 10 个以上,每个变化的可能是q不止两个,因此软g的复杂性很快就会超Zh的理解程度。有一句经典的软g开发名aQ?b style="mso-bidi-font-weight: normal">世界上唯一不变的是变化本n。不断出现的变化Q会使初始的设计和最l的需求之间的距离来远?/span>
软g开发,使用Q维护中出现了以下的“臭味”:
僵化性: rigidity 很难对系l进行改动,因ؓ每个改动都会q许多对系l其他部分的其他改动。即使是单的改动Q也会迫使导致右依赖关系的模块的q锁改动?/span>
脆弱性: fragility 对系l的改动会导致系l中和改动的地方在概念上无关的许多地方出现问题。出现新问题的地方和改动的地Ҏ有概念上的关联,难以排错Q排错的q程中又会引入更多的“臭虫”?/span>
牢固?/span> immobility 很难解开pȝ的纠l,使它成ؓ其他pȝ中重用的lg。系l中包含了对其他pȝ中有用的功能Q当其他人想复用q个功能到新的系l时Q剥d独立的组件的隑ֺq远大于重新实现的难度,在时间和q度的压力下Q大多数人只有选择拯涂鸦的方式来实现新系l的功能?/span>
_滞性: viscosity 做正的事情比错误的事情要困难。程序完成正常的功能L們于得C正确的结果?/span>
不必要的复杂性: needless complexity 设计中包含有不具有Q何直接好处的基础l构。ؓ了预防后期维护更攚w求的Ҏ码的修改Q在设计之初攄了那些处理潜在变化的代码来保持Y件的灉|性,q样的结果是软g中包含了很多复杂的结构,理解h更加困难?/span>
不必要的重复Q?/span> needless repetition 设计中包含有重复的结构,而该重复的结构可以用单一的抽象进行统一。对鼠标右键Q剪切,复制Q粘_的滥用,使得完成同一或类似的代码片断出现在系l各处。如果原始的代码D完成的功能需要变化,或者存在错误,排错和增加新的功能变得非常困难?/span>
晦ӆ性: opacity 很难阅读Q理解。没有很好的表现出意图?/span>
以上讨论了系l构架的臭味Q下面讨论微观层ơ上代码的臭呻I
重复代码Q重复的代码使得更改功能和排错更加困难。同L模块错误会在拯_脓的程序各处多ơ出现?/span>
q长的函敎ͼE序长难于理解,q已l是软g业开发的常识。越隄解的E序Q用维护的成本p大。如果一个函数的行数过一,很少有h能够在看C一늚时候还清楚的记得函数开头的变量定义Q理解和查错更加困难?/span>
q大c:在一个类中完成几乎所有需要的功能。十全能的人是不存在的QY件也一栗?/span>
q长的参数列Q如果一个函敎ͼҎQ的调用参数q长Q用这个函数的调用q程也一定是困难的。想象一下,调用一个十个以上参数存储过E会有多么痛苦。这q只是开始,如果M个参数的定义Q名UͼcdQ发生轻微的变化Q函数的调用客户端会有多么大的改动?/span>
其他的臭呌有发散式变化Q散Ҏ修改Q依恋情l,数据泥团Q基本型别偏执,复杂?/span> switch 分支语句Q^行的l承体系Q冗赘类Q夸夸其谈的未来性,令hqh的暂时值域Q过度耦合的消息链Q中间{手hQ狎昵关p,异曲同工的类Q不完美的程序库c,U数据类Q数据哑元)Q子cM需要父cȝ某些Ҏ,q多注释。详l的讨论可以参见《重构》的介绍?/span>
面向对象软g设计的原?/strong> Q?/span>
M原则 Q?/span>
1Q?span style="FONT: 7pt 'Times New Roman'"> 针对于接口(抽象Q编E,而不要针对于实现Q具体)~程?/span>
举例来说Q操作系l是寚w辑计算机的抽象Q通过操作pȝ的抽象我们不需要考虑具体使用的硬仉|,可以在较高的层次上进行更高生产力的应用。再如:汇编语言Ҏ器的 0 Q?/span> 1 代码q行了抽象,大大加快了开发效率,后来使用的高U语a和第四代语言模型驱动抽象的别更高,生力也更高。再如: java ?/span> .net 实现于一个抽象的软g虚拟机,q一步开发出来的lg可以跨^台和操作pȝ。通过抽象出数据访问层Q持久化层)Q可以业务逻辑和具体的数据库访问代码分,更换数据库提供商对已有的lg没有影响。具体实现可以参?/span> hibernate 实现?/span> dao Q数据访问对象)模式?/span>
优势Q?/span> 1 Q降低程序各个部分之间的耦合性,使程序模块互换成为可能。调用的客户端无需知道具体使用的对象类型,只要对象有客户希望的接口可以用,对象具体是如何实现这些接口的Q客户ƈ不需要考虑?/span> 2 Q简化了E序各个部分的单元测试,需要测试的E序模块中通过重构提炼出抽象的接口Q然后编制和接口一致的 Mock c,试׃变得很容易。如果应用了试优先的方法,从简化客L调用的角度,q可以经q抽象改善Y件模块的设计?/span> 3 Q模块的部v升׃模块之间的耦合度降低变得更加容易?/span>
相关的设计模式有创徏型模式中的工厂模式,l构型模式中的代理模式和l合模式{?/span>
2Q?span style="FONT: 7pt 'Times New Roman'"> 对象l合优于cȝѝ?/span>
面向对象Y件开发引入了三大工具Q承,多态和重蝲。承得程序员可以快速的通过扩展子类来增加功能,但是׃l承是在~译时确定的Q因此增加的功能较多Ӟl承不够灉|Q还有可能出现“子cȝ炸”的局面(Z完成新添功能Q不得不在承体pMd大量的之间只有细微差别的子类Q掌握用扩展都会变得非常困难)。而通过对象的组合,可以动态透明的添加功能。相兌计模式有装饰模式和代理模式等?/span>
3Q?span style="FONT: 7pt 'Times New Roman'"> 分离变化。前面说q需求是在不断变化的Q不同的变化可能是仓库安全库存的计算ҎQ可能是报表和数据的展现形式Q通过把这些不同的变化识别q分d来不同的对象委托Q简化了客户端的调用和升U。相关的设计模式有命令模式,观察者模式,{略模式Q状态模式,模版Ҏ模式{?/span>
1Q?span style="FONT: 7pt 'Times New Roman'"> 单一职责原则 srp Q?/span> single responsibility principle Q:一个模块的功能应该可能的内聚。如果一个类发生了变化,引v变化的原因应该有且只有一个。每一个类承担的职责都是一个变化的轴线Q需求变化时Q会体现为类的职责的变化。如果一个类承担的职责过多,q于把q些职责耦合在了一P一个职责的变化会媄响这个类完成其他职责的能力,会出现前面所说的软g的臭味之一脆弱性。相关的设计模式?/span>
2Q?span style="FONT: 7pt 'Times New Roman'"> 开攑ְ闭原?/span> ocp Q?/span> open closed principle Q:一个模块应该对功能的扩展开放,支持新的行ؓQ对自n的更改封闭。每ơ对模块的修攚w可能会引入新的错误和新的依赖。因此扩展新功能Ӟ已经~好的模块源码和二进制代码都是不应该修改的。相关的设计模式有适配器模式,桥接模式Q访问者模式等?/span>
3Q?span style="FONT: 7pt 'Times New Roman'">
Liskov
替换原则
lsp
Q?/span>
liskov subtitle principle
Q子cd必须可以替换掉他的基cd。一个基cȝ多个子类型之间要完成动态的替换Q各个子cd必须都可以被他们的基cd替换Q这样他们之间动态替换后Q客L调用的代码就不需要冗赘的
switch
cd判断代码。如?/span>
子类型无法替换基cdQ将会导致在zcd象作为基cd象进行传值时的错误。这样多态机制处于瘫痪状态了。相兌计模式ؓl合模式?/span>
4Q?span style="FONT: 7pt 'Times New Roman'"> 依赖倒置原则 dip Q?/span> dependent inverse principle Q高层模块不应该依赖于底层模块,抽象不应该依赖于l节Q细节应该依赖于抽象。假定所有的具体c都是回变化的,因此如果一个客L依赖于(调用或声明)具体的类型,那么当这个具体的cd变化Ӟ依赖的客L业必d时进行修攏V这些具体的更改可能出现在用了某个特定的网l协议,Ҏ的系l?/span> api 调用Q特定的数据库储存过E等Q这些用法或多或都会客户端调用和具体cd成ؓ铁板一块,比较难于重用?/span> Java C中比较热门的 j2ee 轻量U容器框?/span> spring 很好的实现了本原则?/span>
5 Q接口隔d?/span> isp Q?/span> interface segregation principle Q不应该客户依赖于它们不使用的方法。因为每一个实现接口的对象必须实现所有接口中定义的方法。如果接口的_度比较,实现接口的对象可以用一U即用即付的方式动态实现接口。每个接口的_度很小Q复用v来也非常Ҏ?/span>
q体C一个趋势:Z更好的实现重用,接口Q函敎ͼ模块和类{們于更Ҏ使用的“小”体U?/span>
软g开发项目的p|使得Z开始思考Y件开发的q程Qh们希望通过引入严格的过E控制生Y件生命周期中各个阶段的文和制品来保证Y件的质量。比较出名的业界实施Ҏ论有 cmmi Q能力成熟度模型Q和 rup Q瑞理统一q程Q,q些Ҏ论都是重型的。D例来_没有l过剪裁?/span> Rup 实现hQ至需要在全周期完成四十个以上的制品文,文的编写维护和源代码的同步{需要非常多的资源,十h以下的开发团队一般没有精力、时间、h员配|完成这些制品。失控的q程的膨胀qZL一U快速工作,相应变化的“敏L”方法。敏捷团队提倡通过团队成员之间充分有效的沟?/b>l一大家的目标,l伴的方式完成开发技术的团队内传承,使用?b style="mso-bidi-font-weight: normal">够用好”的轻量甚至免费的工L理过E。可以正常工作的代码摆在首要CQ只有必要的时候才生必要的文。强调和客户面对?/b>C合作,U极?b style="mso-bidi-font-weight: normal">响应客户需求的变化而不是遵循机械的计划。?b style="mso-bidi-font-weight: normal">较短的P代周?/b>Q近早和持箋提交有h值的软gl客h验证q修正和用户需求的dE度。提倡可以持l的E_的开发节奏,长期?b style="mso-bidi-font-weight: normal">步快走”的方式代替H然的“百c_刺”。保持设计最?/b>Q?b style="mso-bidi-font-weight: normal">最?/b>的设计ƈ且持l改q,不断调整?/span>