打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
重构改善既有代码的设计第二部java

目录

前言

最近在读《重构_改善既有代码的设计(第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个需要相关的名词。
第四章构筑测试体系,向我们强调了重构时,构建一个完美的测试体系的重要性,并且如何去编写一个好的测试类。
第五章介绍重构名录,为之后介绍各类重构手法做一个铺垫,之后介绍其余的重构手法时,都按照这样的结构来介绍,名称、速写、动机、做法和范例。
有兴趣的可以去详细看书中的内容,这里就直接开始第六章的内容了。

2.1 提炼函数

个人理解:
将一些重复使用、之后改动可能较为频繁、方便将来可以快速的了解当前代码的代码片段进行提取,封装到一个函数中去。
书中范例:

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}`);	}}

做法:

  • 创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而 不是以它“怎样做”命名)。
  • 将待提炼的代码从源函数复制到新建的目标函数中。
  • 仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的 新函数中访问不到的变量。若是,以参数的形式将它们传递给新函数。
  • 所有变量都处理完之后,编译。
  • 在源函数中,将被提炼代码段替换为对目标函数的调用。
  • 测试。
  • 查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以 函数调用取代内联代码(222)令其调用提炼出的新函数。

请看下列函数:

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());    }

2.2 内联函数

动机:
如果代码中有太多间接层,使得系统中的所有函数都似乎只是对另一个函数 的简单委托,造成我在这些委托动作之间晕头转向,那么我通常都会使用内联函 数。当然,间接层有其价值,但不是所有间接层都有价值。通过内联手法,我可 以找出那些有用的间接层,同时将无用的间接层去除。

范例:

 function getRating(driver) {   return moreThanFiveLateDeliveries(driver) ? 2 : 1; } function moreThanFiveLateDeliveries(driver) {    return driver.numberOfLateDeliveries > 5; }
function getRating(driver) {   return (driver.numberOfLateDeliveries > 5) ? 2 : 1;}

做法:

  • 检查函数,确定它不具多态性。
  • 找出这个函数的所有调用点。
  • 将这个函数的所有调用点都替换为函数本体。
  • 每次替换之后,执行测试。
  • 删除该函数的定义。

注:与提炼函数互为相反词,不做过多的演示

2.3 提炼变量

动机:
表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将 表达式分解为比较容易管理的形式。在面对一块复杂逻辑时,局部变量使我能给 其中的一部分命名,这样我就能更好地理解这部分逻辑是要干什么。

范例:

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);    }   }

这是对象带来的一大好处:它们提供了合适的上下文,方便分享相关的逻辑 和数据。在如此简单的情况下,这方面的好处还不太明显;但在一个更大的类当 中,如果能找出可以共用的行为,赋予它独立的概念抽象,给它起一个好名字, 对于使用对象的人会很有帮助。
注:该示例过于简单,不做代码翻译
做法:

  • 确认要提炼的表达式没有副作用。
  • 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。
  • 用这个新变量取代原来的表达式。
  • 测试。

2.4 内联变量

动机:
在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东 西。但有时候,这个名字并不比表达式本身更具表现力。还有些时候,变量可能 会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。

范例:

let basePrice = anOrder.basePrice;return (basePrice > 1000);
return anOrder.basePrice > 1000;

做法:

  • 检查确认变量赋值语句的右侧表达式没有副作用。
  • 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。(这是为了确保该变量只被赋值一次。)
  • 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
  • 测试。
  • 重复前面两步,逐一替换其他所有使用该变量的地方。
  • 删除该变量的声明点和赋值语句。
  • 测试。

2.5 改变函数声明

动机:
一个好名字能让我一眼 看出函数的用途,而不必查看其实现代码。有一个改进函数名字的好办法:先写一句注释描 述这个函数的用途,再把这句注释变成函数的名字。

范例:

function circum(radius) {...}
function circumference(radius) {...}

简单的做法:

  • 如果想要移除一个参数,需要先确定函数体内没有使用该参数。
  • 修改函数声明,使其成为你期望的状态。
  • 找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明。
  • 测试。
    最好能把大的修改拆成小的步骤,所以如果你既想修改函数名,又想添加参数,最好分成两步来做。(并且,不论何时,如果遇到了麻烦,请撤销修改,并 改用迁移式做法。)
    迁移式做法
  • 如果有必要的话,先对函数体内部加以重构,使后面的提炼步骤易于开展。
  • 使用提炼函数(106)将函数体提炼成一个新函数。
  • 如果提炼出的函数需要新增参数,用前面的简单做法添加即可。
  • 测试。
  • 对旧函数使用内联函数(115)。
  • 如果新函数使用了临时的名字,再次使用改变函数声明(124)将其改回原来的名字。
  • 测试。
    如果要重构一个已对外发布的API,在提炼出新函数之后,你可以暂停重构,将原来的函数声明为“不推荐使用”(deprecated),然后给客户端一点时间 转为使用新函数。等你有信心所有客户端都已经从旧函数迁移到新函数,再移除旧函数的声明。
function circum(radius) { 	return circumference(radius); } function circumference(radius) { 	return 2 * Math.PI * radius;  }

此时我要执行测试,然后对旧函数使用内联函数(115):找出所有调用旧 函数的地方,将其改为调用新函数。每次修改之后都可以执行测试,这样我就可 以小步前进,每次修改一处调用者。所有调用者都修改完之后,我就可以删除旧 函数。

2.6 封装变量

动机:
重构的作用就是调整程序中的元素。函数相对容易调整一些,因为函数只有 一种用法,就是调用。在改名或搬移函数的过程中,总是可以比较容易地保留旧 函数作为转发函数(即旧代码调用旧函数,旧函数再调用新函数)。这样的转发 函数通常不会存在太久,但的确能够简化重构过程。
数据就要麻烦得多,因为没办法设计这样的转发机制。如果我把数据搬走, 就必须同时修改所有引用该数据的代码,否则程序就不能运行。如果数据的可访 问范围很小,比如一个小函数内部的临时变量,那还不成问题。但如果可访问范 围变大,重构的难度就会随之增大,这也是说全局数据是大麻烦的原因。
封装数据的价值还不止于此。封装能提供一个清晰的观测点,可以由此监控 数据的变化和使用情况;我还可以轻松地添加数据被修改时的验证或后续逻辑。

范例:
下面这个全局变量中保存了一些有用的数据:
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中,我可以把变量和访问函数搬移到单独一个文件 中,并且只导出访问函数,这样就限制了变量的可见性。

做法:

  • 创建封装函数,在其中访问和更新变量值。
  • 执行静态检查。
  • 逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后, 执行测试。
  • 限制变量的可见性。
  • 测试。

2.7 变量改名

动机:
好的命名是整洁编程的核心。变量可以很好地解释一段程序在干什么——如 果变量名起得好的话。但我经常会把名字起错——有时是因为想得不够仔细,有时是因为我对问题的理解加深了,还有时是因为程序的用途随着用户的需求改变了。

范例:

 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;

有了这个副本,我就可以逐一修改引用旧常量的代码,使其引用新的常量。 全部修改完成后,我会删掉旧的常量。我喜欢先声明新的常量名,然后把新常量 复制给旧的名字。这样最后删除旧名字时会稍微容易一点,如果测试失败,再把 旧常量放回来也稍微容易一点。

做法:

  • 如果变量被广泛使用,考虑运用封装变量(132)将其封装起来。
  • 找出所有使用该变量的代码,逐一修改。
  • 测试。

2.8 引入参数对象

范例:

function amountInvoiced(startDate, endDate) {...} function amountReceived(startDate, endDate) {...} function amountOverdue(startDate, endDate) {...}
function amountInvoiced(aDateRange) {...} function amountReceived(aDateRange) {...} function amountOverdue(aDateRange) {...}

动机:
我常会看见,一组数据项总是结伴同行,出没于一个又一个函数。这样一组 数据就是所谓的数据泥团,我喜欢代之以一个数据结构。
将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得明晰。使用新的数据结构,参数的参数列表也能缩短。并且经过重构之后,所有使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一 致性。
但这项重构真正的意义在于,它会催生代码中更深层次的改变。一旦识别出 新的数据结构,我就可以重组程序的行为来使用这些结构。我会创建出函数来捕 捉围绕这些数据的共用行为——可能只是一组共用的函数,也可能用一个类把数 据结构与使用数据的函数组合起来。这个过程会改变代码的概念图景,将这些数 据结构提升为新的抽象概念,可以帮助我更好地理解问题域。果真如此,这个重 构过程会产生惊人强大的效用——但如果不用引入参数对象开启这个过程,后面 的一切都不会发生。

做法:

  • 如果暂时还没有一个合适的数据结构,就创建一个。
  • 测试。
  • 使用改变函数声明(124)给原来的函数新增一个参数,类型是新建的数据结构。
  • 测试。
  • 调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试。
  • 用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删 除原来的参数。测试。

详细示例:
下面要展示的代码会查看一组温度读数(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()));    }

2.9 函数组合成类

范例:

function base(aReading) {...} function taxableCharge(aReading) {...}function calculateBaseCharge(aReading) {...}
class Reading { 	base() {...} 	taxableCharge() {...}	calculateBaseCharge() {...} }

动机:
如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数 传递给函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一 个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调 用,并且这样一个对象也可以更方便地传递给系统的其他部分。
除了可以把已有的函数组织起来,这个重构还给我们一个机会,去发现其他的计算逻辑,将它们也重构到新的类当中。

做法:

  • 运用封装记录(162)对多个函数共用的数据记录加以封装。
    如果多个函数共用的数据还未组织成记录结构,则先运用引入参数对象 (140)将其组织成记录。
  • 对于使用该记录结构的每个函数,运用搬移函数(198)将其移入新类。
    如果函数调用时传入的参数已经是新类的成员,则从参数列表中去除之。
  • 用以处理该数据记录的逻辑可以用提炼函数(106)提炼出来,并移入新类。

详细示例:
我在英格兰长大,那是一个热爱喝茶的国度。(个人而言,我不喜欢在英格 兰喝到的大部分茶,对中国茶和日本茶倒是情有独钟。)所以,我虚构了一种用 于向老百姓供给茶水的公共设施。每个月会有软件读取茶水计量器的数据,得到 类似这样的读数(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();    }

2.10 函数组合成变换

范例:

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),后者的做法是先用源 数据创建一个类,再把相关的计算逻辑搬移到类中。这两个重构手法都很有用,
我常会根据代码库中已有的编程风格来选择使用其中哪一个。不过,两者有一个 重要的区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。

做法:

  • 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
    这一步通常需要对输入的记录做深复制(deep copy)。此时应该写个测试,确保变换不会修改原来的记录。
  • 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录 中。修改客户端代码,令其使用这个新字段。
    如果计算逻辑比较复杂,先用提炼函数(106)提炼之。
  • 测试。
  • 针对其他相关的计算逻辑,重复上述步骤。

详细说明:
该示例和组合合成类的示例一致,只不过将所有的计算值的操作,放在了克隆源数据的方法中,不建议使用该方法,容易导致数据混乱,不过多介绍。

书中添加的方法

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接口

2.11 拆分阶段

范例:

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]; }

动机:
每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的 模块,因为这样到了需要修改的时候,我就可以单独处理每个主题,而不必同时 在脑子里考虑两个不同的主题。如果运气够好的话,我可能只需要修改其中一个 模块,完全不用回忆起另一个模块的诸般细节。
最简洁的拆分方法之一,就是把一大段行为分成顺序执行的两个阶段。可能
你有一段处理逻辑,其输入数据的格式不符合计算逻辑的要求,所以你得先对输 入数据做一番调整,使其便于处理。也可能是你把数据处理逻辑分成顺序执行的 多个步骤,每个步骤负责的任务全然不同。

做法:

  • 将第二阶段的代码提炼成独立的函数。(拆分出来的代码)
  • 测试。
  • 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
  • 测试。
  • 逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,就将其移入中转数据结构。每次搬移之后都要执行测试。
    有时第二阶段根本不应该使用某个参数。果真如此,就把使用该参数得到 的结果全都提炼成中转数据结构的字段,然后用搬移语句到调用者(217)把 使用该参数的代码行搬移到“第二阶段函数”之外。
  • 对第一阶段的代码运用提炼函数(106),让提炼出的函数返回中转数据结构。
    也可以把第一阶段提炼成一个变换(transform)对象。

详细示例:
我手上有一段“计算订单价格”的代码,至于订单中的商品是什么,我们从代 码中看不出来,也不太关心。

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;    }

第三章 封装

3.1 封装记录

书中范例:

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更显得心有余而力不足。

3.2 封装集合

范例:

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));        }

3.3 以对象取代基本类型

范例:

orders.filter(o => "high" === o.priority || "rush" === o.priority);
orders.filter(o => o.priority.higherThan(new Priority("normal")))

动机:
开发初期,你往往决定以简单的数据项表示简单的情况,比如使用数字或字 符串等。但随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。 比如说,一开始你可能会用一个字符串来表示“电话号码”的概念,但是随后它又 需要“格式化”“抽取区号”之类的特殊行为。这类逻辑很快便会占领代码库,制造出许多重复代码,增加使用时的成本。
一旦我发现对某个数据的操作不仅仅局限于打印时,我就会为它创建一个新 类。一开始这个类也许只是简单包装一下简单类型的数据,不过只要类有了,日后添加的业务逻辑就有地可去了。

做法:

  • 如果变量尚未被封装起来,先使用封装变量(132)封装它。
  • 为这个数据值创建一个简单的类。类的构造函数应该保存这个数据值,并为它 提供一个取值函数。
  • 执行静态检查。
  • 修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果有必要的话,同时修改字段的类型声明。
  • 修改取值函数,令其调用新类的取值函数,并返回结果。
  • 测试。
  • 考虑对第一步得到的访问函数使用函数改名(124),以便更好反映其用途。
  • 考虑应用将引用对象改为值对象(252)或将值对象改为引用对象(256),明 确指出新对象的角色是值对象还是引用对象。

3.4 以查询取代临时变量

范例:

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();    }

3.5 提炼类

范例:

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;} }

动机:
设想你有一个维护大量函数和数据的类。这样的类往往因为太大而不易理 解。此时你需要考虑哪些部分可以分离出去,并将它们分离到一个独立的类中。 如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依, 这就表示你应该将它们分离出去。一个有用的测试就是问你自己,如果你搬移了 某些字段和函数,会发生什么事?其他字段和函数是否因此变得无意义?

3.6 内联类

范例:

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)相反。如果一个类不再承担足够责任,不再有 单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),我就会 挑选这一“萎缩类”的最频繁用户(也是一个类),以本手法将“萎缩类”塞进另一 个类中。

3.7 隐藏委托关系

范例:

manager = aPerson.department.manager;
manager = aPerson.manager;class Person {   get manager() {return this.department.manager;}}

动机:
一个好的模块化的设计,“封装”即使不是其最关键特征,也是最关键特征之 一。“封装”意味着每个模块都应该尽可能少了解系统的其他部分。如此一来,一 旦发生变化,需要了解这一变化的模块就会比较少——这会使变化比较容易进行。
如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调 用后者的函数,那么客户就必须知晓这一层委托关系。万一受托类修改了接口, 变化会波及通过服务对象使用它的所有客户端。我可以在服务对象上放置一个简 单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即使将来 委托关系发生变化,变化也只会影响服务对象,而不会直接波及所有客户端。
简单来说,将委托关系封装起来,如果修改,只需修改一处地方。

3.8 移除中间人

范例:

manager = aPerson.manager; class Person {   get manager() {return this.department.manager;}}
manager = aPerson.department.manager;

动机:
在隐藏委托关系(189)的“动机”一节中,我谈到了“封装受托对象”的好处。 但是这层封装也是有代价的。每当客户端要使用受托类的新特性时,你就必须在 服务端添加一个简单委托函数。随着受托类的特性(功能)越来越多,更多的转 发函数就会使人烦躁。服务类完全变成了一个中间人(81),此时就应该让客户 直接调用受托类。(这个味道通常在人们狂热地遵循迪米特法则时悄然出现。我 总觉得,如果这条法则当初叫作“偶尔有用的迪米特建议”,如今能少很多烦恼)
我可以混用两种用法。有些委托关系非常常用,因此我想保住它们,这样可 使客户端代码调用更友好。何时应该隐藏委托关系,何时应该移除中间人,对我 而言没有绝对的标准——代码环境自然会给出该使用哪种手法的线索,具备思考 能力的程序员应能分辨出何种手法更佳。

3.9 替换算法

范例:

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);

第四章 搬移特性

4.1 搬移函数

范例:

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;        }    }

4.2 搬移字段

范例:

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;}}

动机:
如果我发现数据结构已经不适应于需求,就应该马上修缮它。如果容许瑕疵 存在并进一步累积,它们就会经常使我困惑,并且使代码愈来愈复杂。
我开始寻思搬移数据,可能是因为我发现每当调用某个函数时,除了传入一个记录参数,还总是需要同时传入另一条记录的某个字段一起作为参数。总是一 同出现、一同作为函数参数传递的数据,最好是规整到同一条记录中,以体现它 们之间的联系。
修改的难度也是引起我注意的一个原因,如果修改一条记录时, 总是需要同时改动另一条记录,那么说明很可能有字段放错了位置。此外,如果 我更新一个字段时,需要同时在多个结构中做出修改,那也是一个征兆,表明该 字段需要被搬移到一个集中的地点,这样每次只需修改一处地方。

4.3 搬移语句到函数

范例:

 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");    }

4.4 搬移语句到调用者

范例:

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)手法,先将表 现不同的行为调整到函数的开头或结尾,再使用本手法将语句搬移到其调用点。 只要差异代码被搬移到调用点,我就可以根据需要对其进行修改。

4.5 以函数调用取代内联代码

示例:

let appliesToMass = false; for(const s of states) {   if (s === "MA")     appliesToMass = true;}
appliesToMass = states.includes("MA");

动机:
善用函数可以帮助我将相关的行为打包起来,这对于提升代码的表达力大有 裨益—— 一个命名良好的函数,本身就能极好地解释代码的用途,使读者不必了 解其细节。函数同样有助于消除重复,因为同一段代码我不需要编写两次,每次 调用一下函数即可。此外,当我需要修改函数的内部实现时,也不需要四处寻找 有没有漏改的相似代码。(当然,我可能需要检查函数的所有调用点,判断它们 是否都应该使用新的实现,但通常很少需要这么仔细,即便需要,也总好过四处 寻找相似代码。)
如果我见到一些内联代码,它们做的事情仅仅是已有函数的重复,我通常会 以一个函数调用取代内联代码。但有一种情况需要特殊对待,那就是当内联代码 与函数之间只是外表相似但其实并无本质联系时。

4.6 移动语句

示例:

const pricingPlan = retrievePricingPlan(); cost order = retreiveOrder();let charge;const chargePerUnit = pricingPlan.unit;
const pricingPlan = retrievePricingPlan();const chargePerUnit = pricingPlan.unit; const order = retreiveOrder(); let charge;

动机:
让存在关联的东西一起出现,可以使代码更容易理解。如果有几行代码取用 了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据 结构的代码中间。最简单的情况下,我只需使用移动语句就可以让它们聚集起 来。此外还有一种常见的“关联”,就是关于变量的声明和使用。有人喜欢在函数 顶部一口气声明函数用到的所有变量,我个人则喜欢在第一次需要使用变量的地 方再声明它。

4.7 拆分循环

示例:

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节里所明确指出的一致:先进行重构,然后再进行性 能优化。我得先让代码结构变得清晰,才能做进一步优化;如果重构之后该循环 确实成了性能的瓶颈,届时再把拆开的循环合到一起也很容易。但实际情况是, 即使处理的列表数据更多一些,循环本身也很少成为性能瓶颈,更何况拆分出循 环来通常还使一些更强大的优化手段变得可能。

4.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());    }

第五章 重新组织数据

5.1 拆分变量

范例:

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);

动机:
除了这两种情况,还有很多变量用于保存一段冗长代码的运算结果,以便稍 后使用。这种变量应该只被赋值一次。如果它们被赋值超过一次,就意味它们在 函数中承担了一个以上的责任。如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情,会令代码阅读者糊涂。

5.2 字段改名

范例:

class Organization {   get name() {...}}
class Organization {   get title() {...} }

动机:
记录结构中的字段可能需要改名,类的字段也一样。在类的使用者看来,取 值和设值函数就等于是字段。对这些函数的改名,跟裸记录结构的字段改名一样重要。

5.3 以查询取代派生变量

范例:

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;}

动机:
可变数据是软件中最大的错误源头之一。对数据的修改常常导致代码的各个 部分以丑陋的形式互相耦合:在一处修改数据,却在另一处造成难以发现的破 坏。很多时候,完全去掉可变数据并不现实,但我还是强烈建议:尽量把可变数 据的作用域限制在最小范围。
有些变量其实可以很容易地随时计算出来。如果能去掉这些变量,也算朝着 消除可变性的方向迈出了一大步。计算常能更清晰地表达数据的含义,而且也避 免了“源数据修改时忘了更新派生变量”的错误。

5.4 将引用对象改为值对象

范例:

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

第六章 简单条件逻辑

6.1 分解条件表达式

范例:

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();    }

6.2 合并条件表达式

范例:

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语句用逻辑与来合并。

6.3 以卫语句取代嵌套条件表达式

范例:

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();

6.4 以多态取代条件表达式

范例:

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);    }

第七章 重构API

7.1 将查询函数和修改函数分离

范例:

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)); }

动机:
如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很 有价值的东西。我可以任意调用这个函数,也可以把调用动作搬到调用函数的其 他地方。这种函数的测试也更容易。简而言之,需要操心的事情少多了。

7.2 函数参数化

范例:

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;    }

7.3 移除标记参数

范例:

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到底是什么意思。如果明确用一个函数来完 成一项单独的任务,其含义会清晰得多。

7.4 以工厂函数取代构造函数

范例:

leadEngineer = new Employee(document.leadEngineer, 'E');
leadEngineer = createEngineer(document.leadEngineer)

动机:
很多面向对象语言都有特别的构造函数,专门用于对象的初始化。需要新建 一个对象时,客户端通常会调用构造函数。但与一般的函数相比,构造函数又常 有一些丑陋的局限性。例如,Java的构造函数只能返回当前所调用类的实例,也 就是说,我无法根据环境或参数信息返回子类实例或代理对象;构造函数的名字 是固定的,因此无法使用比默认名字更清晰的函数名;构造函数需要通过特殊的 操作符来调用(在很多语言中是new关键字),所以在要求普通函数的场合就难以使用。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
重构指北——《重构,改善既有代码设计》精读
如何写出整洁的代码——技巧与最佳实践
C语言运行时库详解
C++类使用默认构造函数时各数据成员的初始化
嵌入式程序设计的相关编写规范
提高程序运行效率的10个简单方法
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服