重拾Java基础知识:代码校验

前言

你永远不能保证你的代码是正确的,你只能证明它是错的。

测试

Java是一个静态类型的语言,程序员经常对一种编程语言明显的安全性感到过于舒适,“能通过编译器,那就是没问题的”。但静态类型检查是一种非常局限性的测试,只是说明编译器接受你代码中的语法和基本类型规则,并不意味着你的代码达到程序的目标。随着你代码经验的丰富,你逐渐了解到你的代码从来没有满足过这些目标。迈向代码校验的第一步就是创建测试,针对你的目标检查代码的行为。

单元测试

“单元”是指测试一小部分代码 ,这个过程是将集成测试构建到你创建的所有代码中,并在每次构建系统时运行这些测试。通常,每个类都有测试来检查它所有方法的行为。“系统”测试则是不同的,它检查的是整个程序是否满足要求。

C 风格的语言,尤其是 C++,通常会认为性能比安全更重要。用 Java 编程比 C++(一般认为大概快两倍)快的原因是 Java 的安全性保障:比如垃圾回收以及改良的类型检测等特性。通过将单元测试集成到构建过程中,你扩大了这个安全保障,因而有了更快的开发效率。当发现设计或实现的缺陷时,可以更容易、更大胆地重构你的代码。

Junit

最初的 JUnit 发布于 2000 年,大概是基于 Java 1.0,因此不能使用 Java 的反射工具。这个框架走向了另一个极端,“尝试最简单可行的方法”(极限编程中的一个关键短语)。从那之后,JUnit 通过反射和注解得到了极大的改进,大大简化了编写单元测试代码的过程。在 Java8 中,他们甚至增加了对 lambdas 表达式的支持。如果你正准备去下载junit,我已经帮你下载好了。

public class JunitTest {
    @Test
    public void test(){
        System.out.println("test");
    }
    @BeforeAll
    static void beforeAll(){
        System.out.println("before");
    }
    @AfterAll
    static void afterAll(){
        System.out.println("after");
    }
    @BeforeEach
    public void beforeEach(){
        System.out.println("reload init");
    }
    @AfterEach
    public void afterEach(){
        System.out.println("be clean");
    }
    /** Output: 
     *  before
     *  reload init
     *  test
     *  be clean
     *  after
     */
}

我们可以看到他们的执行顺序。@BeforeAll 注解是在任何其他测试操作之前运行一次的方法。@BeforeEach注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。@Test 注解标记表示测试的每个方法。@AfterAll 是所有其他测试操作之后只运行一次的方法。如果你必须在每次测试后执行清理,那就用注解 @AfterEach。其中@BeforeAll@AfterAll两个方法都必须是静态的。

测试覆盖率

测试覆盖率,同样也称为代码覆盖率,度量代码的测试百分比。百分比越高,测试的覆盖率越大。覆盖率是用来度量测试完整性的手段,是测试效果衡量的标准,是测试技术有效性的度量:

覆盖率 = (至少被执行一次的项目(item)数) / (项目的总数)

项目是指:语句、判定、分支、函数等。覆盖率按照测试方法一般可分为三大类:

  • 白盒覆盖率:语句、判定、条件、路径等等;
  • 灰盒覆盖率:接口相关;
  • 黑盒覆盖率:功能、性能测试;

当分析一个未知的代码库时,测试覆盖率作为一个粗略的度量是有用的。如果覆盖率工具报告的值特别低(比如,少于百分之40),则说明覆盖不够充分。然而,一个非常高的值也同样值得怀疑,你将浪费大量时间来生成剩余的代码,并且在向项目添加代码时浪费的时间更多。覆盖工具的最佳用途是发现代码库中未测试的部分。但是,不要依赖覆盖率来得到测试质量的任何信息。因为测试成本随覆盖率的增加而增加,要在覆盖率和成本之间有所取舍。

前置条件

前置条件的概念来自于契约式设计(Design By Contract, DbC),利用断言机制实现。前置条件确保客户端(调用此方法的代码)履行其部分契约。这意味着在方法调用开始时几乎总是会检查参数(在你用那个方法做任何操作之前)以此保证它们的调用在方法中是合适的。因为你永远无法知道客户端会传递给你什么,前置条件是确保检查的一个好做法。

