从 Java 源码阅读中汲取设计思想及示例解读(上)

写一千行重复代码,不如读十行优雅设计的代码。

引子

“编程漫谈(十八):编程三境界” 谈到:重复性做一件事,并不一定会促进技艺的提升,反而,有可能导致持续性的退步。每日有所进步(哪怕只有微小的进步),才能促进编程技艺的提升。这种进步,或者来自于简单的代码重构,或者来自于大胆的全局结构重构,或者来自于有挑战性的编程课题,或者来自于设计先导的构思,或者来自于对模型完善性的推敲。只有在重复中渐进地变化、小幅提升、大幅突破,才会收获飞跃性的进步。

要持续进步,就需要持续的输入。没有优质的输入,人就很容易陷入重复性行为而浑然不觉。对于技术人员来说,跳出原有思维和做法的一个有效途径,就是阅读优秀的源代码,汲取其中的设计思想,应用到工作中。

有人一听阅读源代码,想想那密密的几百万行,那数不尽的细节,顿时就想打退堂鼓。我也有这种心理。不过,就像编程一样,阅读源代码也是一种技艺,可以一步步提升的。

源代码可以分为设计与实现。设计更多涉及宏观的布局,核心类的设计、全局控制结构的设计,涉及细节并不多;只有深入到实现中,才会涉及细节。实现自然有实现的乐趣和魅力,设计则更注重“观其大略之美”。

本文着重探讨如何从优秀 Java 源码中汲取好的设计思想。假定读者已经有了一定的源码阅读经历和基本的阅读能力,能够通过阅读源码解决一些日常问题。阅读源代码的基本方法可阅:“解锁优秀源代码的方法与技巧总结(上)”

设计思想

设计思想主要包括:

  • 行为。一组简洁的核心类的行为规范定义,通常表现为接口。行为体现了抽象。
  • 流程。将行为串联起来构成完整功能的全局控制结构。流程体现了集成。

软件系统通常由抽象和集成构建。此外,通常会有一些对行为规范的基本实现,称为抽象类,可以供业务方自定义针对具体场景的实现。

要学习源码的设计思想,通常就可以从以下方面着手:

  • 核心接口:定义了核心类的行为及交互方式。比如 Java 集合 List 接口。
  • 抽象类:抽象类是核心接口的基本实现,半成品,只要实现指定类的少数方法,即可提供一个具体的实现。比如 AbstractList。
  • 核心类: 核心类是核心接口的具体实现。必要的话,可以阅读两个基本实现,并加以对比。比如 ArrayList, LinkedList
  • 类交互: 类交互有继承和委托两种基本方式,通过多态在运行时选择具体类及行为。可以阅读设计模式相关的书籍来熟悉常见的类交互方式。
  • 主要流程:一个完整功能,是怎么把接口的行为串联起来的。编写核心类的测试用例,是一个很好的学习方法。

阅读步骤及顺序: 核心接口 -> 主要流程 -> 类交互 -> 抽象类 -> 核心类。实际阅读时,可适当调整。

如果一时读不懂,可以上网去搜索类名(或缩写,比如 AQS)或框架名(SpringBoot),基本上都有人解读过的。 有了这些准备,就可以踏上阅读源码的征途了。

示例解读

从简单着手

"Hello World" 可以说是程序界新手上路的必经之途。对于 Java 源码阅读,个人特别推荐 Java 集合框架和 Java IO 框架。

Java 集合框架

推荐理由:1. 很实用; 2. “接口-抽象类-实现类”的经典示例之一; 3. 类的交互简单。

Java 集合框架的整体设计如下所示。一张图就搞定了。可见,类图是解读源码设计思想的有力武器之一。

理解 Java 集合框架设计的重点在于:

  • 采用的数据结构(数组、链表、哈希表、红黑树等),优势(容量、顺序&随机读写、保序、排序、去重等),适用场景(容量扩展、简单性、大量数据读写性能、顺序还是随机读写、保序场景、排序场景等)。
  • 需要定义哪些行为,提炼出哪些抽象方法,能够支撑完备友好的集合操作,又能够让具体实现类的开发负担最小。可以重点推敲接口和抽象类,尤其是抽象类的设计。
  • 集合框架大量使用了迭代器和并发读写快速失败机制。
  • 成对的相互配合的概念。比如 Collection & Iterator ,Map & Entry , Sorted & Comparator
  • 重点实现类:ArrayList, HashSet, HashMap

