Bruce Eckel:OnJava 模式重构(上)

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 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装满了Trash对象,Bins(垃圾箱)构造器便会通过instanceof将该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类型。相反,该List持有的是Function。这说明Function对象接收Double类型的参数并返回Trash对象——这正是Trash的单参数构造器的行为。然而这些单参数构造器接收的是double,而不是包装类Double,因此Java会进行自动装箱。

random()为随机的Trash类型取出Function,通过apply()方法调用Function,并传入随机生成的double参数。

在main()中,Stream.generate()的参数并不是Supplier,后者是generate()的具体参数类型。即使生成Trash的函数名是random()而不是get(),也是可以运行的。

这个程序满足了设计需求:能够运行。如果这是一次性解决问题的长久方案,便不会有问题。但是,一个有用的程序往往会随着时间的推移而发展,因此你需要问自己:“万一情况有变化呢?”比如,塑料是一种有价值的可回收商品,那么应该怎样将其集成到系统中呢(特别是在程序代码很庞杂的情况下)?虽然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。

Bruce Eckel:OnJava 模式重构(上)_第1张图片

Bruce Eckel:OnJava 模式重构(上)_第2张图片

本书特色

  • 查漏宝典:涵盖Java关键特性的设计原理和应用方法

  • 避坑指南:以产业实践的得失为鉴,指明Java开发者不可不知的设计陷阱

  • 经典普适:值得不同层次的Java开发者反复研读

  • 专家领读:4位一线业务专家、知名作译者帮你拆解书中难点,总结Java开发精要

值得一提的是,为了帮助新手加深理解,出版方邀请了4位从业10年以上知名作译者DDD 专家张逸、服务端专家梁桂钊、软件系统架构专家王前明、译者陈德伟)为本书录制【精讲视频】和【导读指南】,该视频已在B站和图灵社区发布,感兴趣的朋友可以去看看。

Bruce Eckel:OnJava 模式重构(上)_第3张图片

往期推荐

Bruce Eckel:再聊设计模式(篇一)

Bruce Eckel:再聊设计模式(篇二)封装实现

Bruce Eckel:再聊设计模式(篇三)工厂模式

Bruce Eckel:再聊设计模式(篇四)函数对象模式

Bruce Eckel:再聊设计模式(篇五)改变接口

Bruce Eckel:OnJava 再聊设计模式之回调

三板斧!助你成为优秀软件工程师

上千万行,十多G源码,浏览器为什么这么“变态”?

DevOps失败了!!!

复旦博导王鹏:智能运维远没有说得“智能”

取代C++?谷歌新开源编程语言Carbon,入坑么?

史海峰:在时代节点上顺势而为是一种幸运

知明:技术 Leader 的思考法

我,程序员,马上35岁...

后Kubernetes时代的微服务

Bruce Eckel:OnJava 模式重构(上)_第4张图片

你可能感兴趣的:(java,设计模式,编程语言,python,大数据)