断言(Assertions)

断言通过验证在程序执行期间满足某些条件,从而增加了程序的健壮性。如果在方法的内部,则可以使用断言检查参数的有效性。这些是确保程序正确的重要测试,但是它们不能在编译时被检查,并且它们不属于单元测试的范围。断言语句有两种形式 :

  • assert boolean-expression
  • assert boolean-expression: information-expression;

两者似乎告诉我们 “我断言这个布尔表达式会产生 true”, 否则,将抛出 AssertionError 异常。AssertionErrorThrowable 的派生类,因此不需要异常说明。

    public static void main(String[] args) {
//        assert false;
        assert false:"this is error";
        /** Output:
         * Exception in thread "main" java.lang.AssertionError: this is error
         * 	at com.study.test.AssertTest.main(AssertTest.java:6)
         */
    }

information-expression 可以产生任何类型的对象,因此,通常将构造一个包含对象值的更复杂的字符串,它包含失败的断言。一般默认运行程序并不会抛出任何错误,你需要运行前启动断言,一种简单的方法是找到你启动类的配置项,再VM options栏中使用 -ea 标志, 它也可以表示为: -enableassertion。

public class Test {
    public void go(){
        assert false:"Test.go()";
    }
}
public class AssertTest {
    public static void main(String[] args) {
        ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
        new Test().go();
        /** Output:
         * Exception in thread "main" java.lang.AssertionError: AssertTest.go()
         * 	at com.study.test.Test.go(Test.java:7)
         * 	at com.study.test.AssertTest.main(AssertTest.java:8)
         */
    }
}

这消除了在运行程序时在命令行上使用 -ea 标志的需要,使用 -ea 标志启用断言可能同样简单。当交付独立产品时,可能必须设置一个执行脚本让用户能够启动程序,配置其他启动参数,这么做是有意义的。

契约式设计

契约式设计(DbC)是 Eiffel 语言的发明者 Bertrand Meyer 提出的一个概念,通过确保对象遵循某些规则来帮助创建健壮的程序。DbC 假定服务供应者与该服务的消费者或客户之间存在明确指定的契约。在面向对象编程中,服务通常由对象提供,对象的边界 — 供应者和消费者之间的划分 — 是对象类的接口。当客户端调用特定的公共方法时,它们希望该调用具有特定的行为:对象状态改变,以及一个可预测的返回值。

Meyer 认为:

  1. 应该明确指定行为,就好像它是一个契约一样。
  2. 通过实现某些运行时检查来保证这种行为,他将这些检查称为前置条件、后置条件和不变项。

检查指令:检查指令说明你确信代码中的某个特定属性此时已经得到满足。检查指令的思想是在代码中表达非明显性的结论,而不仅仅是为了验证测试,也同样为了将来能够满足阅读者而有一个文档
前置条件:法调用开始时几乎总是会检查参数(在你用那个方法做任何操作之前)以此保证它们的调用在方法中是合适的。因为你永远无法知道客户端会传递给你什么,前置条件是确保检查的一个好做法。
后置条件:后置条件测试你在方法中所做的操作的结果。这段代码放在方法调用的末尾,在 return 语句之前(如果有的话)。对于长时间、复杂的方法,在返回计算结果之前需要对计算结果进行验证(也就是说,在某些情况下,由于某种原因,你不能总是相信结果),后置条件很重要,但是任何时候你可以描述方法结果上的约束时,最好将这些约束在代码中表示为后置条件。
不变性:不变性保证了必须在方法调用之间维护的对象的状态。但是,它并不会阻止方法在执行过程中暂时偏离这些保证,它只是在说对象的状态信息应该总是遵守状态规则:

  1. 在进入该方法时。

  2. 在离开方法之前。

根据这个描述,一个有效的不变性被定义为一个方法,方法以:assert invariant()这样方式调用 ,它在构造之后以及每个方法的开始和结束时调用。如果出于性能原因禁用断言,就不会产生开销。