Java IO 框架

推荐理由:

  • 有一组关于 IO 、流、读写的简洁优雅的概念及组合。体现了设计的基本理念和方法。
  • 使用装饰器优雅地组合出多功能的读写功能,是装饰器模式的经典示例之一。

可阅 “javaIO框架小析”

提升交互难度

JUnit4

读懂 Java 集合框架和 IO 框架之后,接下来可以挑战 JUnit4。

推荐理由:1. 功能简单,不涉及复杂的技术和原理; 2. 模板设计模式; 3. 适量而优雅的类交互;4. 一个简洁易用框架的设计考虑。

解读:

  • 在 junit.framework 包里, TestCase 和 TestResult 联合起来就实现了 JUnit 单测执行的核心功能。
  • TestCase:要运行单测,需要有个类标识单测用例。JUnit4 提供了两种方式:TestCase 和 @Test 注解。其中 TestCase 定义了一个单测的执行流程,采用了模板设计模式,具体的单测用例只要定义符合约定的单测方法(setUp, tearDown, public void testXxx())即可。TestCase 的核心代码如下:
public void runBare() throws Throwable {
    Throwable exception = null;
    this.setUp();

    try {
      this.runTest();
    } catch (Throwable var10) {
      exception = var10;
    } finally {
      try {
        this.tearDown();
      } catch (Throwable var11) {
        if (exception == null) {
          exception = var11;
        }
      }

    }

    if (exception != null) {
      throw exception;
    }
  }

public TestResult run() {
    TestResult result = this.createResult();
    this.run(result);
    return result;
  }
  • TestResult :运行单测、处理异常并统计测试用例执行结果,生成测试报告。此外,还在 startTest 和 endTest 中运行了注册的单测监听器 TestListener. TestListener 定义了在单测出错、失败、启动、结束时的行为。
protected void run(final TestCase test) {
    this.startTest(test);
    Protectable p = new Protectable() {
      public void protect() throws Throwable {
        test.runBare();
      }
    };
    this.runProtected(test, p);
    this.endTest(test);
  }
  • TestSuite & BaseTestRunner & TestRunner: 一组测试用例集的定义和执行。这是在核心的基础上,使之实用化的必要件。具体是利用反射和方法约定,来生成要测试的用例集。BaseTestRunner 是抽象类,定义了测试用例集执行的基本流程,TestRunner 是默认实现,并给出了用例计时功能。

  • TestSuite: 一个极简版组合模式的实现。

public class TestSuite implements Test {
  private String fName;
  private Vector<Test> fTests;
  
  public void run(TestResult result) {
    Iterator i$ = this.fTests.iterator();

    while(i$.hasNext()) {
      Test each = (Test)i$.next();
      if (result.shouldStop()) {
        break;
      }

      this.runTest(each, result);
    }
  }
}
  • Describable & Description: 测试用例集的描述。除了执行,测试用例的自描述也非常重要。尤其在大型工程的测试用例集维护中,描述也许比执行更为重要。Spock 支持直接以描述性的信息作为测试方法名。 对于传统 Java 测试框架,通常是采用注解的方式实现。Description 也采用了组合模式。如果一个类里面的某个属性是它自身的子列表,或者它实现的接口的子列表,那很可能采用了组合模式。此外,对于不太复杂的类,通过阅读它的数据结构,基本就知道它的功能了。
public class Description implements Serializable {
  private static final long serialVersionUID = 1L;
  private static final Pattern METHOD_AND_CLASS_NAME_PATTERN = Pattern.compile("([\\s\\S]*)\\((.*)\\)");
  public static final Description EMPTY = new Description((Class)null, "No Tests", new Annotation[0]);
  public static final Description TEST_MECHANISM = new Description((Class)null, "Test mechanism", new Annotation[0]);
  private final Collection<Description> fChildren;
  private final String fDisplayName;
  private final Serializable fUniqueId;
  private final Annotation[] fAnnotations;
  private volatile Class<?> fTestClass;
}
  • JUnitCore & RunNotifier :JUnit 单测用例执行的全局控制结构。JUnitCore 是主入口,负责在单测运行之前将监听器注入,创造运行单测的上下文环境。 RunNotifier 的 listeners 采用了 CopyOnWriteArrayList 实现,内部使用了 ReentrantLock 进行并发同步。应该是为了支持测试用例的并发执行。
