最近在读《重构_改善既有代码的设计(第2版)》这本书,一本非常经典,并且非常容易读懂的书,强力推荐刚入职场或未入职场的同学去读,书中的代码示例是用JavaScript来编写的,该文只是将书中的代码示例翻译成Java版本,并不会复制书中过多的内容,其中一些做法并不完全相同,加了自己的风格或者是说一些浅显的理解,非常欢迎各位批评和指正。
示例需求:
设想有一个戏剧演出团,演员们经常要去各种场合表演戏剧。通常客户(customer)会指定几出剧目,而剧团则根据观众(audience)人数及剧目类型来向客户收费。该团目前出演两种戏剧:悲剧(tragedy)和喜剧(comedy)。给客户发出账单时,剧团还会根据到场观众的数量给出“观众量积分”(volumecredit)优惠,下次客户再请剧团表演时可以使用积分获得折扣——你可以把它看作一种提升客户忠诚度的方式。
书中给出的起始代码:
//该剧团将剧目的数据存储在一个简单的JSON文件中。plays.json...{ "hamlet": {"name": "Hamlet", "type": "tragedy"}, "as-like": {"name": "As You Like It", "type": "comedy"}, "othello": {"name": "Othello", "type": "tragedy"}}//他们开出的账单也存储在一个JSON文件里。invoices.json...[{ "customer": "BigCo", "performances": [ { "playID": "hamlet", "audience": 55 }, { "playID": "as-like", "audience": 35 }, { "playID": "othello", "audience": 40 }]}//下面这个简单的函数用于打印账单详情。function statement (invoice, plays) {let totalAmount = 0;let volumeCredits = 0;let result = `Statement for ${invoice.customer}\n`;const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format;for (let perf of invoice.performances) { const play = plays[perf.playID]; let thisAmount = 0; switch (play.type) { case "tragedy": thisAmount = 40000; if (perf.audience > 30) { thisAmount += 1000 * (perf.audience - 30); } break; case "comedy": thisAmount = 30000; if (perf.audience > 20) { thisAmount += 10000 + 500 * (perf.audience - 20); } thisAmount += 300 * perf.audience; break; default: throw new Error(`unknown type: ${play.type}`);}// add volume creditsvolumeCredits += Math.max(perf.audience - 30, 0);// add extra credit for every ten comedy attendeesif ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`; totalAmount += thisAmount;}result += `Amount owed is ${format(totalAmount/100)}\n`;result += `You earned ${volumeCredits} credits\n`;return result;}
Java编写:
public class Prototype { static Map<String, Plays> playsMap = new HashMap<>(); static Map<String, Object> invoicesMap = new HashMap<>(); static { //构造剧目数据 playsMap.put("hamlet", new Plays("tragedy", "Hamlet")); playsMap.put("as-like", new Plays("comedy", "As You Like It")); playsMap.put("othello", new Plays("tragedy", "Othello")); //构造账单数据 invoicesMap.put("customer", "BigCo"); List<Invoices> invoicesList = Arrays.asList( new Invoices("hamlet", 55), new Invoices("as-like", 35), new Invoices("othello", 40)); invoicesMap.put("performances", invoicesList); } public static void main(String[] args) { System.out.println(statement()); } private static String statement() { int totalAmount = 0; int volumeCredits = 0; String result = "Statement for " + invoicesMap.get("customer") + "\n"; for (Invoices perf : (List<Invoices>) invoicesMap.get("performances")) { Plays play = playsMap.get(perf.getPlayID()); int thisAmount = 0; switch (play.getType()) { case "tragedy": thisAmount = 40000; if (perf.getAudience() > 30) { thisAmount += 1000 * (perf.getAudience() - 30); } break; case "comedy": thisAmount = 30000; if (perf.getAudience() > 20) { thisAmount += 10000 + 500 * (perf.getAudience() - 20); } thisAmount += 300 * perf.getAudience(); break; default: throw new Error("unknown type"); } volumeCredits += Math.max(perf.getAudience() - 30, 0); if ("comedy".equals(play.getType())) volumeCredits += Math.floor(perf.getAudience() / 5); result += " " +play.getName() + ": " + thisAmount / 100 + "¥(" + perf.getAudience() + " seats)\n"; totalAmount += thisAmount; } result += "Amount owed is " + totalAmount / 100 + "¥ \n"; result += "You earned " + volumeCredits + " credits\n"; return result; }}
Plays.java(剧目实体类)
//剧目数据public class Plays { private String type; private String name; public Plays(String type, String name) { this.type = type; this.name = name; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Plays plays = (Plays) o; return Objects.equals(type, plays.type) && Objects.equals(name, plays.name); } @Override public int hashCode() { return Objects.hash(type, name); }}
Invoices.java(账单实体类)
public class Invoices { private String playID; private Integer audience; public Invoices(String playID, Integer audience) { this.playID = playID; this.audience = audience; } public String getPlayID() { return playID; } public void setPlayID(String playID) { this.playID = playID; } public Integer getAudience() { return audience; } public void setAudience(Integer audience) { this.audience = audience; }}
书中重构后的代码:
function statement (invoice, plays) { let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; } result += `Amount owed is ${usd(totalAmount())}\n`; result += `You earned ${totalVolumeCredits()} credits\n`; return result; function totalAmount() { let result = 0; for (let perf of invoice.performances) { result += amountFor(perf); } return result;}function totalVolumeCredits() { let result = 0; for (let perf of invoice.performances) { result += volumeCreditsFor(perf); } return result;}function usd(aNumber) { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format(aNumber/100);}function volumeCreditsFor(aPerformance) { let result = 0; result += Math.max(aPerformance.audience - 30, 0); if ("comedy" === playFor(aPerformance).type) result += Math.floor(aPerformance.audience /5); return result;}function playFor(aPerformance) { return plays[aPerformance.playID];}function amountFor(aPerformance) { let result = 0; switch (playFor(aPerformance).type) { case "tragedy": result = 40000; if (aPerformance.audience > 30) { result += 1000 * (aPerformance.audience - 30); } break; case "comedy": result = 30000; if (aPerformance.audience > 20) { result += 10000 + 500 * (aPerformance.audience - 20); } result += 300 * aPerformance.audience; break; default: throw new Error(`unknown type: ${playFor(aPerformance).type}`); } return result; }}
添加需求:
1.以HTML格式输出详单
2.表演类型上的变化
书中最终的代码:
statement.js
import createStatementData from './createStatementData.js';function statement (invoice, plays) { return renderPlainText(createStatementData(invoice, plays));}function renderPlainText(data, plays) { let result = `Statement for ${data.customer}\n`; for (let perf of data.performances) { result += ` ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`; } result += `Amount owed is ${usd(data.totalAmount)}\n`; result += `You earned ${data.totalVolumeCredits} credits\n`; return result;}function htmlStatement (invoice, plays) { return renderHtml(createStatementData(invoice, plays));}function renderHtml (data) { let result = `<h1>Statement for ${data.customer}</h1>\n`; result += "<table>\n"; result += "<tr><th>play</th><th>seats</th><th>cost</th></tr>"; for (let perf of data.performances) { result += ` <tr><td>${perf.play.name}</td><td>${perf.audience}</td>`; result += `<td>${usd(perf.amount)}</td></tr>\n`; } result += "</table>\n"; result += `<p>Amount owed is <em>${usd(data.totalAmount)}</em></p>\n`; result += `<p>You earned <em>${data.totalVolumeCredits}</em> credits</p>\n`; return result;}function usd(aNumber) { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format(aNumber/100);}
createStatementData.js
export default function createStatementData(invoice, plays) { const result = {}; result.customer = invoice.customer; result.performances = invoice.performances.map(enrichPerformance); result.totalAmount = totalAmount(result); result.totalVolumeCredits = totalVolumeCredits(result); return result;function enrichPerformance(aPerformance) { const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance)); const result = Object.assign({}, aPerformance); result.play = calculator.play; result.amount = calculator.amount; result.volumeCredits = calculator.volumeCredits; return result;}function playFor(aPerformance) { return plays[aPerformance.playID]}function totalAmount(data) { return data.performances .reduce((total, p) => total + p.amount, 0);}function totalVolumeCredits(data) { return data.performances .reduce((total, p) => total + p.volumeCredits, 0); }}function createPerformanceCalculator(aPerformance, aPlay) { switch(aPlay.type) { case "tragedy": return new TragedyCalculator(aPerformance, aPlay); case "comedy" : return new ComedyCalculator(aPerformance, aPlay); default: throw new Error(`unknown type: ${aPlay.type}`); }}class PerformanceCalculator { constructor(aPerformance, aPlay) { this.performance = aPerformance; this.play = aPlay; } get amount() { throw new Error('subclass responsibility'); } get volumeCredits() { return Math.max(this.performance.audience - 30, 0); }}class TragedyCalculator extends PerformanceCalculator { get amount() { let result = 40000; if (this.performance.audience > 30) { result += 1000 * (this.performance.audience - 30); } return result; }}class ComedyCalculator extends PerformanceCalculator { get amount() { let result = 30000; if (this.performance.audience > 20) { result += 10000 + 500 * (this.performance.audience - 20); } result += 300 * this.performance.audience; return result; } get volumeCredits() { return super.volumeCredits + Math.floor(this.performance.audience / 5); }}
Java编写最终的代码
Prototype.java
public class Prototype { static Map<String, Plays> playsMap = new HashMap<>(); static Map<String, Object> invoicesMap = new HashMap<>(); static { //构造剧目数据 playsMap.put("hamlet", new Plays("tragedy", "Hamlet")); playsMap.put("as-like", new Plays("comedy", "As You Like It")); playsMap.put("othello", new Plays("tragedy", "Othello")); //构造账单数据 invoicesMap.put("customer", "BigCo"); List<Invoices> invoicesList = Arrays.asList( new Invoices("hamlet", 55), new Invoices("as-like", 35), new Invoices("othello", 40)); invoicesMap.put("performances", invoicesList); } public static void main(String[] args) { System.out.println(statement()); System.out.println(htmlstatement()); } private static String htmlstatement() { ResultData data = createStatementData(); String result = "<h1>Statement for " + data.getCustomer() + "</h1>\n"; result += "<table>\n"; result += "<tr><th>play</th><th>seats</th><th>cost</th></tr>\n"; for (Performances performances : data.getPerformances()) { result += "<tr><td>" + performances.getPlays().getName() + "</td><td>" + performances.getPerf().getAudience() + "</td>"; result += "<td>" + performances.getAmount() / 100 + "¥</td></tr>\n"; } result += "</table>\n"; result += "<p>Amount owed is <em>" + data.getTotalAmount()/ 100 + "¥</em></p>\n"; result += "<p>You earned <em>" + data.getVolumeCredits() + "</em> credits</p>\n"; return result; } private static String statement() { ResultData data = createStatementData(); String result = "Statement for " + data.getCustomer() + "\n"; for (Performances performances : data.getPerformances()) { result += " " + performances.getPlays().getName() + ": " + performances.getAmount() / 100 + "¥(" + performances.getPerf().getAudience() + " seats)\n"; } result += "Amount owed is " + data.getTotalAmount() / 100 + "¥ \n"; result += "You earned " + data.getVolumeCredits() + " credits\n"; return result; } private static ResultData createStatementData() { ResultData result = new ResultData(); int totalAmount = 0; int volumeCredits = 0; List<Performances> performancesList = createPerformancesData(); for (Performances performances : performancesList) { totalAmount += performances.getAmount(); volumeCredits += performances.getCredits(); } result.setTotalAmount(totalAmount); result.setVolumeCredits(volumeCredits); result.setCustomer(invoicesMap.get("customer").toString()); result.setPerformances(performancesList); return result; } private static List<Performances> createPerformancesData() { List<Performances> performancesList = new ArrayList<>(); for (Invoices perf : (List<Invoices>) invoicesMap.get("performances")) { Calculate calculate = selectType(playsMap.get(perf.getPlayID()).getType()); performancesList.add(new Performances(playsMap.get(perf.getPlayID()), perf, calculate.calAmount(perf), calculate.calCredits(perf))); } return performancesList; } private static Calculate selectType(String type) { switch (type) { case "tragedy": return new CalTragedy(); case "comedy": return new CalComedy(); default: throw new Error("unknown type"); } }}
Calculate.java(计算抽象类)
public abstract class Calculate { public abstract int calAmount(Invoices perf); public int calCredits(Invoices perf) { return Math.max(perf.getAudience() - 30, 0); }}
CalTragedy.java(悲剧计算类)
public class CalTragedy extends Calculate { public int calAmount(Invoices perf) { int thisAmount = 40000; if (perf.getAudience() > 30) { thisAmount += 1000 * (perf.getAudience() - 30); } return thisAmount; }}
CalComedy.java(戏剧计算类)
public class CalComedy extends Calculate { public int calAmount(Invoices perf) { int thisAmount = 30000; if (perf.getAudience() > 20) { thisAmount += 10000 + 500 * (perf.getAudience() - 20); } thisAmount += 300 * perf.getAudience(); return thisAmount; } @Override public int calCredits(Invoices perf) { return super.calCredits(perf) + (int)Math.floor(perf.getAudience() /5); }}
ResultData.java(结果数据)
public class ResultData { private String customer; private Integer totalAmount; private Integer volumeCredits; private List<Performances> performances; public String getCustomer() { return customer; } public void setCustomer(String customer) { this.customer = customer; } public Integer getTotalAmount() { return totalAmount; } public void setTotalAmount(Integer totalAmount) { this.totalAmount = totalAmount; } public Integer getVolumeCredits() { return volumeCredits; } public void setVolumeCredits(Integer volumeCredits) { this.volumeCredits = volumeCredits; } public List<Performances> getPerformances() { return performances; } public void setPerformances(List<Performances> performances) { this.performances = performances; }}
Performances.java(演出数据实体类)
public class Performances { private Invoices perf; private Plays plays; private Integer amount; private Integer credits; public Performances( Plays plays,Invoices perf, Integer amount, Integer credits) { this.plays=plays; this.perf = perf; this.amount = amount; this.credits = credits; } public Plays getPlays() { return plays; } public void setPlays(Plays plays) { this.plays = plays; } public Invoices getPerf() { return perf; } public void setPerf(Invoices perf) { this.perf = perf; } public Integer getAmount() { return amount; } public void setAmount(Integer amount) { this.amount = amount; } public Integer getCredits() { return credits; } public void setCredits(Integer credits) { this.credits = credits; }}
本章描述的是书中第六章的内容,由于第二章到第五章都是较为理论性的东西,并未涉及到代码的内容,所以并未写上,在这只是大致介绍一下,书中这几章大致的内容。
第二章重构的原则,描述了什么叫重构,何时重构,为何重构,重构的起源和我们开发中的一些关系。
第三章代码的坏味道,详细的介绍了在怎样的常见场景下,需要进行重构,列举了24个需要相关的名词。
第四章构筑测试体系,向我们强调了重构时,构建一个完美的测试体系的重要性,并且如何去编写一个好的测试类。
第五章介绍重构名录,为之后介绍各类重构手法做一个铺垫,之后介绍其余的重构手法时,都按照这样的结构来介绍,名称、速写、动机、做法和范例。
有兴趣的可以去详细看书中的内容,这里就直接开始第六章的内容了。
个人理解:
将一些重复使用、之后改动可能较为频繁、方便将来可以快速的了解当前代码的代码片段进行提取,封装到一个函数中去。
书中范例:
function printOwing(invoice) { printBanner(); let outstanding = calculateOutstanding(); //print details console.log(`name: ${invoice.customer}`); console.log(`amount: ${outstanding}`);}
function printOwing(invoice) { printBanner(); let outstanding = calculateOutstanding(); printDetails(outstanding); function printDetails(outstanding) { console.log(`name: ${invoice.customer}`); console.log(`amount: ${outstanding}`); }}
做法:
请看下列函数:
function printOwing(invoice) { let outstanding = 0; console.log("***********************"); console.log("**** Customer Owes ****"); console.log("***********************"); // calculate outstanding for (const o of invoice.orders) { outstanding += o.amount; } //record due date const today = Clock.today; invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30); //print details console.log(`name: ${invoice.customer}`); console.log(`amount: ${outstanding}`); console.log(`due: ${invoice.dueDate.toLocaleDateString()}`); }
提炼完毕后:
function printOwing(invoice) { printBanner(); const outstanding = calculateOutstanding(invoice); recordDueDate(invoice); printDetails(invoice, outstanding); } function calculateOutstanding(invoice) { let result = 0; for (const o of invoice.orders) { result += o.amount; } return result; } function recordDueDate(invoice) { const today = Clock.today; invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30); } function printDetails(invoice, outstanding) { console.log(`name: ${invoice.customer}`); console.log(`amount: ${outstanding}`); console.log(`due: ${invoice.dueDate.toLocaleDateString()}`); } function printBanner() { console.log("***********************"); console.log("**** Customer Owes ****"); console.log("***********************"); }
本文使用Java翻译过来的代码:
public void printOwing(Invoice invoice) { int outstanding = 0; System.out.println("***********************"); System.out.println("**** Customer Owes ****"); System.out.println("***********************"); // calculate outstanding for (Order order : invoice.getOrders()) { outstanding += order.getAmount(); } // record due date LocalDate localDate = LocalDate.now(); invoice.setDueDate(LocalDate.of(localDate.getYear(), localDate.getMonth(), localDate.getDayOfMonth())); //print details System.out.println("name:" + invoice.getCustomer()); System.out.println("amount:"+outstanding); System.out.println("due:"+invoice.getDueDate().toString()); }
最终提炼后的代码:
public void printOwing(Invoice invoice) { printBanner(); // record due date recordDuedate(invoice); //print details printDetails(invoice); } public void printBanner(){ System.out.println("***********************"); System.out.println("**** Customer Owes ****"); System.out.println("***********************"); } public int calOutstanding(Invoice invoice){ int outstanding = 0; for (Order order : invoice.getOrders()) { outstanding += order.getAmount(); } return outstanding; } public void recordDuedate(Invoice invoice){ LocalDate localDate = LocalDate.now(); invoice.setDueDate(LocalDate.of(localDate.getYear(), localDate.getMonth(), localDate.getDayOfMonth())); } public void printDetails(Invoice invoice){ System.out.println("name:" + invoice.getCustomer()); System.out.println("amount:"+calOutstanding(invoice)); System.out.println("due:"+invoice.getDueDate().toString()); }
动机:
如果代码中有太多间接层,使得系统中的所有函数都似乎只是对另一个函数 的简单委托,造成我在这些委托动作之间晕头转向,那么我通常都会使用内联函 数。当然,间接层有其价值,但不是所有间接层都有价值。通过内联手法,我可 以找出那些有用的间接层,同时将无用的间接层去除。
范例:
function getRating(driver) { return moreThanFiveLateDeliveries(driver) ? 2 : 1; } function moreThanFiveLateDeliveries(driver) { return driver.numberOfLateDeliveries > 5; }
function getRating(driver) { return (driver.numberOfLateDeliveries > 5) ? 2 : 1;}
做法:
注:与提炼函数互为相反词,不做过多的演示
动机:
表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将 表达式分解为比较容易管理的形式。在面对一块复杂逻辑时,局部变量使我能给 其中的一部分命名,这样我就能更好地理解这部分逻辑是要干什么。
范例:
return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1, 100);
这段代码还算简单,不过我可以让它变得更容易理解。首先,我发现,底价 (base price)等于数量(quantity)乘以单价(item price)。
const basePrice = order.quantity * order.itemPrice; const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05; const shipping = Math.min(basePrice * 0.1, 100); return basePrice - quantityDiscount + shipping;
下面是同样的代码,但这次它位于一个类中:
class Order { constructor(aRecord) { this._data = aRecord; } get quantity() { return this._data.quantity; } get itemPrice() { return this._data.itemPrice; } get price() { return this.quantity * this.itemPrice - Math.max(0, this.quantity - 500) * this.itemPrice * 0.05 + Math.min(this.quantity * this.itemPrice * 0.1, 100); } }
我要提炼的还是同样的变量,但我意识到:这些变量名所代表的概念,适用 于整个Order类,而不仅仅是“计算价格”的上下文。既然如此,我更愿意将它们提 炼成方法,而不是变量。
class Order { constructor(aRecord) { this._data = aRecord; } get quantity() { return this._data.quantity; } get itemPrice() { return this._data.itemPrice; } get price() { return this.basePrice - this.quantityDiscount + this.shipping; } get basePrice() { return this.quantity * this.itemPrice; } get quantityDiscount() { return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05; } get shipping() { return Math.min(this.basePrice * 0.1, 100); } }
这是对象带来的一大好处:它们提供了合适的上下文,方便分享相关的逻辑 和数据。在如此简单的情况下,这方面的好处还不太明显;但在一个更大的类当 中,如果能找出可以共用的行为,赋予它独立的概念抽象,给它起一个好名字, 对于使用对象的人会很有帮助。
注:该示例过于简单,不做代码翻译
做法:
动机:
在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东 西。但有时候,这个名字并不比表达式本身更具表现力。还有些时候,变量可能 会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。
范例:
let basePrice = anOrder.basePrice;return (basePrice > 1000);
return anOrder.basePrice > 1000;
做法:
动机:
一个好名字能让我一眼 看出函数的用途,而不必查看其实现代码。有一个改进函数名字的好办法:先写一句注释描 述这个函数的用途,再把这句注释变成函数的名字。
范例:
function circum(radius) {...}
function circumference(radius) {...}
简单的做法:
function circum(radius) { return circumference(radius); } function circumference(radius) { return 2 * Math.PI * radius; }
此时我要执行测试,然后对旧函数使用内联函数(115):找出所有调用旧 函数的地方,将其改为调用新函数。每次修改之后都可以执行测试,这样我就可 以小步前进,每次修改一处调用者。所有调用者都修改完之后,我就可以删除旧 函数。
动机:
重构的作用就是调整程序中的元素。函数相对容易调整一些,因为函数只有 一种用法,就是调用。在改名或搬移函数的过程中,总是可以比较容易地保留旧 函数作为转发函数(即旧代码调用旧函数,旧函数再调用新函数)。这样的转发 函数通常不会存在太久,但的确能够简化重构过程。
数据就要麻烦得多,因为没办法设计这样的转发机制。如果我把数据搬走, 就必须同时修改所有引用该数据的代码,否则程序就不能运行。如果数据的可访 问范围很小,比如一个小函数内部的临时变量,那还不成问题。但如果可访问范 围变大,重构的难度就会随之增大,这也是说全局数据是大麻烦的原因。
封装数据的价值还不止于此。封装能提供一个清晰的观测点,可以由此监控 数据的变化和使用情况;我还可以轻松地添加数据被修改时的验证或后续逻辑。
范例:
下面这个全局变量中保存了一些有用的数据:let defaultOwner = {firstName: "Martin", lastName: "Fowler"};
使用它的代码平淡无奇:spaceship.owner = defaultOwner;
更新这段数据的代码是这样:defaultOwner = {firstName: "Rebecca", lastName: "Parsons"};
首先我要定义读取和写入这段数据的函数,给它做个基本的封装。
function getDefaultOwner() { return defaultOwner; } function setDefaultOwner(arg) { defaultOwner = arg; }
然后就开始处理使用defaultOwner的代码。每看见一处引用该数据的代码, 就将其改为调用取值函数。spaceship.owner = getDefaultOwner();
每看见一处给变量赋值的代码,就将其改为调用设值函数。setDefaultOwner({firstName: "Rebecca", lastName: "Parsons"});
每次替换之后,执行测试。
处理完所有使用该变量的代码之后,我就可以限制它的可见性。这一步的用 意有两个,一来是检查是否遗漏了变量的引用,二来可以保证以后的代码也不会 直接访问该变量。在JavaScript中,我可以把变量和访问函数搬移到单独一个文件 中,并且只导出访问函数,这样就限制了变量的可见性。
做法:
动机:
好的命名是整洁编程的核心。变量可以很好地解释一段程序在干什么——如 果变量名起得好的话。但我经常会把名字起错——有时是因为想得不够仔细,有时是因为我对问题的理解加深了,还有时是因为程序的用途随着用户的需求改变了。
范例:
let a = height * width; let area = height * width;
如果要改名的变量只作用于一个函数(临时变量或者参数),对其改名是最 简单的。这种情况太简单,根本不需要范例:找到变量的所有引用,修改过来就 行。完成修改之后,我会执行测试,确保没有破坏什么东西。 如果变量的作用域不止于单个函数,问题就会出现。代码库的各处可能有很 多地方使用它:
let tpHd = "untitled";
有些地方是在读取变量值:
result += `<h1>${tpHd}</h1>`;
另一些地方则更新它的值:tpHd = obj['articleTitle'];
对于这种情况,我通常的反应是运用封装变量(132):
result += `<h1>${title()}</h1>`; setTitle(obj['articleTitle']); function title() { return tpHd;}function setTitle(arg) { tpHd = arg;}
现在就可以给变量改名:
let _title = "untitled";function title() { return _title;} function setTitle(arg) { _title = arg;}
给常量改名:
如果我想改名的是一个常量(或者在客户端看来就像是常量的元素),我可以复制这个常量,这样既不需要封装,又可以逐步完成改名。假如原来的变量声明是这样:
const cpyNm = "Acme Gooseberries";
改名的第一步是复制这个常量:
const companyName = "Acme Gooseberries"; const cpyNm = companyName;
有了这个副本,我就可以逐一修改引用旧常量的代码,使其引用新的常量。 全部修改完成后,我会删掉旧的常量。我喜欢先声明新的常量名,然后把新常量 复制给旧的名字。这样最后删除旧名字时会稍微容易一点,如果测试失败,再把 旧常量放回来也稍微容易一点。
做法:
范例:
function amountInvoiced(startDate, endDate) {...} function amountReceived(startDate, endDate) {...} function amountOverdue(startDate, endDate) {...}
function amountInvoiced(aDateRange) {...} function amountReceived(aDateRange) {...} function amountOverdue(aDateRange) {...}
动机:
我常会看见,一组数据项总是结伴同行,出没于一个又一个函数。这样一组 数据就是所谓的数据泥团,我喜欢代之以一个数据结构。
将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得明晰。使用新的数据结构,参数的参数列表也能缩短。并且经过重构之后,所有使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一 致性。
但这项重构真正的意义在于,它会催生代码中更深层次的改变。一旦识别出 新的数据结构,我就可以重组程序的行为来使用这些结构。我会创建出函数来捕 捉围绕这些数据的共用行为——可能只是一组共用的函数,也可能用一个类把数 据结构与使用数据的函数组合起来。这个过程会改变代码的概念图景,将这些数 据结构提升为新的抽象概念,可以帮助我更好地理解问题域。果真如此,这个重 构过程会产生惊人强大的效用——但如果不用引入参数对象开启这个过程,后面 的一切都不会发生。
做法:
详细示例:
下面要展示的代码会查看一组温度读数(reading),检查是否有任何一条读数超出了指定的运作温度范围(range)。温度读数的数据如下:
const station = { name: "ZB1", readings: [ {temp: 47, time: "2016-11-10 09:10"}, {temp: 53, time: "2016-11-10 09:20"}, {temp: 58, time: "2016-11-10 09:30"}, {temp: 53, time: "2016-11-10 09:40"}, {temp: 51, time: "2016-11-10 09:50"}]};
下面的函数负责找到超出指定范围的温度读数:
function readingsOutsideRange(station, min, max) { return station.readings.filter(r => r.temp < min || r.temp > max);}
调用方
alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
请注意,这里的调用代码从另一个对象中抽出两项数据,转手又把这一对数 据传递给readingsOutsideRange。代表“运作计划”的operatingPlan对象用了另外 的名字来表示温度范围的下限和上限,与readingsOutsideRange中所用的名字不 同。像这样用两项各不相干的数据来表示一个范围的情况并不少见,最好是将其 组合成一个对象。
修改后的代码
class NumberRange…
class NumberRange { constructor(min, max) { this._data = {min: min, max: max}; } get min() {return this._data.min;} get max() {return this._data.max;} contains(arg) {return (arg >= this.min && arg <= this.max);} }
function readingsOutsideRange(station, range) { return station.readings .f ilter(r => !range.contains(r.temp)); }
调用方
const range = new NumberRange( operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling ); alerts = readingsOutsideRange(station,range);
Java翻译的原始代码
public static void main(String[] args) { readingsOutsideRange(createData(), OperatingPlan.temperatureFloor.getTemp(), OperatingPlan.temperatureCeiling.getTemp()); } public static void readingsOutsideRange (Station station, int min, int max){ station.getReadings().stream() .filter((r -> r.getTemp() > min && r.getTemp() < max)) .forEach((reading)-> System.out.println(reading.toString())); }
修改后的代码:
public static void main(String[] args) { NumberRange range = new NumberRange( OperatingPlan.temperatureFloor.getTemp(), OperatingPlan.temperatureCeiling.getTemp()); readingsOutsideRange(Prototype.createData(), range); } public static void readingsOutsideRange(Station station, NumberRange range) { station.getReadings().stream() .filter((r -> range.contains(r.getTemp()))) .forEach((reading) -> System.out.println(reading.toString())); }
范例:
function base(aReading) {...} function taxableCharge(aReading) {...}function calculateBaseCharge(aReading) {...}
class Reading { base() {...} taxableCharge() {...} calculateBaseCharge() {...} }
动机:
如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数 传递给函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一 个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调 用,并且这样一个对象也可以更方便地传递给系统的其他部分。
除了可以把已有的函数组织起来,这个重构还给我们一个机会,去发现其他的计算逻辑,将它们也重构到新的类当中。
做法:
详细示例:
我在英格兰长大,那是一个热爱喝茶的国度。(个人而言,我不喜欢在英格 兰喝到的大部分茶,对中国茶和日本茶倒是情有独钟。)所以,我虚构了一种用 于向老百姓供给茶水的公共设施。每个月会有软件读取茶水计量器的数据,得到 类似这样的读数(reading
reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};
客户端1…
const aReading = acquireReading();const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
在英格兰,一切生活必需品都得交税,茶自然也不例外。不过,按照规定, 只要不超出某个必要用量,就不用交税。
客户端2…
const aReading = acquireReading(); const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
客户端3…
const aReading = acquireReading(); const basicChargeAmount = calculateBaseCharge(aReading); function calculateBaseCharge(aReading) { return baseRate(aReading.month, aReading.year) * aReading.quantity; }
重构后的代码:
看到这里,我有一种自然的冲动,想把前面两处客户端代码都改为使用这个 函数。但这样一个顶层函数的问题在于,它通常位于一个文件中,读者不一定能 想到来这里寻找它。我更愿意对代码多做些修改,让该函数与其处理的数据在空 间上有更紧密的联系。为此目的,不妨把数据本身变成一个类。
我可以运用封装记录(162)将记录变成类。
class Reading { constructor(data) { this._customer = data.customer; this._quantity = data.quantity; this._month = data.month; this._year = data.year; } get customer() { return this._customer; } get quantity() { return this._quantity; } get month() { return this._month; } get year() { return this._year; } get baseCharge() { return baseRate(this.month, this.year) * this.quantity; } get taxableCharge() { return Math.max(0, this.baseCharge - taxThreshold(this.year)); }}
客户端1…
const rawReading = acquireReading(); const aReading = new Reading(rawReading); const baseCharge = aReading.baseCharge;
客户端2
const rawReading = acquireReading(); const aReading = new Reading(rawReading);const taxableCharge = aReading.taxableCharge;
客户端3…
const rawReading = acquireReading(); const aReading = new Reading(rawReading); const basicChargeAmount= aReading.baseCharge;
Java翻译的原始代码
private static void client1() { Read data=createData(); int baseCharge = baseRate(data.getMonth(), data.getYear()) * data.getQuantity(); } private static void client2() { Read data=createData(); int base = baseRate(data.getMonth(), data.getYear()) * data.getQuantity(); int taxableCharge = Math.max(0, base - taxThreshold(data.getYear())); } private static void client3() { Read data=createData(); int basicChargeAmount=calculateBaseCharge(data); } private static int calculateBaseCharge (Read data) { return baseRate(data.getMonth(),data.getYear()) * data.getQuantity(); }
重构后:
public class Read { private String customer; private int quantity; private int month; private int year; public Read(String customer, int quantity, int month, int year) { this.customer = customer; this.quantity = quantity; this.month = month; this.year = year; } public String getCustomer() { return customer; } public void setCustomer(String customer) { this.customer = customer; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } public int getMonth() { return month; } public void setMonth(int month) { this.month = month; } public int getYear() { return year; } public void setYear(int year) { this.year = year; } public int getBase(){ return baseRate(month, year) * quantity; } public int getTaxableCharge(){ return Math.max(0, getBase() - taxThreshold(year)); } private int baseRate(int month, int year) { return month * year / 2; } private int taxThreshold(int year) { return year / 4; }}
private static void client1() { Read data = Prototype.createData(); int baseCharge = data.getBase(); } private static void client2() { Read data = Prototype.createData(); int base = data.getBase(); int taxableCharge = data.getTaxableCharge(); } private static void client3() { Read data = Prototype.createData(); int basicChargeAmount = data.getBase(); }
范例:
function base(aReading) {...} function taxableCharge(aReading) {...}
function enrichReading(argReading) { const aReading = _.cloneDeep(argReading); aReading.baseCharge = base(aReading); aReading.taxableCharge = taxableCharge(aReading); return aReading;}
动机:
一个方式是采用数据变换(transform)函数:这种函数接受源数据作为输 入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。有了变换函 数,我就始终只需要到变换函数中去检查计算派生数据的逻辑。 函数组合成变换的替代方案是函数组合成类(144),后者的做法是先用源 数据创建一个类,再把相关的计算逻辑搬移到类中。这两个重构手法都很有用,
我常会根据代码库中已有的编程风格来选择使用其中哪一个。不过,两者有一个 重要的区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。
做法:
详细说明:
该示例和组合合成类的示例一致,只不过将所有的计算值的操作,放在了克隆源数据的方法中,不建议使用该方法,容易导致数据混乱,不过多介绍。
书中添加的方法
function enrichReading(original) { const result = _.cloneDeep(original); result.baseCharge = calculateBaseCharge(result); result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year)); return result;}
Java中的方法
protected Read clone() { Read read= null; try { read = (Read)super.clone(); read.setBaseCharge(baseRate(month, year) * quantity); read.setTaxableCharge( Math.max(0, read.getBaseCharge() - taxThreshold(year))); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return read;}
注:记得类实现Cloneable接口
范例:
const orderData = orderString.split(/\s+/); const productPrice = priceList[orderData[0].split("-")[1]];const orderPrice = parseInt(orderData[1]) * productPrice;
const orderRecord = parseOrder(order); const orderPrice = price(orderRecord, priceList);function parseOrder(aString) { const values = aString.split(/\s+/); return ({ productID: values[0].split("-")[1], quantity: parseInt(values[1])}); }function price(order, priceList) { return order.quantity * priceList[order.productID]; }
动机:
每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的 模块,因为这样到了需要修改的时候,我就可以单独处理每个主题,而不必同时 在脑子里考虑两个不同的主题。如果运气够好的话,我可能只需要修改其中一个 模块,完全不用回忆起另一个模块的诸般细节。
最简洁的拆分方法之一,就是把一大段行为分成顺序执行的两个阶段。可能
你有一段处理逻辑,其输入数据的格式不符合计算逻辑的要求,所以你得先对输 入数据做一番调整,使其便于处理。也可能是你把数据处理逻辑分成顺序执行的 多个步骤,每个步骤负责的任务全然不同。
做法:
详细示例:
我手上有一段“计算订单价格”的代码,至于订单中的商品是什么,我们从代 码中看不出来,也不太关心。
function priceOrder(product, quantity, shippingMethod) { const basePrice = product.basePrice * quantity; const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate; const shippingPerCase = (basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase; const shippingCost = quantity * shippingPerCase; const price = basePrice - discount + shippingCost; return price;}
虽然只是个常见的、过于简单的范例,从中还是能看出有两个不同阶段存在 的。前两行代码根据商品(product)信息计算订单中与商品相关的价格,随后的 两行则根据配送(shipping)信息计算配送成本。后续的修改可能还会使价格和 配送的计算逻辑变复杂,但只要这两块逻辑相对独立,将这段代码拆分成两个阶 段就是有价值的。
最终代码:
function priceOrder(product, quantity, shippingMethod) { const priceData = calculatePricingData(product, quantity); return applyShipping(priceData, shippingMethod);}function calculatePricingData(product, quantity) { const basePrice = product.basePrice * quantity; const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate; return {basePrice: basePrice, quantity: quantity, discount:discount};}function applyShipping(priceData, shippingMethod) { const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase; const shippingCost = priceData.quantity * shippingPerCase; return priceData.basePrice - priceData.discount + shippingCost; }
个人理解:中转数据中priceData中之所以没有shippingMethod,是因为只在第二阶段使用了该数据,而第一阶段没有使用过,中转数据应该两个阶段都使用上了
Java中原始代码:
private static int priceOrder(Product product, int quantity, ShippingMethod shippingMethod) { int basePrice = product.getBasePrice() * quantity; int discount = Math.max(quantity - product.getDiscountThreshold(), 0) * product.getBasePrice() * product.getDiscountRate(); int shippingPerCase = (basePrice > shippingMethod.getDiscountThreshold()) ? ShippingMethod.discountedFee.getDiscountThreshold() : ShippingMethod.feePerCase.getDiscountThreshold(); int shippingCost = quantity * shippingPerCase; int price = basePrice - discount + shippingCost; return price; }
重构后:
private static int priceOrder(Product product, int quantity, ShippingMethod shippingMethod) { PriceDataBO data=calPricingData(product,quantity); int price = applyShipping(data,shippingMethod); return price; } private static PriceDataBO calPricingData(Product product,int quantity){ int basePrice = product.getBasePrice() * quantity; int discount = Math.max(quantity - product.getDiscountThreshold(), 0) * product.getBasePrice() * product.getDiscountRate(); return new PriceDataBO(basePrice,discount,quantity); } private static int applyShipping(PriceDataBO data,ShippingMethod shippingMethod){ int shippingPerCase = (data.getBasePrice() > shippingMethod.getDiscountThreshold()) ? ShippingMethod.discountedFee.getDiscountThreshold() : ShippingMethod.feePerCase.getDiscountThreshold(); int shippingCost = data.getQuantity() * shippingPerCase; return data.getBasePrice() - data.getDiscount() + shippingCost; }
书中范例:
organization = {name: "Acme Gooseberries", country: "GB"};
class Organization { constructor(data) { this._name = data.name; this._country = data.country; } get name() {return this._name;} set name(arg) {this._name = arg;} get country() {return this._country;} set country(arg) {this._country = arg;}}
动机:
若这种记录只在程序的一个小范围里使用,那问题还不 大,但若其使用范围变宽,“数据结构不直观”这个问题就会造成更多困扰。我可 以重构它,使其变得更直观——但如果真需要这样做,那还不如使用类来得直接。
Java范例原型:
用Map封装数据之后,使用get/put方法去读取和更新数据,方便简单,适合一些数据类型统一,并且不常被使用的数据。
Map<String,String> organization=new HashMap<>(); organization.put("name","Acme Gooseberries"); organization.put("country","GB"); //读取和更新 String result = organization.get("name"); organization.put("name","newName")
重构后:
我们将Map里的数据封装成一个对象里的实例变量,提供get/set方法
private String name; private String country; public Organization(String name, String country) { this.name = name; this.country = country; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; }
修改之前操作数据的方法
Organization organization=new Organization("Acme Gooseberries","GB"); //读取和更新 String result = organization.getName(); organization.setName("newName");
对象相较Map这种方式,更有通用性,Map在面对数据类型不一致的结构,会存在很多的麻烦事,即便你将Value的类型设置为Object,在之后的数据类型转化时,会存在很多坑,并且面对复杂的嵌套类,Map更显得心有余而力不足。
范例:
class Person { get courses() {return this._courses;} set courses(aList) {this._courses = aList;}}
class Person { get courses() {return this._courses.slice();} addCourse(aCourse) { ... } removeCourse(aCourse) { ... }}
动机:
封装集合时人们常常犯一个错误:只对集合变量的访问进行了封装,但依然让取值函数返回集合本身。这使得集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入。
为避免此种情况,我会在类上提供一些修改集合的方法——通常是“添 加”和“移除”方法。这样就可使对集合的修改必须经过类,当程序演化变大时, 我依然能轻易找出修改点。
Java范例原型:
假设有个人(Person)要去上课。我们用一个简单的Course来表示“课程”。
不了解Java8新特性的,可以看我的这篇文章https://blog.csdn.net/a5f4s6f4a65f4a6sf/article/details/90735310
Person person=new Person(); //1. person.setCourse(basicCourseNames.stream() .map((name)->new Course(name,false)) .collect(Collectors.toList())); //2. for (String basicCourseName : basicCourseNames) { person.getCourse().add(new Course(basicCourseName,false)); }
有些开发者可能觉得这个类已经得到了恰当的封装,毕竟,所有的字段都被 访问函数保护到了。但我要指出,对课程列表的封装还不完整。诚然,对列表整体的任何更新操作,都能通过设值函数得到控制。
但客户端也可能发现,直接更新课程列表显然更容易。
这就破坏了封装性,因为以此种方式更新列表Person类根本无从得知。这里 仅仅封装了字段引用,而未真正封装字段的内容。
重构后:
现在我来对类实施真正恰当的封装,首先要为类添加两个方法,为客户端提供“添加课程”和“移除课程”的接口。
Person类
public void addCourse(Course course) { this.course.add(course); } public void removeCourse(Course course) { int index = this.course.indexOf(course); if (index == -1) { throw new ArrayIndexOutOfBoundsException(); }else { this.course.remove(course); } }
客户端也随之修改为
Person person=new Person(); for (String basicCourseName : basicCourseNames) { person.addCourse(new Course(basicCourseName,false)); }
范例:
orders.filter(o => "high" === o.priority || "rush" === o.priority);
orders.filter(o => o.priority.higherThan(new Priority("normal")))
动机:
开发初期,你往往决定以简单的数据项表示简单的情况,比如使用数字或字 符串等。但随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。 比如说,一开始你可能会用一个字符串来表示“电话号码”的概念,但是随后它又 需要“格式化”“抽取区号”之类的特殊行为。这类逻辑很快便会占领代码库,制造出许多重复代码,增加使用时的成本。
一旦我发现对某个数据的操作不仅仅局限于打印时,我就会为它创建一个新 类。一开始这个类也许只是简单包装一下简单类型的数据,不过只要类有了,日后添加的业务逻辑就有地可去了。
做法:
范例:
const basePrice = this._quantity * this._itemPrice; if (basePrice > 1000) return basePrice * 0.95;else return basePrice * 0.98;
get basePrice() {this._quantity * this._itemPrice;} ...if (this.basePrice > 1000) return this.basePrice * 0.95; else return this.basePrice * 0.98;
动机:
如果我正在分解一个冗长的函数,那么将变量抽取到函数里能使函数的分解 过程更简单,因为我就不再需要将变量作为参数传递给提炼出来的小函数。将变 量的计算逻辑放到函数中,也有助于在提炼得到的函数与原函数之间设立清晰的 边界,这能帮我发现并避免难缠的依赖及副作用。
改用函数还让我避免了在多个函数中重复编写计算逻辑。每当我在不同的地 方看见同一段变量的计算逻辑,我就会想方设法将它们挪到同一个函数里。
Java范例原型:
private int quantity; private Item item; Order(int quantity,Item item) { this.quantity=quantity; this.item=item; } public double prototypeGetPrice() { int basePrice = this.quantity * this.item.getPrice(); double discountFactor = 0.98; if (basePrice > 1000) discountFactor -= 0.03; return basePrice * discountFactor; }
重构后:
private int quantity; private Item item; Order(int quantity,Item item) { this.quantity=quantity; this.item=item; } public int getBasePrice(){ return this.quantity * this.item.getPrice();; } public double getDisCountFactor(){ double discountFactor = 0.98; if (getBasePrice() > 1000) discountFactor -= 0.03; return discountFactor; } public double modificationGetPrice() { return getBasePrice() * getDisCountFactor(); }
范例:
class Person { get officeAreaCode() {return this._officeAreaCode;} get officeNumber() {return this._officeNumber;}}
class Person { get officeAreaCode() {return this._telephoneNumber.areaCode;} get officeNumber() {return this._telephoneNumber.number;} }class TelephoneNumber { get areaCode() {return this._areaCode;} get number() {return this._number;} }
动机:
设想你有一个维护大量函数和数据的类。这样的类往往因为太大而不易理 解。此时你需要考虑哪些部分可以分离出去,并将它们分离到一个独立的类中。 如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依, 这就表示你应该将它们分离出去。一个有用的测试就是问你自己,如果你搬移了 某些字段和函数,会发生什么事?其他字段和函数是否因此变得无意义?
范例:
class Person { get officeAreaCode() {return this._telephoneNumber.areaCode;} get officeNumber() {return this._telephoneNumber.number;} } class TelephoneNumber { get areaCode() {return this._areaCode;} get number() {return this._number;} }
class Person { get officeAreaCode() {return this._officeAreaCode;} get officeNumber() {return this._officeNumber;}}
动机:
内联类正好与提炼类(182)相反。如果一个类不再承担足够责任,不再有 单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),我就会 挑选这一“萎缩类”的最频繁用户(也是一个类),以本手法将“萎缩类”塞进另一 个类中。
范例:
manager = aPerson.department.manager;
manager = aPerson.manager;class Person { get manager() {return this.department.manager;}}
动机:
一个好的模块化的设计,“封装”即使不是其最关键特征,也是最关键特征之 一。“封装”意味着每个模块都应该尽可能少了解系统的其他部分。如此一来,一 旦发生变化,需要了解这一变化的模块就会比较少——这会使变化比较容易进行。
如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调 用后者的函数,那么客户就必须知晓这一层委托关系。万一受托类修改了接口, 变化会波及通过服务对象使用它的所有客户端。我可以在服务对象上放置一个简 单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即使将来 委托关系发生变化,变化也只会影响服务对象,而不会直接波及所有客户端。
简单来说,将委托关系封装起来,如果修改,只需修改一处地方。
范例:
manager = aPerson.manager; class Person { get manager() {return this.department.manager;}}
manager = aPerson.department.manager;
动机:
在隐藏委托关系(189)的“动机”一节中,我谈到了“封装受托对象”的好处。 但是这层封装也是有代价的。每当客户端要使用受托类的新特性时,你就必须在 服务端添加一个简单委托函数。随着受托类的特性(功能)越来越多,更多的转 发函数就会使人烦躁。服务类完全变成了一个中间人(81),此时就应该让客户 直接调用受托类。(这个味道通常在人们狂热地遵循迪米特法则时悄然出现。我 总觉得,如果这条法则当初叫作“偶尔有用的迪米特建议”,如今能少很多烦恼)
我可以混用两种用法。有些委托关系非常常用,因此我想保住它们,这样可 使客户端代码调用更友好。何时应该隐藏委托关系,何时应该移除中间人,对我 而言没有绝对的标准——代码环境自然会给出该使用哪种手法的线索,具备思考 能力的程序员应能分辨出何种手法更佳。
范例:
function foundPerson(people) { for(let i = 0; i < people.length; i++) { if(people[i] === "Don") { return "Don"; } if(people[i] === "John") { return "John"; } if(people[i] === "Kent") { return "Kent"; } } return ""; }
function foundPerson(people) { const candidates = ["Don", "John", "Kent"]; return people.find(p => candidates.includes(p)) || ''; }
动机:
如果我发现做一件事可以有更清晰的方式,我就会用 比较清晰的方式取代复杂的方式。“重构”可以把一些复杂的东西分解为较简单的 小块,但有时你就必须壮士断腕,删掉整个算法,代之以较简单的算法。随着对 问题有了更多理解,我往往会发现,在原先的做法之外,有更简单的解决方案, 此时我就需要改变原先的算法。如果我开始使用程序库,而其中提供的某些功能/ 特性与我自己的代码重复,那么我也需要改变原先的算法。
Java原型:
String[] people = {"Don", "John", "Kent","Jack"}; String result = ""; for (int i = 0; i < people.length; i++) { if ("Don".equals(people[i])) { result = "Don"; } if ("John".equals(people[i])) { result = "John"; } if ("Kent".equals(people[i])) { result = "Kent"; } }
重构后:
String[] people = {"Don", "John", "Kent", "Jack"}; List<String> candidates = Arrays.asList("Don", "John", "Kent"); Arrays.stream(people).filter((p) -> { return candidates.contains(p); }).forEach(System.out::print);
范例:
class Account { get overdraftCharge() {....}}
class AccountType { get overdraftCharge() {...}}
动机:
搬移函数最直接的一个动因是,它频繁引用其他上下文中的元素,而对自身 上下文中的元素却关心甚少。此时,让它去与那些更亲密的元素相会,通常能取 得更好的封装效果,因为系统别处就可以减少对当前模块的依赖。
同样,如果我在整理代码时,发现需要频繁调用一个别处的函数,我也会考 虑搬移这个函数。有时你在函数内部定义了一个帮助函数,而该帮助函数可能在别的地方也有用处,此时就可以将它搬移到某些更通用的地方。同理,定义在一 个类上的函数,可能挪到另一个类中去更方便我们调用。
Java原型范例:
public double bankCharge() { double result = 4.5; if (this.daysOverdrawn > 0) result += this.overdraftCharge(); return result; } public double overdraftCharge() { if (this.type.isPremium()) { int baseCharge = 10; if (this.daysOverdrawn <= 7) { return baseCharge; } else { return baseCharge + (this.daysOverdrawn - 7) * 0.85; } } else { return this.daysOverdrawn * 1.75; } }
重构后:
class Account…
public double bankCharge() { double result = 4.5; if (this.daysOverdrawn > 0) result += this.type.overdraftCharge(this.daysOverdrawn); return result; }
class AccountType
public double overdraftCharge(int daysOverdrawn) { if (this.isPremium()) { int baseCharge = 10; if (daysOverdrawn <= 7) { return baseCharge; } else { return baseCharge + (daysOverdrawn - 7) * 0.85; } } else { return daysOverdrawn * 1.75; } }
范例:
class Customer { get plan() {return this._plan;} get discountRate() {return this._discountRate;}}
class Customer { get plan() {return this._plan;} get discountRate() {return this.plan.discountRate;}}
动机:
如果我发现数据结构已经不适应于需求,就应该马上修缮它。如果容许瑕疵 存在并进一步累积,它们就会经常使我困惑,并且使代码愈来愈复杂。
我开始寻思搬移数据,可能是因为我发现每当调用某个函数时,除了传入一个记录参数,还总是需要同时传入另一条记录的某个字段一起作为参数。总是一 同出现、一同作为函数参数传递的数据,最好是规整到同一条记录中,以体现它 们之间的联系。
修改的难度也是引起我注意的一个原因,如果修改一条记录时, 总是需要同时改动另一条记录,那么说明很可能有字段放错了位置。此外,如果 我更新一个字段时,需要同时在多个结构中做出修改,那也是一个征兆,表明该 字段需要被搬移到一个集中的地点,这样每次只需修改一处地方。
范例:
result.push(`<p>title: ${person.photo.title}</p>`); result.concat(photoData(person.photo)); function photoData(aPhoto) { return [ `<p>location: ${aPhoto.location}</p>`, `<p>date: ${aPhoto.date.toDateString()}</p>`, ]; }
result.concat(photoData(person.photo)); function photoData(aPhoto) { return [ `<p>title: ${aPhoto.title}</p>`, `<p>location: ${aPhoto.location}</p>`, `<p>date: ${aPhoto.date.toDateString()}</p>`, ]; }
动机:
要维护代码库的健康发展,需要遵守几条黄金守则,其中最重要的一条当属“消除重复”。如果我发现调用某个函数时,总有一些相同的代码也需要每次执 行,那么我会考虑将此段代码合并到函数里头。这样,日后对这段代码的修改只 需改一处地方,还能对所有调用者同时生效。如果将来代码对不同的调用者需有 不同的行为,那时再通过搬移语句到调用者(217)将它(或其一部分)搬移出 来也十分简单。
Java代码原型:
我将用一个例子来讲解这项手法。以下代码会生成一些关于相片(photo)的 HTML
public static void main(String[] args) { List<String> result = new ArrayList<>(); result.add("<p>${person.name}</p>"); result.add(renderPhoto(person.getPhoto())); result.add("<p>title:" + person.getPhoto().getTitle() + "</p>"); result.add(emitPhotoData(person.getPhoto())); } private static String photoDiv(Photo photo) { List<String> result = Arrays.asList("<div>", "<p>title: " + photo.getTitle() + "</p>", emitPhotoData(photo), "</div>"); return StringUtils.join(result, ","); } private static String emitPhotoData(Photo photo) { List<String> result=new ArrayList<>(); result.add("<p>location:"+photo.getLocation()+"</p>"); result.add("<p>date:"+photo.getDate().toString()+"</p>"); return StringUtils.join(result,"\n"); }
重构后:
public static void main(String[] args) { List<String> result = new ArrayList<>(); result.add("<p>${person.name}</p>"); result.add(renderPhoto(person.getPhoto())); result.add(emitPhotoData(person.getPhoto())); System.out.println(StringUtils.join(result, ",")); } private static String photoDiv(Photo photo) { List<String> result = Arrays.asList("<div>",emitPhotoData(photo), "</div>"); return StringUtils.join(result, ","); } private static String emitPhotoData(Photo photo) { List<String> result=new ArrayList<>(); result.add("<p>title:" + photo.getTitle() + "</p>"); result.add("<p>location:"+photo.getLocation()+"</p>"); result.add("<p>date:"+photo.getDate().toString()+"</p>"); return StringUtils.join(result,"\n"); }
范例:
emitPhotoData(outStream, person.photo); function emitPhotoData(outStream, photo) { outStream.write(`<p>title: ${photo.title}</p>\n`); outStream.write(`<p>location: ${photo.location}</p>\n`); }
emitPhotoData(outStream, person.photo); outStream.write(`<p>location: ${person.photo.location}</p>\n`);function emitPhotoData(outStream, photo) { outStream.write(`<p>title: ${photo.title}</p>\n`); }
动机
与其他抽象机制的设计一样,我们并非总能平衡好 抽象的边界。随着系统能力发生演进(通常只要是有用的系统,功能都会演 进),原先设定的抽象边界总会悄无声息地发生偏移。对于函数来说,这样的边 界偏移意味着曾经视为一个整体、一个单元的行为,如今可能已经分化出两个甚 至是多个不同的关注点。
函数边界发生偏移的一个征兆是,以往在多个地方共用的行为,如今需要在 某些调用点面前表现出不同的行为。于是,我们得把表现不同的行为从函数里挪 出,并搬移到其调用处。这种情况下,我会使用移动语句(223)手法,先将表 现不同的行为调整到函数的开头或结尾,再使用本手法将语句搬移到其调用点。 只要差异代码被搬移到调用点,我就可以根据需要对其进行修改。
示例:
let appliesToMass = false; for(const s of states) { if (s === "MA") appliesToMass = true;}
appliesToMass = states.includes("MA");
动机:
善用函数可以帮助我将相关的行为打包起来,这对于提升代码的表达力大有 裨益—— 一个命名良好的函数,本身就能极好地解释代码的用途,使读者不必了 解其细节。函数同样有助于消除重复,因为同一段代码我不需要编写两次,每次 调用一下函数即可。此外,当我需要修改函数的内部实现时,也不需要四处寻找 有没有漏改的相似代码。(当然,我可能需要检查函数的所有调用点,判断它们 是否都应该使用新的实现,但通常很少需要这么仔细,即便需要,也总好过四处 寻找相似代码。)
如果我见到一些内联代码,它们做的事情仅仅是已有函数的重复,我通常会 以一个函数调用取代内联代码。但有一种情况需要特殊对待,那就是当内联代码 与函数之间只是外表相似但其实并无本质联系时。
示例:
const pricingPlan = retrievePricingPlan(); cost order = retreiveOrder();let charge;const chargePerUnit = pricingPlan.unit;
const pricingPlan = retrievePricingPlan();const chargePerUnit = pricingPlan.unit; const order = retreiveOrder(); let charge;
动机:
让存在关联的东西一起出现,可以使代码更容易理解。如果有几行代码取用 了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据 结构的代码中间。最简单的情况下,我只需使用移动语句就可以让它们聚集起 来。此外还有一种常见的“关联”,就是关于变量的声明和使用。有人喜欢在函数 顶部一口气声明函数用到的所有变量,我个人则喜欢在第一次需要使用变量的地 方再声明它。
示例:
let averageAge = 0; let totalSalary = 0; for (const p of people) { averageAge += p.age; totalSalary += p.salary; }averageAge = averageAge / people.length;
let totalSalary = 0; for (const p of people) { totalSalary += p.salary; }let averageAge = 0;for (const p of people) { averageAge += p.age; }averageAge = averageAge / people.length;
动机:
这项重构手法可能让许多程序员感到不安,因为它会迫使你执行两次循环。 对此,我一贯的建议也与2.8节里所明确指出的一致:先进行重构,然后再进行性 能优化。我得先让代码结构变得清晰,才能做进一步优化;如果重构之后该循环 确实成了性能的瓶颈,届时再把拆开的循环合到一起也很容易。但实际情况是, 即使处理的列表数据更多一些,循环本身也很少成为性能瓶颈,更何况拆分出循 环来通常还使一些更强大的优化手段变得可能。
示例:
const names = []; for (const i of input) { if (i.job === "programmer") names.push(i.name); }
const names = input.filter(i => i.job === "programmer") .map(i => i.name) ;
动机:
我发现一些 逻辑如果采用集合管道来编写,代码的可读性会更强——我只消从头到尾阅读一 遍代码,就能弄清对象在管道中间的变换过程。
Java原型范例:
public static void main(String[] args) { String input = "office, country, telephone \n" + "Chicago, USA, +1 312 373 1000 \n" + "Beijing, China, +86 4008 900 505 \n" + "Bangalore, India, +91 80 4064 9570 \n" + "Porto Alegre, Brazil, +55 51 3079 3550 \n" + "Chennai, India, +91 44 660 44766"; System.out.println(acquireData(input)); } public static List<String> acquireData(String input) { String[] lines = input.split("\n"); boolean firstLine = true; List<String> result = new ArrayList<>(); for (String line : lines) { if (firstLine) { firstLine = false; continue; } if (line.trim() == "") continue; String[] record = line.split(","); if ("India".equals(record[1].trim())) { result.add("city:" + record[0].trim() + ",phone:" + record[2].trim()); } } return result; }
重构后:
public static List<String> acquireData(String input) { String[] lines = input.split("\n"); return Arrays.stream(lines).skip(1) .filter((line)->line.trim()!="") .map((line)->line.split(",")) .filter((fileds)->"India".equals(fileds[1].trim())) .map((fileds)->"city:" + fileds[0].trim() + ",phone:" + fileds[2].trim()) .collect(Collectors.toList()); }
范例:
let temp = 2 * (height + width);console.log(temp);temp = height * width; console.log(temp);
const perimeter = 2 * (height + width);console.log(perimeter); const area = height * width; console.log(area);
动机:
除了这两种情况,还有很多变量用于保存一段冗长代码的运算结果,以便稍 后使用。这种变量应该只被赋值一次。如果它们被赋值超过一次,就意味它们在 函数中承担了一个以上的责任。如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情,会令代码阅读者糊涂。
范例:
class Organization { get name() {...}}
class Organization { get title() {...} }
动机:
记录结构中的字段可能需要改名,类的字段也一样。在类的使用者看来,取 值和设值函数就等于是字段。对这些函数的改名,跟裸记录结构的字段改名一样重要。
范例:
get discountedTotal() {return this._discountedTotal;} set discount(aNumber) { const old = this._discount; this._discount = aNumber; this._discountedTotal += old - aNumber; }
get discountedTotal() {return this._baseTotal - this._discount;}set discount(aNumber) {this._discount = aNumber;}
动机:
可变数据是软件中最大的错误源头之一。对数据的修改常常导致代码的各个 部分以丑陋的形式互相耦合:在一处修改数据,却在另一处造成难以发现的破 坏。很多时候,完全去掉可变数据并不现实,但我还是强烈建议:尽量把可变数 据的作用域限制在最小范围。
有些变量其实可以很容易地随时计算出来。如果能去掉这些变量,也算朝着 消除可变性的方向迈出了一大步。计算常能更清晰地表达数据的含义,而且也避 免了“源数据修改时忘了更新派生变量”的错误。
范例:
class Product { applyDiscount(arg) {this._price.amount -= arg;}}
class Product { applyDiscount(arg) { this._price = new Money(this._price.amount - arg, this._price.currency); }
动机:
在把一个对象(或数据结构)嵌入另一个对象时,位于内部的这个对象可以 被视为引用对象,也可以被视为值对象。两者最明显的差异在于如何更新内部对 象的属性:如果将内部对象视为引用对象,在更新其属性时,我会保留原对象不 动,更新内部对象的属性;如果将其视为值对象,我就会替换整个内部对象,新 换上的对象会有我想要的属性值。
值对象和引用对象的区别也告诉我,何时不应该使用本重构手法。如果我想 在几个对象之间共享一个对象,以便几个对象都能看见对共享对象的修改,那么这个共享的对象就应该是引用。
简单来说,值对象更改当前数据,不会影响源数据,引用对象更改当前数据,会影响源数据。
Java代码示例:
class Product…
private Money price; public Product(Money price) { this.price = price; } public Money getPrice() { return price; } public void setPrice(Money price) { this.price = price; } public void applyDiscount1(Integer amount) { this.price.setAmount(this.price.getAmount() - amount); } public void applyDiscount2(Integer amount) { this.price=new Money(this.price.getAmount()-amount,this.price.getCurrency()); }
class money…
private int amount; private String currency; public Money(int amount, String currency) { this.amount = amount; this.currency = currency; } public int getAmount() { return amount; } public void setAmount(int amount) { this.amount = amount; } public String getCurrency() { return currency; } public void setCurrency(String currency) { this.currency = currency; }
Money money = new Money(100, "元"); Product p=new Product(money); p.applyDiscount1(20); System.out.println(money.getAmount()); //80 p.applyDiscount2(20); System.out.println(money.getAmount()); //80
范例:
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) charge = quantity * plan.summerRate; else charge = quantity * plan.regularRate + plan.regularServiceCharge;
if (summer()) charge = summerCharge(); else charge = regularCharge();
动机:
程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。我必须编写 代码来检查不同的条件分支,根据不同的条件做不同的事,然后,我很快就会得 到一个相当长的函数。大型函数本身就会使代码的可读性下降,而条件逻辑则会 使代码更难阅读。在带有复杂条件逻辑的函数中,代码(包括检查条件分支的代 码和真正实现功能的代码)会告诉我发生的事,但常常让我弄不清楚为什么会发 生这样的事,这就说明代码的可读性的确大大降低了。
和任何大块头代码一样,我可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函 数,从而更清楚地表达自己的意图。对于条件逻辑,将每个分支条件分解成新函 数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
Java范例:
假设我要计算购买某样商品的总价(总价=数量×单价),而这个商品在冬季 和夏季的单价是不同的:
if (aDate.getTime() > plan.getSummerStart().getTime() && aDate.getTime() < plan.getSummerEnd().getTime()) charge = quantity * plan.getSummerRate();else charge = quantity * plan.getRegularRate() + plan.getRegularServiceCharge();
重构后:
int charge = isSummer(aDate, plan) ? summerCharge(quantity, plan.getSummerRate()) : regularCharge(quantity, plan); private static boolean isSummer(Date aDate, Plan plan) { return aDate.getTime() > plan.getSummerStart().getTime() && aDate.getTime() < plan.getSummerEnd().getTime(); } private static int summerCharge(int quantity, int rate) { return quantity * rate; } private static int regularCharge(int quantity, Plan plan) { return quantity * plan.getRegularRate() + plan.getRegularServiceCharge(); }
范例:
if (anEmployee.seniority < 2) return 0; if (anEmployee.monthsDisabled > 12) return 0; if (anEmployee.isPartTime) return 0;
if (isNotEligibleForDisability()) return 0; function isNotEligibleForDisability() { return ((anEmployee.seniority < 2) || (anEmployee.monthsDisabled > 12) || (anEmployee.isPartTime)); }
动机:
有时我会发现这样一串条件检查:检查条件各不相同,最终行为却一致。如 果发现这种情况,就应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。
顺序执行的条件表达式用逻辑或来合并,嵌套的if语句用逻辑与来合并。
范例:
function getPayAmount() { let result; if (isDead) result = deadAmount(); else { if (isSeparated) result = separatedAmount(); else { if (isRetired) result = retiredAmount(); else result = normalPayAmount(); } } return result; }
function getPayAmount() { if (isDead) return deadAmount(); if (isSeparated) return separatedAmount(); if (isRetired) return retiredAmount(); return normalPayAmount();}
动机:
以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。如 果使用if-then-else结构,你对if分支和else分支的重视是同等的。这样的代码 结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它 告诉阅读者:“这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请 做一些必要的整理工作,然后退出。
卫语句:就是把复杂的条件表达式拆分成多个条件表达式,比如一个很复杂的表达式,嵌套了好几层的if - then-else语句,转换为多个if语句,实现它的逻辑,这多条的if语句就是卫语句.
Java示例:
public String payAmount(Employee employee) { String result; if(employee.isSeparated()) { result="{amount: 0, reasonCode:SEP}"; }else { if (employee.isRetired()) { result = "{amount: 0, reasonCode: RET}"; }else { result = someFinalComputation(); } } return result; }
重构后:
public String payAmount(Employee employee) { if (employee.isSeparated()) { return "{amount: 0, reasonCode:SEP}"; } if (employee.isRetired()) { return "{amount: 0, reasonCode: RET}"; } return someFinalComputation(); }
将条件反转(Java原型):
public int adjustedCapital(AnInstrument anInstrument) { int result = 0; if (anInstrument.getCapital() > 0) { if (anInstrument.getInterestRate() > 0 && anInstrument.getDuration() > 0) { result = (anInstrument.getIncome() / anInstrument.getDuration()) * anInstrument.getAdjustmentFactor(); } } return result; }
重构后:
if (anInstrument.getCapital() =<0 || anInstrument.getInterestRate() <= 0 || anInstrument.getDuration() <= 0) { return 0; } return (anInstrument.getIncome() / anInstrument.getDuration()) * anInstrument.getAdjustmentFactor();
范例:
switch (bird.type) { case 'EuropeanSwallow': return "average"; case 'AfricanSwallow': return (bird.numberOfCoconuts > 2) ? "tired" : "average"; case 'NorwegianBlueParrot': return (bird.voltage > 100) ? "scorched" : "beautiful"; default: return "unknown";
class EuropeanSwallow { get plumage() { return "average"; } class AfricanSwallow { get plumage() { return (this.numberOfCoconuts > 2) ? "tired" : "average"; } class NorwegianBlueParrot { get plumage() { return (this.voltage > 100) ? "scorched" : "beautiful"; }
动机:
复杂的条件逻辑是编程中最难理解的东西之一,因此我一直在寻求给条件逻 辑添加结构。很多时候,我发现可以将条件逻辑拆分到不同的场景(或者叫高阶 用例),从而拆解复杂的条件逻辑。这种拆分有时用条件逻辑本身的结构就足以 表达,但使用类和多态能把逻辑的拆分表述得更清晰。
Java范例原型:
下面有一个这样的例子:有一家评级机构,要对远洋航船的航行进行投资评 级。这家评级机构会给出“A”或者“B”两种评级,取决于多种风险和盈利潜力的因 素。在评估风险时,既要考虑航程本身的特征,也要考虑船长过往航行的历史。
总结:找出相似的需要判断的地方,仔细找出其中的规律,构建多态关系
public String rating(Voyage voyage, List<History> histories) { int vpf = voyageProfitFactor(voyage, histories); int vr = voyageRisk(voyage); int chr = captainHistoryRisk(voyage, histories); if (vpf * 3 > (vr + chr * 2)) return "A"; else return "B"; } private int captainHistoryRisk(Voyage voyage, List<History> histories) { int result = 1; if (histories.size() < 5) result += 4; result += histories.stream().filter(v->v.getProfit()<0).count(); if ("china".equals(voyage.getZone()) && hasChina(histories)) result -= 2; return Math.max(result, 0); } private int voyageProfitFactor(Voyage voyage, List<History> histories) { int result = 2; if ("china".equals(voyage.getZone())) result += 1; if ("east-indies".equals(voyage.getZone())) result += 1; if ("china".equals(voyage.getZone()) && hasChina(histories)) { result += 3; if (histories.size() > 10) result += 1; if (voyage.getLength() > 12) result += 1; if (voyage.getLength() > 18) result -= 1; }else { if (histories.size() > 8) result += 1; if (voyage.getLength() > 14) result -= 1; } return result; } private int voyageRisk(Voyage voyage) { int result = 1; if (voyage.getLength() > 4) result += 2; if (voyage.getLength() > 8) result += voyage.getLength() - 8; if (Arrays.asList("china", "east-indies").contains(voyage.getZone())) result += 4; return Math.max(result, 0); } private boolean hasChina(List<History> histories) { return histories.stream().filter(v->"china".equals(v.getZone())).count()>0; }
重构后:
private Voyage voyage; private List<History> histories; public Rating(Voyage voyage, List<History> histories) { this.voyage = voyage; this.histories = histories; } public String rating() { int vpf = voyageProfitFactor(); int vr = voyageRisk(); int chr = captainHistoryRisk(); if (vpf * 3 > (vr + chr * 2)) return "A"; else return "B"; } public int captainHistoryRisk() { int result = 1; if (histories.size() < 5) result += 4; result += histories.stream().filter(v -> v.getProfit() < 0).count(); return Math.max(result, 0); } public int voyageProfitFactor() { int result = 2; if ("china".equals(voyage.getZone())) result += 1; if ("east-indies".equals(voyage.getZone())) result += 1; result += historyLengthFactor(); result += voyageLengthFactor(); return result; } public int historyLengthFactor() { return histories.size() > 8 ? 1 : 0; } public int voyageLengthFactor() { return voyage.getLength() > 14 ? -1 : 0; } public int voyageRisk() { int result = 1; if (voyage.getLength() > 4) result += 2; if (voyage.getLength() > 8) result += voyage.getLength() - 8; if (Arrays.asList("china", "east-indies").contains(voyage.getZone())) result += 4; return Math.max(result, 0); }
范例:
function getTotalOutstandingAndSendBill() { const result = customer.invoices .reduce((total, each) => each.amount + total, 0); sendBill(); return result; }
function totalOutstanding() { return customer.invoices.reduce((total, each) => each.amount + total, 0); }function sendBill() { emailGateway.send(formatBill(customer)); }
动机:
如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很 有价值的东西。我可以任意调用这个函数,也可以把调用动作搬到调用函数的其 他地方。这种函数的测试也更容易。简而言之,需要操心的事情少多了。
范例:
function tenPercentRaise(aPerson) { aPerson.salary = aPerson.salary.multiply(1.1); }function fivePercentRaise(aPerson) { aPerson.salary = aPerson.salary.multiply(1.05); }
function raise(aPerson, factor) { aPerson.salary = aPerson.salary.multiply(1 + factor); }
动机:
如果我发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并 成一个函数,以参数的形式传入不同的值,从而消除重复。这个重构可以使函数 更有用,因为重构后的函数还可以用于处理其他的值。
Java原型范例:
public double baseCharge(int usage) { if (usage < 0) return usd(0); double amount = bottomBand(usage) * 0.03 + middleBand(usage) * 0.05 + topBand(usage) * 0.07; return usd(amount); } public double bottomBand(int usage) { return Math.min(usage, 100); } public double middleBand(int usage) { return usage > 100 ? Math.min(usage, 200) - 100 : 0; } public double topBand(int usage) { return usage > 200 ? usage - 200 : 0; }
重构后:
public double baseCharge(int usage) { if (usage < 0) return usd(0); double amount = withinBand(usage,100,0) * 0.03 + withinBand(usage,200,100) * 0.05 + withinBand(usage,Integer.MIN_VALUE,200) * 0.07; return usd(amount); } public double withinBand(int usage,int top,int bottom){ return usage > bottom ? Math.min(usage, top) - bottom : 0; }
范例:
function setDimension(name, value) { if (name === "height") { this._height = value; return; } if (name === "width") { this._width = value; return; } }
function setHeight(value) {this._height = value;} function setWidth (value) {this._width = value;}
动机:
我不喜欢标记参数,因为它们让人难以理解到底有哪些函数可以调用、应该 怎么调用。拿到一份API以后,我首先看到的是一系列可供调用的函数,但标记 参数却隐藏了函数调用中存在的差异性。使用这样的函数,我还得弄清标记参数 有哪些可用的值。布尔型的标记尤其糟糕,因为它们不能清晰地传达其含义—— 在调用一个函数时,我很难弄清true到底是什么意思。如果明确用一个函数来完 成一项单独的任务,其含义会清晰得多。
范例:
leadEngineer = new Employee(document.leadEngineer, 'E');
leadEngineer = createEngineer(document.leadEngineer)
动机:
很多面向对象语言都有特别的构造函数,专门用于对象的初始化。需要新建 一个对象时,客户端通常会调用构造函数。但与一般的函数相比,构造函数又常 有一些丑陋的局限性。例如,Java的构造函数只能返回当前所调用类的实例,也 就是说,我无法根据环境或参数信息返回子类实例或代理对象;构造函数的名字 是固定的,因此无法使用比默认名字更清晰的函数名;构造函数需要通过特殊的 操作符来调用(在很多语言中是new关键字),所以在要求普通函数的场合就难以使用。
联系客服