尽管 Meyer 强调了前置条件、后置条件和不变性的价值以及在开发过程中使用它们的重要性,他承认在一个产品中包含所有 DbC 代码并不总是实际的。你可以基于对特定位置的代码的信任程度放松 DbC 检查。你不应该直接删除检查的代码,而只需要禁用检查(添加注释)。这样如果发现错误,就可以轻松地恢复检查以快速发现问题。

测试驱动开发

测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。TDD 不是一种开发工具,也不是一种测试方法, 它是一种编码之前进行单元测试的软件开发思想,这有助于编写简洁可用和高质量的代码,并加速开发过程。
在这里插入图片描述

纯粹的 TDD 主义者会在实现新功能之前就为其编写测试,这称为测试优先的开发。

日志

日志会给出正在运行的程序的各种信息。

在调试期间,日志也能带来好处。 如果没有日志,你可能会尝试通过插入 System.out.println()语句来打印出程序的行为。如果你想在调试期间不执行某些代码,可以通过定义一个变量来控制对它的访问。但是,该技术的一个缺点是你必须重新编译代码才能启用和关闭跟踪语句。

public class Test {
    private static final boolean debug = false;
    public static void main(String[] args) {
        if (debug){
            //filtering code
        }
    }
}

业内普遍认为标准 Java 发行版本中的日志包 (java.util.logging) 的设计相当糟糕。 大多数人会选择其他的替代日志包。如 Simple Logging Facade for Java(SLF4J) ,它为多个日志框架提供了一个封装好的调用方式,这些日志框架包括 java.util.logging , logback 和 log4jSLF4J 允许用户在部署时插入所需的日志框架。

public class Test {
    private static Logger log =
            LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) {
        log.info("hello logger");
        /** Output:
         * [main] INFO com.test.Test - hello logger
         */
    }
}

日志输出中的格式和信息,甚至输出是否正常或“错误”都取决于 SLF4J 所连接的后端程序包是怎样实现的。 在上面的示例中,它连接到的是 logback 库(通过本书的 build.gradle 文件),并显示为标准输出。

日志等级

SLF4J 提供了多个等级的日志消息。下面这个例子以“严重性”的递增顺序对它们作出演示:

public class Test {
    private static Logger log =
            LoggerFactory.getLogger(Test.class);

    public static void main(String[] args) {
        log.trace("trace:logger");
        log.debug("debug:logger");
        log.info("info:logger");
        log.warn("warn:logger");
        log.error("error:logger");
    }
}

级别通常设置在单独的配置文件中,因此你可以重新配置而无需重新编译。 配置文件格式取决于你使用的后端日志包实现。 如果你没有写 xml 文件,日志系统将采取默认配置。

调试

尽管聪明地使用 System.out 或日志信息能给我们带来对程序行为的有效见解,但对于困难问题来说,这种方式就显得笨拙且耗时了。你也可能需要更加深入地理解程序,仅依靠打印日志做不到。此时你需要调试器。除了比打印语句更快更轻易地展示信息以外,调试器还可以设置断点,并在程序运行到这些断点处暂停程序。使用调试器,可以展示任何时刻的程序状态,查看变量的值,一步一步运行程序,连接远程运行的程序等等。特别是当你构建较大规模的系统(bug 容易被掩埋)时,熟练使用调试器是值得的。

使用 JDB 调试

Java 调试器(JDB)是 JDK 内置的命令行工具。从调试的指令和命令行接口两方面看的话,JDB 至少从概念上是 GNU 调试器(GDB,受 Unix DB 的影响)的继承者。**JDB **对于学习调试和执行简单的调试任务来说是有用的,而且知道只要安装了 JDK 就可以使用 JDB 是有帮助的

假设你想调试如下程序:

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println(i+1);
        }
    }
}

为了运行 JDB,你需要在编译代码时加上 -g 标记,从而告诉编译器生成编译信息。然后使用如下命令开始调试程序:jdb 类名,接着 JDB 就会运行,出现命令行提示。你可以输入 ? 查看可用的 JDB 命令。案例如下

D:\study\src\com\study\test>jdb Test
正在初始化jdb...
> run
运行Test
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
>
VM 已启动: 1
2
3
4
5

应用程序已退出

图形化调试器