public class JUnitCore {
  private final RunNotifier notifier = new RunNotifier();

  Result runMain(JUnitSystem system, String... args) {
    system.out().println("JUnit version " + Version.id());
    JUnitCommandLineParseResult jUnitCommandLineParseResult = JUnitCommandLineParseResult.parse(args);
    RunListener listener = new TextListener(system);
    this.addListener(listener);
    return this.run(jUnitCommandLineParseResult.createRequest(defaultComputer()));
  }

  public Result run(Runner runner) {
    Result result = new Result();
    RunListener listener = result.createListener();
    this.notifier.addFirstListener(listener);

    try {
      this.notifier.fireTestRunStarted(runner.getDescription());
      runner.run(this.notifier);
      this.notifier.fireTestRunFinished(result);
    } finally {
      this.removeListener(listener);
    }

    return result;
  }
}

RunNotifier 和 Description 的示例如下:

  • Runner & RunListener & RunnerBuilder : Runner 是全局控制结构的核心,是驱动器。RunnerBuilder 构建出具体的 Runner 。 在 Runner 执行的前后及过程中,会执行 RunNotifier 注册的 RunListener 的方法。通过 Debug 单测可知, Spock 单测,Runner 的实现类是 Sputnik ,普通 Java 单测的 Runner 实现类是 BlockJUnit4ClassRunner ,实际在 ParentRunner 中实现。
public abstract class ParentRunner<T> extends Runner implements Filterable, Sortable {

   public void run(RunNotifier notifier) {
    EachTestNotifier testNotifier = new EachTestNotifier(notifier, this.getDescription());

    try {
      Statement statement = this.classBlock(notifier);
      statement.evaluate();
    } catch (AssumptionViolatedException var4) {
      testNotifier.addFailedAssumption(var4);
    } catch (StoppedByUserException var5) {
      throw var5;
    } catch (Throwable var6) {
      testNotifier.addFailure(var6);
    }

  }

   protected Statement childrenInvoker(final RunNotifier notifier) {
    return new Statement() {
      public void evaluate() {
        ParentRunner.this.runChildren(notifier);
      }
    };
  }
}

  • Statement: 单测执行环节的抽象。 使用了职责链模式,将一个单测方法的执行链串联起来。比如 RunBefores 的定义。这篇文章讲得不错: “深入JUnit源码之Statement”
protected Statement withBeforeClasses(Statement statement) {
    List<FrameworkMethod> befores = this.testClass.getAnnotatedMethods(BeforeClass.class);
    return (Statement)(befores.isEmpty() ? statement : new RunBefores(statement, befores, (Object)null));
}

protected Statement withAfterClasses(Statement statement) {
    List<FrameworkMethod> afters = this.testClass.getAnnotatedMethods(AfterClass.class);
    return (Statement)(afters.isEmpty() ? statement : new RunAfters(statement, afters, (Object)null));
}

public class RunBefores extends Statement {
  private final Statement next;
  private final Object target;
  private final List<FrameworkMethod> befores;

  public RunBefores(Statement next, List<FrameworkMethod> befores, Object target) {
    this.next = next;
    this.befores = befores;
    this.target = target;
  }

  public void evaluate() throws Throwable {
    Iterator i$ = this.befores.iterator();

    while(i$.hasNext()) {
      FrameworkMethod before = (FrameworkMethod)i$.next();
      before.invokeExplosively(this.target, new Object[0]);
    }

    this.next.evaluate();
  }
}
  • FrameworkMethod : 单测执行过程中,具体被执行的方法。FrameworkMethod 的执行委托给了 method 对象。
public class FrameworkMethod extends FrameworkMember<FrameworkMethod> {
    private final Method method;

    public Object invokeExplosively(final Object target, final Object... params) throws Throwable {
    return (new ReflectiveCallable() {
      protected Object runReflectiveCall() throws Throwable {
        return FrameworkMethod.this.method.invoke(target, params);
      }
    }).run();
  }
}

public abstract class FrameworkMember<T extends FrameworkMember<T>> implements Annotatable {     
}

