Bruce Eckel
读完需要
10
分钟速读仅需 1 分钟
布鲁斯 • 埃克尔(Bruce Eckel),C++ 标准委员会的创始成员之一,知名技术顾问,专注于编程语言和软件系统设计方面的研究,常活跃于世界各大顶级技术研讨会。
他自 1986 年以来,累计出版 Thinking in C++、Thinking in Java、On Java 等十余部经典计算机著作,曾多次荣获 Jolt 最佳图书奖(被誉为“软件业界的奥斯卡”),其代表作 Thinking in Java 被译为中文、日文、俄文、意大利文、波兰文、韩文等十几种语言,在世界范围内产生了广泛影响。
Bruce Eckel:再聊设计模式(篇一)设计模式分类
Bruce Eckel:再聊设计模式(篇二)封装实现
Bruce Eckel:再聊设计模式(篇三)工厂模式
Bruce Eckel:再聊设计模式(篇四)函数对象模式
Bruce Eckel:再聊设计模式(篇五)改变接口
Bruce Eckel:再聊设计模式(篇六)回调
Bruce Eckel:OnJava 再聊设计模式之多路分发
12
模式重构
本章的剩余部分将探讨如何以一种逐渐演进的方式来应用设计模式,从而解决问题。首先会选择一种设计用于实现最初的方案,然后对该方案进行验证,随后会尝试更多的设计模式来解决问题——有些有效,有些行不通。寻找解决方案的过程中,最关键的问题永远是“哪些部分是会改变的”。
这个过程和 Martin Fowler 在其著作《重构:改善既有代码的设计》中的论述非常相似,虽然他更倾向于讨论代码级的优化(相较于模式级别的设计)。一开始我们会选择一种解决方案,然后渐渐发现该方案无法满足需求的持续变化,便要做出相应的改进。这是一种自然的趋势,但是在计算机编程中,很难用过程式的程序来完成。接受“我们可以重构代码和设计”这种理念,是迈向系统改进的第一步。
我们用来重构的示例是一个垃圾收集系统。垃圾以未分类的状态到达垃圾收集厂,随后我们建立了垃圾分类和评估的模型。在最开始的方案中,反射(参见基础卷第 19 章)会接收匿名的垃圾分块,并检测出它们的类型以进行分类。
12.1
Trash 和它的子类
Trash(垃圾)的基类含有 weight 和 price()等信息,并带有一个通过反射来生成 Trash 子类精确名称的 toString()方法。其中还包含了 accept()方法,稍后会用它实现访问者模式,不过在此之前,你可以先暂时忽略它。
// patterns/trash/Trash.java
// 垃圾收集示例的基类
package patterns.trash;
public abstract class Trash {
public final double weight;
public Trash(double weight) {
this.weight = weight;
}
public abstract double price();
@Override public String toString() {
return String.format(
"%s weight: %.2f * price: %.2f = %.2f",
getClass().getSimpleName(),
weight, price(), weight * price());
}
// 暂时可以忽略,稍后会用到它:
public abstract void accept(Visitor v);
}
price()是一个会返回材料当前价格的 abstract 方法。价格并不会随着每块不同的 Trash 而变化——比如 Aluminum(铝)的价格就是固定的(由材质决定)。将所有的价格都放在一个地方非常方便,这样改变价格就很容易了。通过 interface,每个字段都被自动分配了 public、static 和 final 权限。
// patterns/trash/Price.java
package patterns.trash;
public interface Price {
double
ALUMINUM = 1.67,
PAPER = 0.10,
GLASS = 0.23,
CARDBOARD = 0.11;
}
每种不同类型的Trash的price()方法都会返回Price中适当的字段:
// patterns/trash/Aluminum.java
package patterns.trash;
public class Aluminum extends Trash {
public Aluminum(double wt) { super(wt); }
@Override public double price() {
return Price.ALUMINUM;
}
// 暂时可以忽略,稍后会用到它:
@Override public void accept(Visitor v) {
v.visit(this);
}
}
// patterns/trash/Paper.java
package patterns.trash;
public class Paper extends Trash {
public Paper(double wt) { super(wt); }
@Override public double price() {
return Price.PAPER;
}
// 暂时可以忽略,稍后会用到它:
@Override public void accept(Visitor v) {
v.visit(this);
}
}
// patterns/trash/Glass.java
package patterns.trash;
public class Glass extends Trash {
public Glass(double wt) { super(wt); }
@Override public double price() {
return Price.GLASS;
}
// 暂时可以忽略,稍后会用到它:
@Override public void accept(Visitor v) {
v.visit(this);
}
}
// patterns/trash/Cardboard.java
package patterns.trash;
public class Cardboard extends Trash {
public Cardboard(double wt) { super(wt); }
@Override public double price() {
return Price.CARDBOARD;
}
// 暂时可以忽略,稍后会用到它:
@Override public void accept(Visitor v) {
v.visit(this);
}
}
TrashValue是一个带有static函数sum()的工具类。这个类接收一个由Trash组成的List,并显示其中的每块Trash,最后显示出该List中所有Trash的总价值。
// patterns/trash/TrashValue.java
// 累加一个垃圾箱中所有垃圾的价值
package patterns.trash;
import java.util.*;
public class TrashValue {
private static double total;
public static void
sum(List extends Trash> bin, String type) {
total = 0.0;
bin.forEach( t -> {
System.out.println(t);
total += t.weight * t.price();
});
System.out.printf(
"Total %s value = %.2f%n", type, total);
}
}
看起来并无必要将total定义在sum()外部,因为永远不会在sum()之外的地方用到它。不过如果试图将其定义为sum()内部的本地变量,便会报出错误消息:“lambda表达式引用的本地变量必须定义为final,或具有final的效果(local variables referenced from a lambda expression must be final or effectively final)。”
一旦List
// patterns/trash/Bins.java
package patterns.trash;
import java.util.*;
public class Bins {
final List bin;
final List aluminum = new ArrayList<>();
final List paper = new ArrayList<>();
final List glass = new ArrayList<>();
final List cardboard = new ArrayList<>();
public Bins(List source) {
bin = new ArrayList<>(source); // 复制
bin.forEach( t -> {
// 通过反射发现Trash的类型:
if(t instanceof Aluminum)
aluminum.add((Aluminum)t);
if(t instanceof Paper)
paper.add((Paper)t);
if(t instanceof Glass)
glass.add((Glass)t);
if(t instanceof Cardboard)
cardboard.add((Cardboard)t);
});
}
public void show() {
TrashValue.sum(aluminum, "Aluminum");
TrashValue.sum(paper, "Paper");
TrashValue.sum(glass, "Glass");
TrashValue.sum(cardboard, "Cardboard");
TrashValue.sum(bin, "Trash");
}
}
增加一种新类型的垃圾意味着在Bins中增加一个新的List和instanceof,以及show()中的另一行。
现在我们已经准备好创建一个简单工厂了,该工厂持有一个名为constructors的List,其中包含了用于创建新Trash对象的构造器。
// patterns/recyclea/RecycleA.java
// 用反射实现的垃圾收集
// {java patterns.recyclea.RecycleA}
package patterns.recyclea;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import patterns.trash.*;
class SimpleFactory {
static final
List> constructors =
Arrays.asList(
Aluminum::new, Paper::new, Glass::new);
static final int SIZE = constructors.size();
private static SplittableRandom rand =
new SplittableRandom(42);
public static Trash random() {
return constructors
.get(rand.nextInt(SIZE))
.apply(rand.nextDouble());
}
}
public class RecycleA {
public static void main(String[] args) {
List bin =
Stream.generate(SimpleFactory::random)
.limit(10)
.collect(Collectors.toList());
Bins bins = new Bins(bin);
bins.show();
}
}
/* 输出:
Aluminum weight: 0.34 * price: 1.67 = 0.57
Aluminum weight: 0.62 * price: 1.67 = 1.03
Aluminum weight: 0.49 * price: 1.67 = 0.82
Aluminum weight: 0.50 * price: 1.67 = 0.83
Total Aluminum value = 3.26
Paper weight: 0.69 * price: 0.10 = 0.07
Total Paper value = 0.07
Glass weight: 0.16 * price: 0.23 = 0.04
Glass weight: 0.87 * price: 0.23 = 0.20
Glass weight: 0.80 * price: 0.23 = 0.18
Glass weight: 0.52 * price: 0.23 = 0.12
Glass weight: 0.20 * price: 0.23 = 0.05
Total Glass value = 0.59
Total Cardboard value = 0.00
Glass weight: 0.16 * price: 0.23 = 0.04
Aluminum weight: 0.34 * price: 1.67 = 0.57
Glass weight: 0.87 * price: 0.23 = 0.20
Glass weight: 0.80 * price: 0.23 = 0.18
Aluminum weight: 0.62 * price: 1.67 = 1.03
Aluminum weight: 0.49 * price: 1.67 = 0.82
Glass weight: 0.52 * price: 0.23 = 0.12
Glass weight: 0.20 * price: 0.23 = 0.05
Aluminum weight: 0.50 * price: 1.67 = 0.83
Paper weight: 0.69 * price: 0.10 = 0.07
Total Trash value = 3.91
*/
注意SimpleFactory不会生成Cardboard,因为constructors中并没有Cardboard::new。
constructors类型并不是一个List
random()为随机的Trash类型取出Function,通过apply()方法调用Function,并传入随机生成的double参数。
在main()中,Stream.generate()的参数并不是Supplier
这个程序满足了设计需求:能够运行。如果这是一次性解决问题的长久方案,便不会有问题。但是,一个有用的程序往往会随着时间的推移而发展,因此你需要问自己:“万一情况有变化呢?”比如,塑料是一种有价值的可回收商品,那么应该怎样将其集成到系统中呢(特别是在程序代码很庞杂的情况下)?虽然SimpleFactory确实封装了创建过程,但程序的其余部分散落着若干类型检查的代码,因此每次增加新类型时,都必须找到这些代码。如果遗漏了某一处,则编译器并不会产生任何有帮助的错误消息。
如果每种类型都经过了验证,你就会知道自己是在误用反射。如果只是因为需要特殊对待类型的某个子集而要找出该子集,那么这样做大概没什么问题。但是如果要找出switch语句中的所有类型,那么可能会有办法来改进设计,使其具有更好的可维护性。本章剩余部分会通过多个阶段逐步演化该程序,使其变得更加灵活。这种方式应该可以为如何设计程序提供有一个有价值的案例。
12.2
信使对象
面向对象的设计有一个原则(我最初是从 Grady Booch 那里听到的):“如果觉得设计太复杂,那就生成更多对象。”这种说法既违反直觉,又简单得可笑,但我发现它很有用(“生成更多对象”通常等同于“再增加一层抽象”)。总的来说,如果发现有些地方代码很乱,就要考虑用哪种类可以清理代码。通常清理代码带来的副作用是使系统更灵活并且结构更好。
SimpleFactory 是个合理的首选方案,但是如果派生的 Trash 构造器需要不同的或更多的参数呢?“生成更多对象”可以解决该问题。为了隐藏用于创建的数据,TrashInfo 包含了工厂为创建合适的 Trash 对象的所有必要信息:
// patterns/trash/TrashInfo.java
// 携带Trash创建时的数据的信使类
package patterns.trash;
public class TrashInfo {
public final String type;
public final double data;
public TrashInfo(String type, double data) {
this.type = type;
this.data = data;
}
@Override public String toString() {
return "TrashInfo(" + type + ", " + data + ")";
}
}
TrashInfo对象的唯一职责就是持有并传递信息,因此它被称为信使对象(Messenger Object)或者数据传输对象(Data Transfer Object, DTO)。信使一般是不可变的,因此这两个字段都是final并且public的。
如果某些事物发生了变化,导致工厂需要不同的或更多的信息来创建新的Trash对象,则此时无须改变工厂的参数。可以通过增加新的数据和构造器,或者通过子类化来直接修改TrashInfo。
本书特色
查漏宝典:涵盖Java关键特性的设计原理和应用方法
避坑指南:以产业实践的得失为鉴,指明Java开发者不可不知的设计陷阱
经典普适:值得不同层次的Java开发者反复研读
专家领读:4位一线业务专家、知名作译者帮你拆解书中难点,总结Java开发精要
值得一提的是,为了帮助新手加深理解,出版方邀请了4位从业10年以上知名作译者(DDD 专家张逸、服务端专家梁桂钊、软件系统架构专家王前明、译者陈德伟)为本书录制【精讲视频】和【导读指南】,该视频已在B站和图灵社区发布,感兴趣的朋友可以去看看。
往期推荐
Bruce Eckel:再聊设计模式(篇一)
Bruce Eckel:再聊设计模式(篇二)封装实现
Bruce Eckel:再聊设计模式(篇三)工厂模式
Bruce Eckel:再聊设计模式(篇四)函数对象模式
Bruce Eckel:再聊设计模式(篇五)改变接口
Bruce Eckel:OnJava 再聊设计模式之回调
三板斧!助你成为优秀软件工程师
上千万行,十多G源码,浏览器为什么这么“变态”?
DevOps失败了!!!
复旦博导王鹏:智能运维远没有说得“智能”
取代C++?谷歌新开源编程语言Carbon,入坑么?
史海峰:在时代节点上顺势而为是一种幸运
知明:技术 Leader 的思考法
我,程序员,马上35岁...
后Kubernetes时代的微服务