使用类似 JDB 的命令行调试器是不方便的。它需要显式的命令去查看变量的状态(locals, dump),列出源代码中的执行点(list),查找系统中的线程(threads),设置断点(stop in, stop at)等等。使用图形化调试器更加高效、更快速地追踪 bug,只需要点击几下,不需要使用显式的命令就能使用这些特性,而且能查看被调试程序的最新细节。IBM 的 Eclipse,Oracle 的 NetBeansJetBrainsIntelliJ 这些集成开发环境都含有面向 Java 语言的好用的图形化调试器。

基准测试

用来测量机器的硬件最高实际运行性能,以及软件优化的性能提升效果。像 Java 这样拥有复杂的运行时系统的编程语言中,基准测试变得更有挑战性。为了生成可靠的数据,环境设置必须控制诸如 CPU 频率,节能特性,其他运行在相同机器上的进程,优化器选项等等。

微基准测试

微基准测试程序用来测量一个计算机系统的某一特定方面,如CPU定点/浮点性能、存储器速度、I/O速度、网络速度或系统软件性能(如同步性能)

public class Test {

    private long start = System.nanoTime();

    public long duration() {
        return NANOSECONDS.toMillis(System.nanoTime() - start);
    }

    public static long duration(Runnable test) {
        Test timer = new Test();
        test.run();
        return timer.duration();
    }

    static final int SIZE = 5_000_000;

    public static void main(String[] args) {
            long[] la = new long[SIZE];
            Random r = new Random();
            System.out.println("parallelSetAll: " + Test.duration(() 
            -> Arrays.parallelSetAll(la, n -> r.nextLong())));
            System.out.println("setAll: " + Test.duration(() 
            -> Arrays.setAll(la, n -> r.nextLong())));
            SplittableRandom sr = new SplittableRandom();
            System.out.println("parallelSetAll: " + Test.duration(() 
            -> Arrays.parallelSetAll(la, n -> sr.nextLong())));
            System.out.println("setAll: " + Test.duration(() 
            -> Arrays.setAll(la, n -> sr.nextLong())));
        /** Output:
         *  parallelSetAll: 994
         *  setAll: 91
         *  parallelSetAll: 64
         *  setAll: 21
         */
    }
}

这是一个很直接的计时方式。通过一些代码然后看它的运行时长。我们比较了 Arrays.setAll()Arrays.parallelSetAll() 的性能。SplittableRandom 是为并行算法设计的,它当然看起来比普通的 RandomparallelSetAll() 中运行得更快。 但是看上去还是比非并发的 setAll() 运行时间更长,有点难以置信(也许是真的,但我们不能通过一个坏的微基准测试得到这个结论)。这个并行的版本会尝试使用多个处理器加快完成任务。然而非并行的版本似乎运行得更快,尽管在不同的机器上结果可能不同。

这只考虑了微基准测试的问题。Java 虚拟机 Hotspot 也非常影响性能。如果你在测试前没有通过运行代码给 JVM 预热,那么你就会得到“冷”的结果,不能反映出代码在 JVM 预热之后的运行速度(假如你运行的应用没有在预热的 JVM 上运行,你就可能得不到所预期的性能,甚至可能减缓速度)。

优化器有时可以检测出你创建了没有使用的东西,或者是部分代码的运行结果对程序没有影响。如果它优化掉你的测试,那么你可能得到不好的结果。

一个良好的微基准测试系统能自动地弥补像这样的问题(和很多其他的问题)从而产生合理的结果,但是创建这么一套系统是非常棘手,需要深入的知识。

JMH

JMHJava Micro Benchmark Harness的简写,是一个微基准测试框架。JMH是由实现Java虚拟机的团队开发的,因此他们非常清楚开发者所编写的代码在虚拟机中将会如何执行。由于现代JVM已经变得越来越智能,在Java文件的编译阶段、类的加载阶段,以及运行阶段都可能进行了不同程度的优化,因此开发者编写的代码在运行中未必会像自己所预期的那样具有相同的性能体现,JVM的开发者为了让普通开发者能够了解自己所编写的代码运行的情况,JMH便因此而生。

再讲解之前先了解下注解的含义:

@BenchmarkMode表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前 JMH 共有四种模式:

  • Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”,单位是操作数/时间。
  • AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”,单位是时间/操作数。
  • SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
  • SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。

@OutputTimeUnit输出的时间单位。
@IterationJMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 Benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。
@WarmUp是指在实际进行 Benchmark 前先进行预热的行为。如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 Benchmark 的结果更加接近真实情况就需要进行预热。
@State类注解,它定义了一个类实例的生命周期,可以类比 Spring Bean 的 Scope。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:

  • Scope.Thread:默认的 State,每个测试线程分配一个实例;
  • Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能;
  • Scope.Group:每个线程组共享一个实例;

@Fork进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
@Meansurement提供真正的测试阶段参数。指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量(通常使用 @BenchmarkMode(Mode.SingleShotTime) 测试一组操作的开销——而不使用循环)
@Setup用于初始化。
@TearDown主要用于资源的回收等。
@Benchmark表示该方法是需要进行 benchmark 的对象。
@Param用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param 注解接收一个String数组,在 @Setup 方法执行前转化为为对应的数据类型。多个 @Param 注解的成员之间是乘积关系,譬如有两个用 @Param 注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5*2=10次。

@BenchmarkMode(Mode.Throughput) // 吞吐量
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 结果所使用的时间单位
@State(Scope.Thread) // 每个测试线程分配一个实例
@Fork(2) // Fork的数目
@Warmup(iterations = 1) // 先预热1轮
@Measurement(iterations = 2) // 进行10轮测试
public class Test {
    /**
     *  定义四个参数,之后会分别对这四个参数进行测试
     */
    @Param({"1", "2", "3", "4"})
    private int n;

    private List<Integer> array;

    private List<Integer> list;

    /**
     * 初始化方法,在全部Benchmark运行之前进行
     */
    @Setup(Level.Trial)
    public void init() {
        array = new ArrayList<>(0);
        list = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            array.add(i);
            list.add(i);
        }
    }

    @Benchmark
    public void arrayTraverse() {
        for (int i = 0; i < n; i++) {
            array.get(i);
        }
    }

    @Benchmark
    public void listTraverse() {
        for (int i = 0; i < n; i++) {
            list.get(i);
        }
    }

    /**
     * 结束方法,在全部Benchmark运行之后进行
     */
    @TearDown(Level.Trial)
    public void arrayRemove() {
        for (int i = 0; i < n; i++) {
            array.remove(0);
            list.remove(0);
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder().include(Test.class.getSimpleName()).build();
        new Runner(options).run();
    }
}

打印结果

# JMH version: 1.21
# VM version: JDK 1.8.0_101, Java HotSpot(TM) 64-Bit Server VM, 25.101-b13
# VM invoker: C:\Program Files\Java\jdk1.8.0_101\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.3.5\lib\idea_rt.jar=53166:D:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.3.5\bin -Dfile.encoding=UTF-8
# Warmup: 1 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.pro.service.Test.arrayTraverse
# Parameters: (n = 1)

# Run progress: 0.00% complete, ETA 00:08:00
# Fork: 1 of 2
# Warmup Iteration   1: 311987.285 ops/ms
Iteration   1: 278448.512 ops/ms
Iteration   2: 244808.077 ops/ms

# Run progress: 6.25% complete, ETA 00:07:46
# Fork: 2 of 2
# Warmup Iteration   1: 285601.991 ops/ms
Iteration   1: 296701.241 ops/ms
Iteration   2: 269544.495 ops/ms

剖析和优化

有时你必须检测程序运行时间花在哪儿,从而看是否可以优化那一块的性能。剖析器可以找到这些导致程序慢的地方,因而你可以找到最轻松,最明显的方式加快程序运行速度。剖析器收集的信息能显示程序哪一部分消耗内存,哪个方法最耗时。一些剖析器甚至能关闭垃圾回收,从而帮助限定内存分配的模式。剖析器还可以帮助检测程序中的线程死锁。注意剖析和基准测试的区别。剖析关注的是已经运行在真实数据上的整个程序,而基准测试关注的是程序中隔离的片段,通常是去优化算法。