注意,这里的泛型定义,T extends S<T> , S<T extend S<T>> 看上去有点绕,实际上是对 S 中的泛型参数 T 进行了类型限定(T 必须是 S 的子类),避免传入不合预期的对象。用个小栗子说明下。如下代码所示。如果 Animal 的泛型 T 不做任何限定,意味着方法里的对象可以为任意对象类型。如果要做比较的话,就显得比较荒谬(比如将 人与石头比较)。此时,可以对 T 做个限定,只能是 Animal 的子类。这样比较就合理了。男人与男人比较,女人与女人比较。

import lombok.EqualsAndHashCode;
import org.junit.Test;

public class GenericTypeInheritedTest {

  @Test
  public void test() {
    Human human = new Human();
    human.compare(new Stone());

    Female female = new Female("qin");
    Female female2 = new Female("qin");
    female.compare(female2);

    Male male = new Male("Li");
    Male maie2 = new Male("Li");
    male.compare(maie2);
  }
}

class Stone {
}

class Animal<T> {
  private String name;

  public void compare(T t) {
    System.out.println(t.equals(this));
  }
}

class Human extends Animal<Stone> {

}

class Animal2<T extends Animal2> {
  protected String name;

  public void compare(T t) {
    System.out.println(t.equals(this));
  }
}

@EqualsAndHashCode
class Female extends Animal2<Female> {

  public Female(String name) {
    this.name = name;
  }

  public void compare(Female t) {
    System.out.println(t.equals(this));
  }
}

@EqualsAndHashCode
class Male extends Animal2<Male> {

  public Male(String name) {
    this.name = name;
  }

  public void compare(Male t) {
    System.out.println(t.equals(this));
  }
}

即使极简如 JUnit4 , 也能学到很多:

  • 先把握核心接口和核心类。核心接口和核心类是一个设计的精髓所在。阅读源码,不能买椟还珠。
  • 模板设计模式。若要设计一个框架,模板设计模式几乎是不可避免的,因为其作用正在于将通用流程与定制化业务相分离。使用模板设计模式,需要注意两点:1. 模板方法的参数尽可能宽而不是窄,比如导出,应该用导出上下文 ExportContext ,而不是只用到 File 就用 File ,否则后续优化时改动的影响很大; 2. 流程不要定太死,粗粒度,灵活些。不然,后面需要增减流程时,就比较尴尬。这反过来说明,要验证一个设计是否实用,只要多上几个需求就可以了。
  • 职责链模式。 适合模拟一个流水线执行。流水线上的每道工序只负责完成自己的工作,完成之后就交给下一个工序。
  • 表示、构建、执行、描述的分离。表示,是如何用子概念或子组件的组合去表达一个概念或组件;构建,子概念或子组件通过何种方式和步骤组合构成这个概念或组件;执行,是概念及组件之间如何优雅地交互和集成,得到期望结果;描述,是如何让这些概念及交互能够在构建和执行的过程中清晰自然地描述自己的行为,而不需要额外的代码注释。
  • 监听器、拦截器等。 对于一个框架设计来说,能够在某个事件发生前、发生时、发生后,发送一些通知,做一些自定义的操作也是很重要的。这可以增强框架的自定义的灵活性。
  • 异常、监控、统计等。这些外围的配套能力,使得设计更加完备更容易获得青睐。
  • 自描述能力。除了优雅的执行,让自定义的业务实现类具有自描述能力,也是大型工程维护的一个很重要的考虑。
  • 编写单测用例找到实现类。 由于框架设计通常是抽象的,有时跟踪代码流程走着走着,就不知道跳转到哪里了,在哪个实现类。这时,就需要编写单测用例,单步调试下,通常能找到那个默认类,然后再继续跟踪下去。

小结

本文探讨了最为简单的三个设计:Java 集合框架、Java IO 框架和 JUnit4 。这些简单的框架实现中,蕴含了很多经典的设计示例,能够帮助新手打下一个较好的代码设计基础。

阅读源代码并不是件轻松的事情。它需要投注大量的心智。而正是需要直面自己的心智,攀登心智之山,才成就了一种特殊体验吧。

接下来,我们将离开浅水滩,驶向深海领域。

PS:找到了一个很好的 Markdown 到微信公众号的排版工具:https://mdnice.com/ 😃


心中无剑,处处含剑。剑舞九天,光照苍穹。此名曰:水玉剑法。

posted @ 2020-07-18 17:37  琴水玉  阅读(515)  评论(0编辑  收藏  举报