标题:【读书笔记】《重构_改善既有代码的设计》24种代码的坏味道
时间:2024.01.11
作者:耿鬼不会笑
描述:改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能潜藏着更深的设计问题。
优化:
(1)改变函数声明
(2)变量改名
(3)字段改名
样例:
public static double countOrder(Order order) {
double basePrice = order.getQuantity() * order.getItemPrice();
double quantityDiscount = Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
double shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
说明: 函数名 countOrder 的第一感觉不太清晰,无法确认函数的作用?统计订单?订单商品数量吗?还是统计什么?但是通过函数的实现可以确认,这是个统计订单总价格的函数。
修改:
public static double getPrice(Order order) {
...
}
描述:一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”
优化:
(1)提炼函数
(2)如果重复代码只是相似而不是完全相同,请首先尝试用移动语句
重组代码顺序,把相似的部分放在一起以便提炼
(3)如果重复的代码段位于同一个超类的不同子类中,可以使用函数上移
来避免在两个子类之间互相调用
样例:
public String renderPerson(Person person) {
List<String> result = new ArrayList<>();
result.add("<p>" + person.getName() + "</p>");
result.add("<p>title: " + person.getPhoto().getTitle() + "</p>");
result.add(emitPhotoData(person.getPhoto()));
return String.join("\n", result);
}
public String photoDiv(Photo photo) {
List<String> result = new ArrayList<>();
result.add("<div>");
result.add("<p>title: " + photo.getTitle() + "</p>");
result.add(emitPhotoData(photo));
result.add("</div>");
return String.join("\n", result);
}
public String emitPhotoData(Photo aPhoto) {
List<String> result = new ArrayList<>();
result.add("<p>location: " + aPhoto.getLocation() + "</p>");
result.add("<p>date: " + aPhoto.getDate() + "</p>");
return String.join("\n", result);
}
renderPerson方法 和 photoDiv 中有一个同样的实现,那就是渲染 photo.title 的部分。这一部分的逻辑总是在执行 emitPhotoData 函数的前面,这是一段重复代码。
修改:
public String renderPerson(Person person) {
List<String> result = new ArrayList<>();
result.add("<p>" + person.getName() + "</p>");
result.add(emitPhotoData(person.getPhoto()));
return String.join("\n", result);
}
public String photoDiv(Photo photo) {
List<String> result = new ArrayList<>();
result.add("<div>");
result.add(emitPhotoData(photo));
result.add("</div>");
return String.join("\n", result);
}
public String emitPhotoData(Photo aPhoto) {
List<String> result = new ArrayList<>();
result.add("<p>title: " + aPhoto.getTitle() + "</p>");
result.add("<p>location: " + aPhoto.getLocation() + "</p>");
result.add("<p>date: " + aPhoto.getDate() + "</p>");
return String.join("\n", result);
}
描述:函数越长,就越难理解。如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。让小函数易于理解的关键还是在于良好的命名。如果你能给函数起个好名字,阅读代码的人就可以通过名字了解函数的作用,根本不必去看其中写了些什么,这可以节约大量的时间。条件表达式和循环常常也是提炼的信号。
优化:
(1)提炼函数
(2)如果提炼函数时会把许多参数传递给被提炼出来的新函数,可以经常运用以查询取代临时变量
来消除这些临时元素
(3)引入参数对象
和保持对象完整
则可以简化过长的参数列表
(4)仍然有太多临时变量和参数时,应该考虑使用以命令取代函数
(5)使用分解条件表达式
处理条件表达式
(6)对于庞大的switch语句,其中的每个分支都应该通过 提炼函数
变成独立的函数调用
(7)如果有多个switch语句基于同一个条件 进行分支选择,就应该使用以多态取代条件表达式
。
(8)对于循环,应该将循环和循环内的代码提炼到一个独立的函数中
(9)如果提炼出的循环很难命名,可能是因为其中做了几件不同的事,此时应使用拆分循环
将其拆分成各自独立的任务
样例:
public static void printOwing(Invoice invoice) {
double outstanding = 0;
System.out.println("***********************");
System.out.println("**** Customer Owes ****");
System.out.println("***********************");
// Calculate outstanding
List<Order> orders = invoice.getOrders();
for (Order o : orders) {
outstanding += o.getAmount();
}
// Record due date
Date today = new Date();
Date dueDate = new Date(today.getYear(), today.getMonth(), today.getDate() + 30);
invoice.setDueDate(dueDate);
// Print details
System.out.println("name: " + invoice.getCustomer());
System.out.println("amount: " + outstanding);
System.out.println("due: " + invoice.getDueDate());
}
函数体过长,进行函数的拆分,重构后的 printOwing 函数,简单的四行代码,清晰的描述了函数所做的事情
修改:
public static void printBanner() {
System.out.println("***********************");
System.out.println("**** Customer Owes ****");
System.out.println("***********************");
}
public static double calculateOutstanding(Invoice invoice) {
double outstanding = 0;
List<Order> orders = invoice.getOrders();
for (Order o : orders) {
outstanding += o.getAmount();
}
return outstanding;
}
public static void recordDueDate(Invoice invoice) {
Date today = new Date();
Date dueDate = new Date(today.getYear(), today.getMonth(), today.getDate() + 30);
invoice.setDueDate(dueDate);
}
public static void printDetails(Invoice invoice, double outstanding) {
System.out.println("name: " + invoice.getCustomer());
System.out.println("amount: " + outstanding);
System.out.println("due: " + invoice.getDueDate());
}
public static void printOwing(Invoice invoice) {
printBanner();
double outstanding = calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
描述:过长的参数列表本身令人迷惑
优化:
(1)如果可以向某个参数发起查询而获得另一个参数的值,那么就可以使用以查询取代参数
去掉这第二个参数
(2)如果正在从现有的数据结构中抽出很多数据项,就可以考虑使用保持对象完整
手法,直接传入原来的数据结构
(3)如果有几项参数总是同时出现,可以用引入参数对象
将其 合并成一个对象
(4)如果某个参数被用作区分函数行为的标记(flag),可以使用 移除标记参数
(5)如果多个函数有同样的几个参数,可以使用函数组合成类
,将这些共同的参数变成这个类的字段
样例:
public class PriceRangeFilter {
public static List<Product> filterPriceRange(List<Product> products, double min, double max, boolean isOutside) {
if (isOutside) {
return products.stream()
.filter(product -> product.getPrice() < min || product.getPrice() > max)
.collect(Collectors.toList());
} else {
return products.stream()
.filter(product -> product.getPrice() > min && product.getPrice() < max)
.collect(Collectors.toList());
}
}
@Data
static class Product {
private double price;
}
public static List<Product> outsidePriceProducts(){
filterPriceRange(
List.of(new Product(/* ... */), new Product(/* ... */)),
1.0,
10.0,
true
)
}
public static List<Product> insidePriceProducts(){
filterPriceRange(
List.of(new Product(/* ... */), new Product(/* ... */)),
5.0,
8.0,
false
)
}
}
filterPriceRange
是过滤商品的函数,仔细看的话会发现,主要比对的是 product.price
字段和传入的参数 min
与 max
之间的大小对比关系。如果 isOutSide
为 true
的话,则过滤出价格区间之外的商品,否则过滤出价格区间之内的商品。isOutSide
作为标记参数,因为它们让人难以理解到底有哪些函数可以调用、应该怎么调用。使用这样的函数,我还得弄清标记参数有哪些可用的值。尽管priceOutSideRange
和 priceInsideRange
的函数命名已经足够清晰,但是内部对 range
范围的判定还是需要花费一定时间理解,而 range
作为我们刚识别出来的一种结构,可以进行重构。
修改:
public class PriceRangeFilter {
public static List<Product> filterPriceOutsideRange(List<Product> products, Range range) {
return products.stream()
.filter(product -> range.outside(product.getPrice()))
.collect(Collectors.toList());
}
public static List<Product> filterPriceInsideRange(List<Product> products, Range range) {
return products.stream()
.filter(product -> range.inside(product.getPrice()))
.collect(Collectors.toList());
}
@Data
public static class Range {
private double min;
private double max;
public boolean outside(double num) {
return num < min || num > max;
}
public boolean inside(double num) {
return num > min && num < max;
}
}
@Data
public static class Product {
private double price;
}
public static List<Product> outsidePriceProducts(){
filterPriceRange(
List.of(new Product(/* ... */), new Product(/* ... */)),
new Range(1, 10)
)
}
public static List<Product> insidePriceProducts(){
filterPriceRange(
List.of(new Product(/* ... */), new Product(/* ... */)),
new Range(5, 8)
)
}
}
描述:全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机 制可以探测出到底哪段代码做出了修改。全局数据最显而易见的形式就是全局变量,但类变量和单例(singleton)也有这样的问题。
优化:
(1)封装变量
,把全局数据用一个函数包装起来,对于修改它的地方,控制对它的访问。
(2)最好将这个函数(及其封装的数据)搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域。
样例:
public class Global {
public static String platform = "pc";
public static String token = "";
}
这个 Global.java
用来提供全局数据的,不能修改为别的值,不然后序的判断逻辑中就会报错,而且没有任何机制可以探测出到底哪段代码做出了修改,将其用一个函数包装起来,至少看见修改它的地方,并开始控制对它的访问。
修改:
public class Global {
private static String platform = "pc";
private static String token = "";
public static String getPlatform() {
return platform;
}
public static String getToken() {
return token;
}
public static void setPlatform(String newPlatform) {
platform = newPlatform;
}
public static void setToken(String newToken) {
token = newToken;
}
}
描述:在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据,于是一个功能失效了,且要找出故障原因就会更加困难
优化:
(1)用封装变量
来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进
(2)如果一个变量在不同时候被用于存储不同的东西, 可以使用拆分变量
将其拆分为各自不同用途的变量,从而避免危险的更新操作
(3)使用移动语句
和提炼函数
尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开
(4)设计API时,可以使用将查询函数和修改函数分离
确保调用者不会调到有副作用的代码,除非他们真的需要更新数据
(5)尽早使用移除设值函数
——把设值函数的使用者找出来,缩小变量作用域
(6)如果可变数据的值能在其他地方计算出来,则使用以查询取代派生变量
消除这种坏味道
(7)如果变量作用域只有几行代码,即使其中的数据可变,也不是什么大问题; 但随着变量作用域的扩展,风险也随之增大。用函数组合成类
或者 函数组合成变换
来限制需要对变量进行修改的代码量
(8)如果一个变量在其内部结构中包含了数据,通常最好不要直接修改其中的数据,而是用将引用对象改为值对象
令其直接替换整个数据结构
(9)如果要更新一个数据结构,可以就返回一份新的数据副本,旧的数据仍保持不变
样例:
public static Map<K, V> merge(Map<K, V> target, Map<K, V> source) {
for (Map.Entry<K, V> entry : source.entrySet()) {
target.put(entry.getKey(), entry.getValue());
}
return target;
}
对源对象进行了修改调整,从而影响了源对象的值,当使用到源对象时,可能会因为取到错误的数据
修改:
public static Map<K, V> merge(Map<K, V> target, Map<K, V> source) {
Map<K, V> mergedMap = new HashMap<>(target);
mergedMap.putAll(source);
return mergedMap;
}
说明:如果某个模块经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了。例如:如果新加入一个数据库,我必须修改这3个函数;如果新出现一种金融工具,我必须修改这4个函数。这就是发散式变化的征兆。数据库交互和金融逻辑处理是两个不同的上下文,将它们分别搬移到各自独立的模块中。每当要对某个上下文做修改时,我们只需要理 解这个上下文,而不必操心另一个。
优化:
(1)如果发生变化的两个方向自然地形成了先后次序(比如说,先从数据库取出数据,再对其进行金融逻辑处理),就可以用拆分阶段
将两者分开,两者之间通过一个清晰的数据结构进行沟通。
(2)如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后用搬移函数
把处理逻辑分开。
(3)如果函数内部混合了两类处理逻辑,应该先用提炼函数
将其分开,然后再做搬移。
(4)如果模块是以类的形式定义的,就可以用提炼类
来做拆分。
样例:
public static double getPrice(Order order) {
double basePrice = order.getQuantity() * order.getItemPrice();
double quantityDiscount = Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
double shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
这个函数的职责就是计算基础价格 - 数量折扣 + 运费。如果基础价格计算规则改变,需要修改这个函数,如果折扣规则发生改变也需要修改这个函数,同理,运费计算规则也会引发它的改变。
优化:
public static double calculateBasePrice(Order order) {
return order.getQuantity() * order.getItemPrice();
}
public static double calculateDiscount(Order order) {
return Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
}
public static double calculateShipping(double basePrice) {
return Math.min(basePrice * 0.1, 100);
}
public static double getPrice(Order order) {
double basePrice = calculateBasePrice(order);
double discount = calculateDiscount(order);
double shipping = calculateShipping(basePrice);
return basePrice - discount + shipping;
}
说明:霰弹式修改类似于发散式变化,但又恰恰相反。如果每遇到某种变化,都必须在许多不同的类内做出许多小修改,这就是霰弹式修改。如果需要修改的代码散布四处,你不但很难找到它们,也很容易错过某个重要的修改。
优化:
(1)使用搬移函数
和搬移字段
把所有需要修改的代码放进同一个模块里。
(2)如果有很多函数都在操作相似的数据,可以使用函数组合成类
。
(3)如果有些函数的功能是转化或者充实数据结构,可以使用函数组合成变换
。
(4)如果一些函数的输出可以组合后提供给一段专门使用 这些计算结果的逻辑,这种时候常常用得上拆分阶段
。
(5)使用与内联(inline)相关的重构,如内联函数
或是内联类
,把本不该分散的逻辑拽回一处。
// File Reading
@Data
public static class Reading {
private String customer;
private int quantity;
private int month;
private int year;
}
public static Reading acquireReading() {
return new Reading("ivan",10,5,2017);
}
// File 1
Reading aReading1 = acquireReading();
double baseCharge1 = baseRate(aReading1.getMonth(), aReading1.getYear()) * aReading1.getQuantity();
// File 2
Reading aReading2 = acquireReading();
double base2 = baseRate(aReading2.getMonth(), aReading2.getYear()) * aReading2.getQuantity();
double taxableCharge = Math.max(0, base2 - taxThreshold(aReading2.getYear()));
public static double taxThreshold(int year) {
return ...;
}
// File 3
Reading aReading3 = acquireReading();
double basicChargeAmount = calculateBaseCharge(aReading3);
public static double calculateBaseCharge(Reading aReading) {
return baseRate(aReading.getMonth(), aReading.getYear()) * aReading.getQuantity();
}
如果 reading
的部分逻辑发生了改变,对这部分逻辑的修改需要跨越好几个文件调整。
@Data
public class Reading {
private String customer;
private int quantity;
private int month;
private int year;
public double getBaseRate(int month,int year) {
return ...;
}
public double getBaseCharge() {
return getBaseRate(this.month, this.year) * getQuantity();
}
public double getTaxableCharge() {
return Math.max(0, getBaseCharge() - getTaxThreshold());
}
public double getTaxThreshold() {
return ...;
}
public static void main(String[] args) {
Reading reading = new Reading();
reading.setCustomer("ivan");
reading.setQuantity(10);
reading.setMonth(5);
reading.setYear(2017);
double baseCharge = reading.getBaseCharge();
double taxableCharge = reading.getTaxableCharge();
}
}
说明:一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流
优化:
(1)使用搬移函数
把相关代码移过去
(2)如果函数中只有一部分受这种依恋之苦,这时候应该使用提炼函数
把这一部分提炼到独立的函数中,再使用搬移函数
。
(3)如果一个函数往往会用到几个模块的功能,首先要判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。若先以提炼函数
将这个函数分解为数个较小的函数并分别置放于不同地点,上述步骤也就比较容易完成了。
@Data
public class Account {
private String name;
private AccountType type;
public int getLoanAmount() {
if (getType().getType().equals("vip")) {
return 20000;
} else {
return 10000;
}
}
public static void main(String[] args) {
AccountType vipType = new AccountType("vip");
Account account = new Account();
account.setName("John Doe");
account.setType(vipType);
int loanAmount = account.getLoanAmount();
}
}
@Data
public class AccountType {
private String type;
}
这段代码是账户 Account
和账户类型 AccountType
,如果账户的类型是 vip
,贷款额度 loanAmount
就有 20000,否则就只有 10000。在获取贷款额度时,Account
内部的 loanAmount
方法和另一个类 AccountType
的内部数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。
@Data
public class Account {
private String name;
private AccountType type;
public int getLoanAmount() {
return getType().getLoanAmount();
}
public static void main(String[] args) {
AccountType vipType = new AccountType("vip");
Account account = new Account();
account.setName("John Doe");
account.setType(vipType);
int loanAmount = account.getLoanAmount();
}
}
@Data
public class AccountType {
private String type;
public int getLoanAmount() {
if (getType().equals("vip")) {
return 20000;
} else {
return 10000;
}
}
}
说明:
两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。
优化:
(1)运用提炼类
将它们提炼到一个独立对象中。
(2)对于函数签名,运用引入参数对象
或保持对象完整
为它瘦身。
@Data
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getTelephoneNumber() {
return "(" + getOfficeAreaCode() + ") " + getOfficeNumber();
}
public static void main(String[] args) {
Person person = new Person();
person.setName("jack");
person.setOfficeAreaCode("+86");
person.setOfficeNumber("18726182811");
System.out.println("person's name is " + person.getName() + ", telephoneNumber is " + person.getTelephoneNumber());
// person's name is jack, telephoneNumber is (+86) 18726182811
}
}
这个 Person 类记录了用户的名字(name),电话区号(officeAreaCode)和电话号码(officeNumber),如果我把 officeNumber
字段删除,那 officeAreaCode
就失去了意义。这说明这两个字段总是一起出现的,除了 Person
类,其他用到电话号码的地方也是会出现这两个字段的组合
@Data
public class Person {
private String name;
private TelephoneNumber telephoneNumber;
public String getTelephoneNumber() {
return telephoneNumber.toString();
}
public String getOfficeAreaCode() {
return telephoneNumber.getAreaCode();
}
public void setOfficeAreaCode(String arg) {
this.telephoneNumber = new TelephoneNumber(arg, getOfficeNumber());
}
public String getOfficeNumber() {
return telephoneNumber.getNumber();
}
public void setOfficeNumber(String arg) {
this.telephoneNumber = new TelephoneNumber(getOfficeAreaCode(), arg);
}
public static void main(String[] args) {
//
Person person = new Person("John");
person.setOfficeAreaCode("+86");
person.setOfficeNumber("18726182811");
System.out.println("Person's name is " + person.getName() + ", telephoneNumber is " + person.getTelephoneNumber());
// Person's name is John, telephoneNumber is (+86) 18726182811
}
}
@Data
public class TelephoneNumber {
private String areaCode;
private String number;
@Override
public String toString() {
return "(" + getAreaCode() + ") " + getNumber();
}
}
说明:很多程序员不愿意 创建对自己的问题域有用的基本类型,如钱、坐标、范围等。于是,我们看到了把钱当作普通数字来计算的情况、计算物理量时无视单位(如把英寸与毫米相 加)的情况以及大量类似if (a < upper && a > lower)这样的代码。
优化:
(1)以对象取代基本类型
将原本单独存在的数据值替换为对象。
(2)如果想要替换的数据值是 控制条件行为的类型码,则可以运用以子类取代类型码
加上以多态取代 条件表达式
的组合将它换掉。
(3)如果有一组总是同时出现的基本类型数据,这就是数据泥团的征兆,应该运用提炼类
和引入参数对象
来处理。
@Data
public class Product {
private String name;
private String price;
public String getPrice() {
return getPriceCount() + " " + getPriceSuffix();
}
public double getPriceCount() {
return Double.parseDouble(this.price.substring(1));
}
public String getPriceUnit() {
switch (this.price.charAt(0)) {
case '¥':
return "cny";
case '$':
return "usd";
case 'k':
return "hkd";
default:
throw new UnsupportedOperationException("Unsupported unit");
}
}
public double getPriceCnyCount() {
switch (this.getPriceUnit()) {
case "cny":
return this.getPriceCount();
case "usd":
return this.getPriceCount() * 7;
case "hkd":
return this.getPriceCount() * 0.8;
default:
throw new UnsupportedOperationException("Unsupported unit");
}
}
public String getPriceSuffix() {
switch (this.getPriceUnit()) {
case "cny":
return "元";
case "usd":
return "美元";
case "hkd":
return "港币";
default:
throw new UnsupportedOperationException("Unsupported unit");
}
}
}
这个 Product
(产品)类,price
字段作为一个基本类型,在 Product
类中被各种转换计算,然后输出不同的格式,Product
类需要关心 price
的每一个细节。在这里,应当为price
创建一个属于它自己的基本类型 Price
。同时,重构 Product
类,将原有跟 price
相关的逻辑,使用中间人委托来调用。
@Data
public class Product {
private String name;
private Price price;
}
@Data
public class Price {
private double count;
private String unit;
private double cnyCount;
private String suffix;
}
说明:很多语言支持更复杂的switch语句,而不只是根据基本类型值来做条件判 断。因此,我们现在更关注重复的switch:在不同的地方反复使用同样的switch 逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形 式)。重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有 的switch,并逐一更新。
优化:
(1)以多态取代条件表达式
样例:
@Data
public class Product {
private String name;
private String price;
public String getPrice() {
return getPriceCount() + " " + getPriceSuffix();
}
public double getPriceCount() {
return Double.parseDouble(this.price.substring(1));
}
public String getPriceUnit() {
switch (this.price.charAt(0)) {
case '¥':
return "cny";
case '$':
return "usd";
case 'k':
return "hkd";
default:
throw new UnsupportedOperationException("Unsupported unit");
}
}
public double getPriceCnyCount() {
switch (this.getPriceUnit()) {
case "cny":
return this.getPriceCount();
case "usd":
return this.getPriceCount() * 7;
case "hkd":
return this.getPriceCount() * 0.8;
default:
throw new UnsupportedOperationException("Unsupported unit");
}
}
public String getPriceSuffix() {
switch (this.getPriceUnit()) {
case "cny":
return "元";
case "usd":
return "美元";
case "hkd":
return "港币";
default:
throw new UnsupportedOperationException("Unsupported unit");
}
}
}
创建一个工厂函数,同时将 Product
类的实例方法也使用工厂函数创建
优化:
// Price.java
@Data
public abstract class Price {
protected String value;
public double getCount() {
return Double.parseDouble(this.value.substring(1));
}
}
// CnyPrice.java
@Data
public class CnyPrice extends Price {
public CnyPrice(String value) {
super(value);
}
@Override
public String getUnit() {
return "cny";
}
@Override
public double getCnyCount() {
return getCount();
}
@Override
public String getSuffix() {
return "元";
}
}
// UsdPrice.java
@Data
public class UsdPrice extends Price {
public UsdPrice(String value) {
super(value);
}
@Override
public String getUnit() {
return "usd";
}
@Override
public double getCnyCount() {
return getCount() * 7;
}
@Override
public String getSuffix() {
return "美元";
}
}
// HkdPrice.java
@Data
public class HkdPrice extends Price {
public HkdPrice(String value) {
super(value);
}
@Override
public String getUnit() {
return "hkd";
}
@Override
public double getCnyCount() {
return getCount() * 0.8;
}
@Override
public String getSuffix() {
return "港币";
}
}
// PriceFactory.java
public class PriceFactory {
public static Price createPrice(String value) {
switch (value.charAt(0)) {
case '¥':
return new CnyPrice(value);
case '$':
return new UsdPrice(value);
case 'k':
return new HkdPrice(value);
default:
throw new UnsupportedOperationException("Unsupported unit");
}
}
public static void main(String[] args) {
// Example usage
Price cnyPrice = PriceFactory.createPrice("¥50.0");
System.out.println("CNY Price: " + cnyPrice.toString());
System.out.println("CNY Count: " + cnyPrice.getCnyCount());
}
}
说明:如今循环已经有点儿过时,如今,如今越来越多的编程语言都提供了更好的语言结构来处理迭代过程,管道操作(如filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作。
优化:
(1)以管道取代循环
样例:
public static List<CityAreaCodeData> acquireCityAreaCodeData(String input, String country) {
String[] lines = input.split("\n");
boolean firstLine = true;
List<CityAreaCodeData> result = new ArrayList<>();
for (String line : lines) {
if (firstLine) {
firstLine = false;
continue;
}
if (line.trim().isEmpty()) {
continue;
}
String[] record = line.split(",");
if (record[1].trim().equals(country)) {
result.add(new CityAreaCodeData(record[0].trim(), record[2].trim()));
}
}
return result;
}
@Data
public static class CityAreaCodeData {
private String city;
private String phone;
}
Java提供了更好的语言结构来处理迭代过程,可以使用stream流来优化代码
修改:
public static List<CityData> acquireCityData(String input, String country) {
String[] lines = input.split("\n");
return Arrays.stream(lines)
.skip(1)
.filter(line -> !line.trim().isEmpty())
.map(line -> line.split(","))
.filter(record -> record[1].trim().equals(country))
.map(record -> new CityData(record[0].trim(), record[2].trim()))
.collect(Collectors.toList());
}
@Data
public static class CityData {
private String city;
private String phone;
}
说明:程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用,但有时我们真的不需要这层额外的结构。可能有这样一个函数,它的名字就跟实现代码看起来一模一样;也可能有这样一个类, 根本就是一个简单的函数。这可能是因为,起初在编写这个函数时,程序员也许 期望它将来有一天会变大、变复杂,但那一天从未到来;也可能是因为,这个类 原本是有用的,但随着重构的进行越变越小,最后只剩了一个函数。
优化:
(1)内联函数
(2)内联类
(3)如果这个类处于一个继承体系中,可以使用折叠继承体系
。
样例:
public List<String[]> reportLines(Customer aCustomer) {
List<String[]> lines = new ArrayList<>();
gatherCustomerData(lines, aCustomer);
return lines;
}
private void gatherCustomerData(List<String[]> out, Customer aCustomer) {
out.add(new String[]{"name", aCustomer.getName()});
out.add(new String[]{"location", aCustomer.getLocation()});
}
gatherCustomerData函数显得有点多余,函数逻辑可以直接与reportLines函数合并
优化:
public List<String[]> reportLines(Customer aCustomer) {
List<String[]> lines = new ArrayList<>();
lines.add(new String[]{"name", aCustomer.getName()});
lines.add(new String[]{"location", aCustomer.getLocation()});
return lines;
}
public List<String[]> reportLines(Customer aCustomer) {
return Arrays.asList(
new String[]{"name", aCustomer.getName()},
new String[]{"location", aCustomer.getLocation()}
);
}
说明:当有人说“噢,我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些 非必要的事情,这种坏味道就出现了。这么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,就值得那么做;如果用不到,就不值得。用不上的装置只会挡你的路。
优化:
(1)如果你的某个抽象类其实没有太大作用,请运用折叠继承体系
(2)不必要的委托可运用内联函数
和内联类
除掉。
(3)如果函数的某些参数未被用上,可以用改变函数声明
去掉这些参数。如果有并非真正需要、 只是为不知远在何处的将来而塞进去的参数,也应该用改变函数声明
去掉。
(4)如果函数或类的唯一用户是测试用例,可以先删掉测试用例,然后移除死代码
@Data
public class TrackingInformation {
private String shippingCompany;
private String trackingNumber;
}
@Data
public class Shipment {
private TrackingInformation trackingInformation;
}
说明:这个关于这两个物流的类,而 TrackingInformation
记录物流公司和物流单号,而 Shipment
只是使用 TrackingInformation
管理物流信息,并没有其他任何额外的工作。为什么用一个额外的 TrackingInformation
来管理物流信息,而不是直接用 Shipment
来管理呢?因为 Shipment
可能还会有其他的职责。但N年已经过去了,它还没有出现其他的职责。
优化:
@Data
public class Shipment {
private String shippingCompany;
private String trackingNumber;
}
说明:当某个类其内部某个字段仅为某种特定情况而设。这样的代码难以理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下也很难猜测当初设置它的目的。
优化:
(1)使用提炼类
为将其收拢到一个地方,然后用搬移函数
把所有和这些字段相关的代码放到一起统一管理。
(2)也许你还可以使用引入特例
在“变量不合法”的情况下创建一个替代对象,从而避免写出条件式代码。
样例:
@Data
public class Site {
private Customer customer;
}
@Data
public class Customer {
private String name;
private BillingPlan billingPlan;
private PaymentHistory paymentHistory;
}
@Data
public class BillingPlan {
......
}
@Data
public class PaymentHistory {
private int weeksDelinquentInLastYear;
}
public class Main {
public static void main(String[] args) {
//initial
Site site = xxx ;
// Client 1
Customer aCustomer = site.getCustomer();
String customerName = (aCustomer == null) ? "occupant" : aCustomer.getName();
System.out.println("Client 1: " + customerName);
// Client 2
BillingPlan plan = (aCustomer == null) ? Registry.getBillingPlans().get("basic") : aCustomer.getBillingPlan();
System.out.println("Client 2: " + plan);
// Client 3
if (aCustomer != null) {
BillingPlan newPlan = new BillingPlan();
aCustomer.setBillingPlan(newPlan);
}
// Client 4
int weeksDelinquent = (aCustomer == null) ? 0 : aCustomer.getPaymentHistory().getWeeksDelinquentInLastYear();
System.out.println("Client 4: " + weeksDelinquent);
}
}
这一段代码是,我们的线下商城服务点,在老客户搬走新客户还没搬进来的时候,会出现暂时没有客户的情况。在每个查询客户信息的地方,都需要判断这个服务点有没有客户,然后再根据判断来获取有效信息。aCustomer === 'unknown'
这是个特例情况,在这个特例情况下,就会使用到很多临时字段,或者说是特殊值字段。这种重复的判断不仅会来重复代码的问题,也会非常影响核心逻辑的代码可读性,造成理解的困难。这里,要把所有的重复判断逻辑都移除掉,保持核心逻辑代码的纯粹性。然后,要把这些临时字段收拢到一个地方,进行统一管理。
优化:
public class NullCustomer extends Customer {
public NullCustomer() {
super(new CustomerData("occupant", new BillingPlan(0, 0), new PaymentHistory(0)));
}
}
// Initial
Site site = (customer==null) ? new Site(new NullCustomer()) : new Site(new Customer(customer));
// Client 1
Customer aCustomer = site.getCustomer();
String customerName = aCustomer.getName();
// Client 2
BillingPlan plan = aCustomer.getBillingPlan();
// Client 3
......
// Client 4
int weeksDelinquent = aCustomer.getPaymentHistory().getWeeksDelinquentInLastYear();
说明:如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对 象,然后再请求另一个对象……这就是消息链。在实际代码中你看到的可能是一 长串取值函数或一长串临时变量。采取这种方式,意味客户端代码将与查找过程 中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出 相应修改。
优化:
(1)隐藏委托关系
(2)用提炼函数
把使用该对象的代码提炼到一个独立 的函数中,再运用搬移函数
把这个函数推入消息链
样例:
const result = a(b(c(1, d(f()))));
优化:
const result = goodNameFunc();
function goodNameFunc() {
return a(b(c(1, d(f()))));
}
说明:人们可能过度运用委托。你也许会看到某个类的接口有一半的函数都委托给其他类,这样就是过度运用。这时应该移除中间人,直接和真正负责的对象打交道。
优化:
(1)移除中间人
(2)如果这样“不干实事”的函数只有少数几个,可以运用内联函数
把它们放进调用端。
(3)如果这些中间人还有其他行为,可以运用以委托取代超类
或者以委托取代子类
把它变成真正的对象,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。
样例:
@Data
public class Product {
private String name;
private Price price;
public String get price() {
return this.price.toString();
}
public String get priceCount() {
return this.price.count;
}
public String get priceUnit() {
return this.price.unit;
}
public String get priceCnyCount() {
return this.price.cnyCount;
}
public String get priceSuffix() {
return this.price.suffix;
}
}
现在我要访问 Product
价格相关的信息,都是直接通过 Product
访问,而 Product
负责提供 price
的很多接口。随着 Price
类的新特性越来越多,更多的转发函数就会使人烦躁,而现在已经有点让人烦躁了。此时,这个 Product
类已经快完全变成一个中间人了,那我现在希望调用方应该直接使用 Price
类。
优化:
@Data
public class Product {
private String name;
private Price price;
public Price get price() {
return this.price();
}
}
说明:
软件开发者喜欢在模块之间建起高墙,极其反感在模块之间大量交换数据, 因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们 必须尽量减少这种情况,并把这种交换都放到明面上来。
优化:
(1)如果两个模块总是在咖啡机旁边窃窃私语,就应该用搬移函数
和搬移字段
减少它们的私下交流。
(2)如果两个模块有共同的兴趣,可以尝试再 新建一个模块,把这些共用的数据放在一个管理良好的地方;或者用隐藏委托关系
,把另一个模块变成两者的中介。
(3)如果子类对超类的了解总是超过后者的主观愿望,请运用以委托取代子类
或以委托取代超类
让它离开继承体系。
样例:
@Data
public class Person {
private String name;
private Department department;
}
@Data
public class Department {
private String code;
private Person manager;
}
在这个案例里,如果要获取 Person
的部门代码 code
和部门领导 manager
都需要先获取 Person.department
。这样一来,调用者需要额外了解 Department
的接口细节,如果 Department
类修改了接口,变化会波及通过 Person
对象使用它的所有客户端。
优化:
@Data
public class Person {
private String name;
private Department department;
public String getDepartmentCode() {
return department.getCode();
}
public void setDepartmentCode(String code) {
department.setCode(code);
}
public Person getManager() {
return department.getManager();
}
public void setManager(Person manager) {
department.setManager(manager);
}
}
@Data
public class Department {
private String code;
private Person manager;
}
说明:如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了。
优化:
(1)运用提炼类
将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。
(2)如果类内的数个变量有着相同的前缀或后缀,这就意味着有机会把它们提炼到某个组件内。如果这个组件适合作 为一个子类,你会发现提炼超类
或者以子类取代类型码
(其实就 是提炼子类)往往比较简单。
(3)观察一个大类的使用者,经常能找到如何拆分类的线索。看看使用者是否只 用到了这个类所有功能的一个子集,每个这样的子集都可能拆分成一个独立的 类。一旦识别出一个合适的功能子集,就试用提炼类
、提炼超类
或是以子类取代类型码
将其拆分出来。
样例:
@Data
public class Product {
private String name;
private String price;
public String getPrice() {
return getPriceCount() + " " + getPriceSuffix();
}
public double getPriceCount() {
return Double.parseDouble(this.price.substring(1));
}
public String getPriceUnit() {
...
}
public double getPriceCnyCount() {
...
}
public String getPriceSuffix() {
...
}
}
在 Product
类中就发现了三个坏味道:基本类型偏执、重复的 switch、中间人。在解决这三个坏味道的过程中,也把 过大的类
这个问题给解决了。
优化:
@Data
public abstract class Price {
protected String value;
public double getCount() {
return Double.parseDouble(this.value.substring(1));
}
}
@Data
public class CnyPrice extends Price {
public CnyPrice(String value) {
super(value);
}
......
}
@Data
public class UsdPrice extends Price {
public UsdPrice(String value) {
super(value);
}
......
}
@Data
public class HkdPrice extends Price {
public HkdPrice(String value) {
super(value);
}
......
}
public class PriceFactory {
public static Price createPrice(String value) {
switch (value.charAt(0)) {
case '¥':
return new CnyPrice(value);
case '$':
return new UsdPrice(value);
case 'k':
return new HkdPrice(value);
default:
throw new UnsupportedOperationException("Unsupported unit");
}
}
public static void main(String[] args) {
Price cnyPrice = PriceFactory.createPrice("¥50.0");
System.out.println("CNY Price: " + cnyPrice.toString());
System.out.println("CNY Count: " + cnyPrice.getCnyCount());
}
}
说明:
使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个 类。但只有当两个类的接口一致时,才能做这种替换。
优化:
(1)可以用改变函数声明
将函数签名变得一致。但这往往还不够,请反复运用搬移函数
将某些行为移入类中,直到两者的协议一致为止。
(2)如果搬移过程造成了重复代码, 或许可运用提炼超类
补偿一下。
样例:
@Data
public class Employee {
private int id;
private String name;
private double monthlyCost;
public double getAnnualCost() {
return monthlyCost * 12;
}
}
@Data
public class Department {
private String name;
private List<Employee> staff;
public double getTotalMonthlyCost() {
return staff.stream().mapToDouble(Employee::getMonthlyCost).sum();
}
public int getHeadCount() {
return staff.size();
}
public double getTotalAnnualCost() {
return getTotalMonthlyCost() * 12;
}
}
在这个案例中,Employee
类和 Department
都有 name
字段,也都有月度成本 monthlyCost
和年度成本 annualCost
的概念,可以说这两个类其实在做类似的事情。我们可以用提炼超类来组织这种异曲同工的类,来消除重复行为。
优化:
@Data
public class Party {
private String name;
public double getMonthlyCost() {
return 0;
}
public double getAnnualCost() {
return getMonthlyCost() * 12;
}
}
@Data
public class Employee extends Party {
private int id;
private double monthlyCost;
@Override
public double getMonthlyCost() {
return monthlyCost;
}
}
@Data
public class Department extends Party {
private List<Employee> staff;
@Override
public double getMonthlyCost() {
return staff.stream().mapToDouble(Employee::getMonthlyCost).sum();
}
}
说明:所谓纯数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。但也有例外情况, 一个最好的例外情况就是,纯数据记录对象被用作函数调用的返回结果,比如使用拆分阶段
之后得到的中转数据结构就是这种情况。这种结果数据对象有一个关键的特征:它是不可修改的(至少在拆分阶段
的实际操作中是 这样)。不可修改的字段无须封装,使用者可以直接通过字段取得数据,无须通过取值函数。
优化:
(1)如果有public字段,应立刻运用封装记录
将它们封装起来。
(2)对于那些不该被其他类修改的字段,请运用移除设值函数
样例:
@Data
public class Category {
private String name;
private int level;
}
@Data
public class Product {
private String name;
private Category category;
public String getCategory() {
return category.getLevel() + "." + category.getName();
}
}
Category 是个纯数据类,像这样的纯数据类,直接使用字面量对象似乎也没什么问题。但是,纯数据类常常意味着行为被放在了错误的地方。比如在 Product
有一个应该属于 Category
的行为,就是转化为字符串,如果把处理数据的行为从其他地方搬移到纯数据类里来,就能使这个纯数据类有存在的意义。
优化:
@Data
public class Category {
private String name;
private int level;
@Override
public String toString() {
return level + "." + name;
}
}
public class Product {
private String name;
private Category category;
public String getCategory() {
return category.toString();
}
}
描述:
子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么 办呢?它们得到所有礼物,却只从中挑选几样来玩! 按传统说法,这就意味着继承体系设计错误。
优化:
(1)为这个子类新建一个兄弟类,再运用函数下移
和字段下移
把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。
(2)如果子类复用了超类的行为(实现),却又不愿意支持超类的接口时,应该运用以委托取代子类
或者以委托取代超类
彻底划清界限。
样例:
@Data
public class Party {
private String name;
private List<Employee> staff;
}
@Data
public class Employee extends Party {
private String id;
private double monthlyCost;
}
@Data
public class Department extends Party {
public double getMonthlyCost() {
return getStaff().stream().mapToDouble(Employee::getMonthlyCost).sum();
}
public int getHeadCount() {
return getStaff().size();
}
}
在 Employee
类并不关心 staff
这个字段,这就是 被拒绝的遗赠
。重构手法也很简单,就是把 staff
字段下移到真正需要它的子类 Department
中
@Data
public class Party {
private String name;
}
@Data
public class Employee extends Party {
private String id;
private double monthlyCost;
}
@Data
public class Department extends Party {
private List<Employee> staff;
public double getMonthlyCost() {
return getStaff().stream().mapToDouble(Employee::getMonthlyCost).sum();
}
public int getHeadCount() {
return getStaff().size();
}
}
说明:
注释并不是坏味道,并且属于一种好味道,但是注释的问题在于很多人是经常把它当作“除臭剂”来使用。你经常会看到,一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕,创造它的程序员不想管它了。当你感觉需要写注释时,请先尝试重构,试着让所有注释都变得多余。
优化:
(1)如果你需要注释来解释一块代码做了什么,试试提炼函数
(2)如果函 数已经提炼出来,但还是需要注释来解释其行为,试试用改变函数声明
为它改名;
(3)如果你需要注释说明某些系统的需求规格,试试引入断言
样例:
public static void main(String[] args) {
double discountRate = getDiscountRate();
double base = 10;
//DiscountRate 作为折扣比率,必须要大于0
if (discountRate > 0) {
base = base - discountRate * base;
}
System.out.println("Final base after discount: " + base);
}
public static double getDiscountRate() {
return 0.25;
}
对于discountRate参数,在业务逻辑中必须要保持大于0,当他小于0时,应抛出异常
优化:
public static void main(String[] args) {
double base = 10;
double discountRate = getDiscountRate();
assert discountRate > 0 : "Discount rate should be greater than 0";
if (discountRate > 0) {
base = base - discountRate * base;
}
System.out.println("Final base after discount: " + base);
}
public static double getDiscountRate() {
return -0.25;
}
重构:改善既有代码的设计(第2版) 马丁·福勒(Martin Fowler)