安装 Java 开发工具包(JDK)时会顺带安装一个虚拟的剖析器,叫做 VisualVM。它会被自动安装在与 javac 相同的目录下,你的执行路径应该已经包含该目录。启动 VisualVM 的控制台命令是:

>jvisualvm

运行该命令后会弹出一个窗口,其中包括一些指向帮助信息的链接。
在这里插入图片描述

优化准则

  • 避免为了性能牺牲代码的可读性。
  • 不要独立地看待性能。衡量与带来的收益相比所需投入的工作量。
  • 程序的大小很重要。性能优化通常只对运行了长时间的大型项目有价值。性能通常不是小项目的关注点。
  • 运行起来程序比一心钻研它的性能具有更高的优先级。一旦你已经有了可工作的程序,如有必要的话,你可以使用剖析器提高它的效率。只有当性能是关键因素时,才需要在设计/开发阶段考虑性能。
  • 不要猜测瓶颈发生在哪。运行剖析器,让剖析器告诉你。
  • 无论何时有可能的话,显式地设置实例为 null 表明你不再用它。这对垃圾收集器来说是个有用的暗示。
  • static final 修饰的变量会被 JVM 优化从而提高程序的运行速度。因而程序中的常量应该声明 static final

风格检测

当你在一个团队中工作时(包括尤其是开源项目),让每个人遵循相同的代码风格是非常有帮助的。这样阅读项目的代码时,不会因为风格的不同产生思维上的中断。然而,如果你习惯了某种不同的代码风格,那么记住项目中所有的风格准则对你来说可能是困难的。或许你可以使用Checkstyle工具进行风格检测。

静态错误分析

尽管 Java 的静态类型检测可以发现基本的语法错误,其他的分析工具可以发现躲避 javac 检测的更加复杂的bug。一个这样的工具叫做 Findbugs。这会为每一章生成一个名为 main.html 的报告,报告中会说明代码中潜在的问题。Gradle 命令的输出会告诉你每个报告在何处。当你查看报告时,你将会看到很多 false positive 的情况,即代码没问题却报告了问题

代码重审

单元测试能找到明显重要的 bug 类型,风格检测和 Findbugs 能自动执行代码重审,从而发现额外的问题。最终你走到了必须人为参与进来的地步。代码重审是一个或一群人的一段代码被另一个或一群人阅读和评估的众多方式之一。这最初看起来会使人不安,而且需要情感信任,但它的目的肯定不是羞辱任何人。它的目标是找到程序中的错误,代码重审是最成功的能做到这点的途径之一。可惜的是,它们也经常被认为是“过于昂贵的”(有时这会成为程序员避免代码被重审时感到尴尬的借口)。

代码重审可以作为结对编程的一部分,作为代码签入过程的一部分(另一个程序员自动安排上审查新代码的任务)或使用群组预排的方式,即每个人阅读代码并讨论之。后一种方式对于分享知识和营造代码文化是极其有益的。

结对编程

结对编程是指两个程序员一起编程的实践活动。通常来说,一个人“驱动”(敲击键盘,输入代码),另一人(观察者或指引者)重审和分析代码,同时也要思考策略。这产生了一种实时的代码重审。通常程序员会定期地互换角色。而且两个人一起工作时,可以更容易地推进开发的进展,而只有一个程序员的话,可能被轻易地卡住。结对编程的程序员通常可以从工作中感到更高的满足感。有时很难向管理人员们推行结对编程,因为他们可能觉得两个程序员解决同一个问题的效率比他们分开解决不同问题的效率低。尽管短期内是这样,但是结对编程能带来更高的代码质量;除了结对编程的其他益处,如果你眼光长远的话,这会产生更高的生产力。

重构

技术负债是指迭代发展的软件中为了应急而生的丑陋解决方案从而导致设计难以理解,代码难以阅读的部分。特别是当你必须修改和增加新特性的时候,这会造成麻烦。重构可以矫正技术负债。重构的关键是它能改善代码设计,结构和可读性(因而减少代码负债),但是它不能改变代码的行为。

