重构_改善既有代码的设计_第二版(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 credits
volumeCredits += Math.max(perf.audience - 30, 0);
// add extra credit for every ten comedy attendees
if ("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关键字),所以在要求普通函数的场合就难以使用。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页