很难向管理人员推行重构:“我们将投入很多工作不是增加新的特性,当我们完成时,外界无感知变化。但是相信我们,事情会变得更加美好”。不幸的是,管理人员意识到重构的价值时都为时已晚了:当他们提出增加新的特性时,你不得不告诉他们做不到,因为代码基底已经埋藏了太多的问题,试图增加新特性可能会使软件崩溃,即使你能想出怎么做。

重构基石

在开始重构代码之前,你需要有以下三个系统的支撑:

  • 测试(通常,JUnit 测试作为最小的根基),因此你能确保重构不会改变代码的行为。
  • 自动构建,因而你能轻松地构建代码,运行所有的测试。通过这种方式做些小修改并确保修改不会破坏任何事物是毫不费力的。
  • 版本控制,以便你能回退到可工作的代码版本,能够一直记录重构的每一步。

没有这三个系统的支持,重构几乎是不可能的。确实,没有这些系统,起初维护和增加代码是一个巨大的挑战。令人意外的是,有很多成功的公司竟然在没有这三个系统的情况下在相当长的时间里勉强过得去。然而,对于这样的公司来说,在他们遇到严重的问题之前,这只是个时间问题。

持续集成

在软件开发的早期,人们只能一次处理一步,所以他们坚信他们总是在经历快乐之旅,每个开发阶段无缝进入下一个。在这片童话的土地上,每一步都按照指定的预计时间准时完美结束,然后下一步开始。当最后一步结束时,所有的部件都可以无缝地滑在一起,瞧,一个装载产品诞生了!当然,现实中没有事能按计划或预计时间运作。因为受流水工作线的思路影响,所以每个开发阶段都有自己的团队。上游团队的延期传递到下游团队,当到了需要进行测试和集成的时候,这些团队被指望赶上预期时间,当他们必然做不到时,就认为他们是“差劲的团队成员”。不可能的时间安排和负相关的结合产生了自实现的预期:只有最绝望的开发者才会乐意做这些工作。

有许多不同的想法导向这种方式,但是目前首要的术语叫持续集成(CI)。CI 与导向 CI 的想法之间的不同之处在于 CI 是一种独特的机械式的过程,过程中涵盖了这些想法;它是一种定义好的做事方式。事实上,它定义得如此明确以至于整个过程是自动化的。

当前 CI 技术的高峰是持续集成服务器。这是一台独立的机器或虚拟机,通常是由第三方公司托管的完全独立的服务。这些公司通常免费提供基本服务,如果你需要额外的特性如更多的处理器或内存或者专门的工具或系统,你需要付费。CI 服务器起初是完全空白状态,即只是可用的操作系统的最小配置。这很重要因为你可能之前在你的开发机器上安装过一些程序,却没有在你的构建和部署系统中包含它。正如重构一样,持续集成需要分布式版本管理,自动构建和自动测试系统作为基础。通常来说,CI 服务器会绑定到你的版本控制仓库上。当 CI 服务器发现仓库中有改变时,就会拉取最新版本的代码,并按照 CI 脚本中的过程处理。这包括安装所有必要的工具和类库(记住,CI 服务器起初只有一个干净、基本的操作系统),所以如果过程中出现任何问题,你都可以发现它们。接着它会执行脚本中定义的构建和测试操作;通常脚本中使用的命令与人们在安装和测试中使用的命令完全相同。如果执行成功或失败,CI 服务器会有许多种方式汇报给你,包括在你的代码仓库上显示一个简单的标记。

使用持续集成,每次你合进仓库时,这些改变都会被从头到尾验证。通过这种方式,一旦出现问题你能立即发现。甚至当你准备交付一个产品的新版本时,都不会有延迟或其他必要的额外步骤(在任何时刻都可以交付叫做持续交付)。

本章小结

代码校验不是单一的过程或技术。每种方法只能发现特定类型的 bug,作为程序员的你在开发过程中会明白每个额外的技术都能增加代码的可靠性和鲁棒性。校验不仅能在开发过程中,还能在为应用添加新功能的整个项目期间帮你发现更多的错误。现代化开发意味着比仅仅编写代码更多的内容,每种你在开发过程中融入的测试技术—— 包括而且尤其是你创建的能适应特定应用的自定义工具——都会带来更好、更快和更加愉悦的开发过程,同时也能为客户提供更高的价值和满意度体验。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值