# 04 | 实战:构建一个Kotlin版本的四则运算计算器 你好,我是朱涛。 前面几节课,我们学了不少Kotlin的语法,也算是对Kotlin有了一个基本认识。不过,单纯只认识Kotlin是远远不够的,我们还要**会用Kotlin**。当遇到一个具体问题的时候,我们得能用Kotlin来解决这个问题。换句话说,就是要实战。在实战的过程中,我们对Kotlin的理解也会进一步加深。 那么这节课,我们就把前面的知识点串联起来,一起做一个Kotlin版本的计算器。为了便于理解,我会以**循序渐进**的方式来编写这个计算器程序,由简单到复杂。你在这个由易到难的实操过程中,可以实际体会到Kotlin的代码实现思路以及编码方式的变化,进而也就能更好地掌握和运用前面所学的基础语法,以及与面向对象相关的知识点。 这个计算器程序大致会分为三个版本: * 计算器1.0,实现两个整数的“加减乘除”,对输入数据有严格要求。 * 计算器2.0,对输入数据无严格要求,融入面向对象的编程思想。 * 计算器3.0,支持“大数的加法”,增加单元测试。 现在,我们就开始实战吧。 ## 创建Kotlin工程 如果你之前没有使用过IntelliJ或Android Studio,你可能还不知道怎么创建一个工程。别担心,这个过程其实很简单,它分为以下几个步骤。 * 第一步:选择菜单“File -> New -> Project”。 * 第二步:选中菜单左边的“Gradle”,然后在右边勾选“Java 和 Kotlin/JVM”,最后点击右下角的“Next”。 * 第三步:给工程取一个你喜欢的名字,我们这里就用Calculator。GroupId这个地方一般使用倒过来的域名,这里根据你的实际情况填写即可。默认情况下,IDE会自动帮你设置成“org.example”,所以你不去改动它也没问题。最后,我们点击Finish,工程就创建成功了! * 第四步:等待工程配置完成。**如果你是第一次创建Kotlin工程,点击Finish以后,你可能需要等待一段时间,IDE需要下载Gradle,然后用Gradle下载工程所需的依赖。**当你在IDE当中能看到这样的工程结构时,这个工程就算配置完成了。 ![图片](https://static001.geekbang.org/resource/image/89/9f/89cyycca9830e62538528df5e62fbc9f.gif?wh=1000x750) ## 导入初始化工程 其实,你只需要知道如何创建一个Kotlin工程就行了,也没必要真的跟着我一步步操作。课程配套的源代码已经在[GitHub](https://github.com/chaxiu/Calculator.git)开源,你可以将其下载下来并切换到start分支,这样就可以跟着课程一步步实现计算器的三个版本了。 **具体做法是这样的:**打开IntelliJ,点击“Get from VCS”按钮,接着在弹出的窗口中,填入我们的GitHub URL“[https://github.com/chaxiu/Calculator.git](https://github.com/chaxiu/Calculator.git)”,然后点击右下角的Clone按钮即可。 ![图片](https://static001.geekbang.org/resource/image/47/03/477cd386aa83dddb7e2ab659b433f503.gif?wh=1000x750) 等代码下载完成以后,IDE会问你是否要打开此工程,我们选择打开。这样,我们的计算器工程就算导入进来了。 最后,我们还需要将工程改为初始化状态,借助Git我们可以非常方便地实现: * 在IntelliJ的右下角,找到main按钮并且点击; * 在弹出的菜单中,点击start分支; * 最后,点击Checkout,代表将当前代码切换到初始状态。 ![图片](https://static001.geekbang.org/resource/image/73/a7/730f5377ef2ee6e9d8dbe9179eb849a7.png?wh=581x379) 这样,我们就完成了整个工程的初始化配置了。为了测试我们的开发环境是否已经配置好,我们可以打开工程里的HelloWorld文件,运行一下,看看程序是否正常执行。 ![图片](https://static001.geekbang.org/resource/image/db/1f/db316ef784abb04cd3fc87fcd9b0a31f.gif?wh=976x544) 如果你也能在工程当中看见控制台输出“Hello world.”,说明你的开发环境已经完全没问题了。接下来,就让我们一起用Kotlin完成计算器的1.0版本吧! ## 计算器1.0 第一个版本的计算器,它的功能非常简单,你可以看看下面的动图演示。 ![图片](https://static001.geekbang.org/resource/image/dd/10/dd01d14119706f7eeef220576dda5510.gif?wh=596x359) 我们大致列举一下这个计算器的功能需求: * 交互式界面,输入算式,按下回车,程序就会帮我们计算出结果; * 数字与字符之间要求有空格,“1 + 1”是可以的,“1+1”则不行; * 输入exit,按下回车,程序就会退出; * 支持“加减乘除”,四种运算,仅支持两个数的运算。 搞清楚功能需求以后,我们就可以开始写代码了。 首先,我们要创建一个Kotlin源代码文件:在Kotlin文件夹下,点击右键,选择“New -> Kotlin Class/File”,然后填写文件名字即可,这里我们创建一个名为Calculator的Kotlin文件。 ![图片](https://static001.geekbang.org/resource/image/32/19/32be58b2b1ee01ccb888yy862546d019.png?wh=815x225) 由于我们的程序要和命令进行交互,根据不同的命令来做出不同的行为,因此,我们的程序需要有一个 **while循环**的逻辑,在循环当中,还要读取命令行的输入,然后根据输入的结果来判断执行逻辑。我们可以将整个程序分为以下几个步骤: * 初始化,打印提示信息; * 第一步,读取输入命令; * 第二步,判断命令是不是exit,如果用户输入的是“exit”则直接退出程序; * 第三步,解析算式,分解出“数字”与“操作符”:“1”“+”“2”; * 第四步,根据操作符类型,算出结果:3; * 第五步,输出结果:1 + 2 = 3; * 第六步,进入下一个while循环。 ```plain fun main() { while(true) { // 初始化,打印提示信息 println("请输入标准的算式,并且按回车; \n" + "比如:1 + 1,注意符合与数字之间要有空格。\n" + "输入exit,退出程序。") // 第一步,读取输入命令; var input = readLine() if (input == null) continue // 第二步,判断命令是不是exit,如果是则直接退出程序; if (input == "exit") exitProcess(0) // 第三步,解析算式,分解出“数字”与“操作符”:“1”“+”“2”; var inputList = input.split(" ") // 第四步,根据操作符类型,算出结果:3; var result = calculate(inputList) // 第五步,输出结果:1 + 2 = 3; if (result == null) { println("输入格式不对") continue } else { println("$input = $result") } // 第六步,进入下一个while循环。 } } // 具体计算逻辑 private fun calculate(inputList: List): Int? { if (inputList.size != 3) return null // 第七步,取出数字和操作符 var left = inputList.get(0).toInt() var operation = inputList.get(1) var right = inputList.get(2).toInt() // 第八步,根据操作符的类型,执行计算 when(operation) { "+" -> return left + right "-" -> return left - right "*" -> return left * right "/" -> return left / right else -> return null } } ``` 上面的代码非常简单直白,即使你没有任何编程经验,应该也能够理解。它也非常符合人的编程直觉。 不过,站在Kotlin的角度上看,以上的代码其实是有不少问题的,让我们通过一个图来对比着看: ![图片](https://static001.geekbang.org/resource/image/84/03/84cac3866e2b1e14fd3ae6dc68074a03.png?wh=1240x1016) * 箭头①,表示程序中的“提示信息”应该使用Kotlin的“三引号”的原始字符串,这样的话,我们可以省去繁琐的“\\n和+”,并且所见即所得; * 箭头②,表示读取输入命令后,我们可以直接使用Elvis表达式,两行代码就会变成一行; * 箭头③,表示程序中所有的var都应该改为val,我在[第1讲](https://time.geekbang.org/column/article/472154)中说过,在Kotlin当中,我们应该优先使用val,尽量避免使用可变的变量。 * 箭头④,表示inputList.get(i)可以改为inputList\[i\],这是因为Kotlin统一了数组和集合的元素访问操作,我们再也不用担心弄混了。 * 箭头⑤,表示了两点。首先,我们可以将return放到when表达式的前面,这样就省得我们每个分支都写一遍return。另外,当我们使用when表达式的时候,应该尽量结合“枚举”或者“密封类”来使用。为此,我们可以为“加减乘除”四个操作符创建一个枚举类。这样,when表达式的分支会自动判定完备,而不需要else分支了。 那么经过调整,最终的源代码应该是这样的: ``` val help = """ -------------------------------------- 使用说明: 1. 输入 1 + 1,按回车,即可使用计算器; 2. 注意:数字与符号之间要有空格; 3. 想要退出程序,请输入:exit --------------------------------------""".trimIndent() fun main() { while (true) { println(help) val input = readLine() ?: continue if (input == "exit") exitProcess(0) val inputList = input.split(" ") val result = calculate(inputList) if (result == null) { println("输入格式不对") continue } else { println("$input = $result") } } } private fun calculate(inputList: List): Int? { if (inputList.size != 3) return null val left = inputList[0].toInt() // ① // ↓ val operation = Operation.valueOf(inputList[1]) val right = inputList[2].toInt() return when (operation) { Operation.ADD -> left + right Operation.MINUS -> left - right Operation.MULTI -> left * right Operation.DIVI -> left / right } } enum class Operation(val value: String) { ADD("+"), MINUS("-"), MULTI("*"), DIVI("/") } ``` 好,我们的计算器1.0版本,到这里就算是完成了。 如果你跟随着我,一起来实现了这个简单的计算器,那么你在这个实操过程中就可以体会到,Kotlin编程与传统的Java/C之间确实是存在着一定的差别的。 **想要学会Kotlin语法其实不难,但要写出优雅的Kotlin代码,却不是一件容易的事情。**我们唯一能做的,就是多写Kotlin代码,同时多看优秀的Kotlin代码,以及多思考改进自己已有的代码。 不过,代码中注释①处其实还有一些问题,接着让我们进入第二个版本的开发吧! ## 计算器2.0 在2.0版本中,我们会分成两个阶段: * 第一个阶段,**融入面向对象的思想**。1.0版本中,我们只写了两个函数,一个是main()函数,另一个是calculate()函数。虽然这样的设计非常直观且便于理解,但却不太符合我们工程界的思维习惯。我们应该将程序封装到一个类当中,并且尽量让每个函数的功能划分清楚,保持每个函数尽量简单。 * 第二个阶段,**兼容输入格式**。1.0版本中,我们对输入有严格的要求,数字和符号之间必须有空格,否则我们的算式解析会出错。在2.0版本中,我们尝试兼容不同的输入格式,不管数字和符号之间有没有空格,我们都要能成功执行。 让我们一步步来,首先是融入面向对象的思想。 ### 第一阶段:融入面向对象思想 具体做法其实也很简单,我们可以将前面定义的两个函数收拢到一个类当中去,比如“CalculatorV2”: ```plain class CalculatorV2 { fun start() {} fun calculate(input: String): Int? {} } ``` 可以看到,在这个CalculatorV2类当中有两个方法,**start()** 用于启动我们的计算器程序,监听控制台的文本输入;**calculate(input)** 用于接收输入文本,计算出算式的结果,然后返回一个可为空的整型,当输入不合法的时候会返回null。 这样,我们的计算器作为一个整体已经是一个对象了,我们可以很方便地在main()函数当中,创建一个实例,并且调用它的start()函数。这样一来,我们的计算器也就可以充分发挥出面向对象的优势。 ```plain fun main() { val calculator = CalculatorV2() calculator.start() } ``` 除了计算器本身需要面向对象,我们的输入表达式也可以抽象出一个具体模型出来。 我们知道,一个算式分为左边的数字、操作符和右边的数字。因此我们还可以定义一个类,来代表算式表达式。 ```plain data class Expression( val left: String, val operator: Operation, val right: String ) ``` 比如,我们想要表达“1 + 2”这个式子的话,我们就可以用这样一个结构来表示: ```plain Expression("1", Operation.ADD, "2") ``` 那么,在完成了面向对象的模型化以后,我们还需要进一步拆分函数的职责与颗粒度。其中,start()方法,主要用于控制程序的流程、输入与输出: ```plain fun start() { while (true) { println(HELP) val input = readLine() ?: continue val result = calculate(input) if (result == null) { println("输入格式不对") continue } else { println("$input = $result") } } } ``` 而calculate()方法,则需要进一步地拆分: ```plain fun calculate(input: String): String? { if (shouldExit(input)) exitProcess(0) val exp = parseExpression(input) ?: return null val left = exp.left val operator = exp.operator val right = exp.right return when (operator) { Operation.ADD -> addString(left, right) Operation.MINUS -> minusString(left, right) Operation.MULTI -> multiString(left, right) Operation.DIVI -> diviString(left, right) } } fun addString(left: String, right: String): String { val result = left.toInt() + right.toInt() return result.toString() } fun minusString(left: String, right: String): String { val result = left.toInt() - right.toInt() return result.toString() } fun multiString(left: String, right: String): String { val result = left.toInt() * right.toInt() return result.toString() } fun diviString(left: String, right: String): String { val result = left.toInt() / right.toInt() return result.toString() } fun shouldExit(input: String): Boolean { return input == EXIT } fun parseExpression(input: String): Expression? { // 待完成 } fun parseOperator(input: String): Operation? { // 待完成 } ``` 通过以上代码可以看到,我们拆分calculate()方法主要做了三件事: * 第一,**将“是否退出”的逻辑封装到了shouldExit()方法当中**,如果将来这部分逻辑变得更复杂,我们只改动这一个方法即可。 * 第二,**将算式的解析,封装到了parseExpression()方法当中**,而解析算式的时候也需要解析操作符,这时候我们也需要parseOperator()。 * 第三,**将具体的计算逻辑交给了对应的方法**。这么做的原因,是可以让我们的程序变得更加灵活。比如,我们在下个版本当中会更改“加法”的计算逻辑,那么我们就只需要改动这一个方法就行了。 同时,以上所有独立抽出来的方法,它们也都将变得**可测试**,这有利于提升程序的稳定性。 到这里,我们对计算器2.0的第一阶段改造就差不多完成了,我们融入了面向对象的思想,也对calculate()方法进行了更细颗粒度的拆分。下一步,我们要做的就是兼容算式的格式,让它能够解析没有空格的算式。 ### 第二阶段:兼容输入格式 现在,假设我们的输入是“1+2”,数字与字符之间没有空格。那在这种情况下,我们就无法使用空格作为分隔符了。所以要换一种方式,想办法从算式当中,解析出操作符“加减乘除”中的一种,然后再用操作符作为我们的分隔符去找出数字。 其实,因为操作符只有这四种情况,所以我们很容易就能想到一种方案,一个个去尝试: ```plain fun parseOperator(input: String): Operation? { return when { input.contains(Operation.ADD.value) -> Operation.ADD input.contains(Operation.MINUS.value) -> Operation.MINUS input.contains(Operation.MULTI.value) -> Operation.MULTI input.contains(Operation.DIVI.value) -> Operation.DIVI else -> null } } ``` 虽然,这段代码运行起来没什么问题,逻辑也非常得清晰,但它看起来很丑陋。而且它还有一个坏处:随着枚举类型的增多,我们的逻辑分支也会增多,手动添加起来也特别麻烦。 因此这种情况,我们就应该充分借助 **Kotlin枚举**的优势,通过遍历的方式来做: ```plain fun parseOperator(input: String): Operation? { Operation.values().forEach { if (input.contains(it.value)) { return it } } return null } ``` 可以看到,优化后的代码中,我们不再需要手动地去写when的逻辑分支,也不必自己去枚举Operation的每一种情况,就连代码量也降低了很多,即使将来枚举种类增加了,我们也不必修改这部分代码了。 需要注意,以上的代码中,我们用到了集合遍历的语法“forEach”,你可以将它想象成强化版的for循环,它具体的用法我会在后面“Kotlin集合”那一节中讲解。 现在,对于一个算式“1+2”,我们已经可以成功解析出操作符“+”了,接下来要做的,就是通过“+”来分割字符串,将左右两个数字取出来“1”“2”。这个逻辑就很简单了: ```plain fun parseExpression(input: String): Expression? { // 解析操作符 val operation = parseOperator(input) ?: return null // 用操作符分割算式,拿到数字 val list = input.split(operation.value) if (list.size != 2) return null return Expression( // 算式左边 left = list[0].trim(), operator = operation, // 算式右边 right = list[1].trim() ) } ``` 以上代码大致分为三个步骤,我们以“1+2”为例: * 第一个步骤,调用parseOperator()方法解析操作符“+”; * 第二个步骤,根据操作符分割算式的数字,分割之后得到的会是“1”“2”组成的列表; * 第三个步骤,将操作符、数字组合成Expression对象。 这里有一个细节需要注意,我们兼容的输入其实有两种情况,第一种是**不包含空格**“1+2”,那么我们解析出来的数字会是“1”“2”,这种情况下不会有问题;但还有第二种情况**包含空格**,对于原本正确的格式,我们更应该支持,比如“1 + 2”,被分隔之后的结果会是“1 ”“2”,这两个数字当中是包含空格符的。 所以,我们使用了`list[0].trim()`,这里的`trim()`方法就是用于去掉多余空格的。 让我们实际运行一下看看效果: ![图片](https://static001.geekbang.org/resource/image/ce/b2/ce2c6debc469a274d7d0a6fd488978b2.gif?wh=852x452) 至此,我们的计算器2.0版本就完成了。在2.0版本的实操过程中,我们其实是在原有的基础上,融入了面向对象的思想,将计算器功能收拢到了一个类当中,同时也对计算器内部的方法进行了细颗粒度的拆分。 在这个过程中,我们创建了三个类:“Calculator”类,代表整个计算器;“Operation”枚举类,代表加减乘除四种运算操作符;“Expression”数据类,代表我们算式当中的数字和操作符。之后,我们又对计算器的核心功能进行了更细颗粒度的拆分,提高了程序的灵活性,为我们的功能扩展打下了基础。 好了,现在让我们进入3.0版本的开发吧。 ## 计算器3.0 针对3.0这个版本,我们也分为了两个阶段: * 第一阶段,**增加单元测试**。单元测试是软件工程当中的一个概念,它指的是对软件当中的最小可执行单元进行测试,以提高软件的稳定性。在Java当中,最小单元一般会认为是类,因此,我们一般会以类为单元,对类当中的方法进行一一测试。 * 第二阶段,**支持大数的加法**。我们知道Java、Kotlin当中的整型都是有范围限制的,如果我们输入两个特别大的数字进行计算,那么程序是无法正常工作的。因此,我们需要对特别大的数进行兼容。 下面,我们先来搞定单元测试。 ### 第一阶段:单元测试 在Kotlin当中,如果要使用单元测试,我们需要在gradle文件当中,添加Kotlin官方提供的依赖: ```plain testImplementation 'org.jetbrains.kotlin:kotlin-test' ``` 这样,我们的工程就拥有单元测试的能力了。单元测试的代码,我们一般会放在工程的test目录下: ![图片](https://static001.geekbang.org/resource/image/c3/1f/c305f53c828d94e671f7e89267c2111f.png?wh=395x629) 我们可以从这个图中看出很多信息: * 第一,test目录、main目录,它们是平级的目录,内部拥有着相同的结构。main目录下放的是功能代码,test目录下放的则是测试代码。 * 第二,由于我们要开发3.0版本,所以我们在main目录下创建了CalculatorV3这个类;另外,由于我们需要在3.0版本加入单元测试,所以对应的,我们在test目录下相同的地方,创建了TestCalculatorV3。这两个类的关系是一一对应的,CalculatorV3是为了实现3.0版本的功能,TestCalculatorV3是为了测试3.0版本的功能,确保功能正常。 不过这里你要**注意**,虽然我们创建了CalculatorV3这个类,但其实它里面的代码还是用的CalculatorV2的代码。3.0版本的功能,我们放到第二阶段才会去实现。 接下来,让我们来编写测试代码: ```plain class TestCalculatorV3 { @Test fun testCalculate() { val calculator = CalculatorV3() val res1 = calculator.calculate("1+2") assertEquals("3", res1) } } ``` 首先,我们定义了一个方法testCalculate(),并且使用了一个注解@Test来修饰它。因为这样做以后,IntelliJ就会知道:哦,这是一个用来做测试的方法。 接着,我们在testCalculate()当中创建了一个CalculatorV3的对象,然后调用了它的calculate()方法,传入了“1+2”。我们知道,如果程序正常工作的话,返回的结果应该是“3”。因此,我们紧接着就执行了一个**断言**“assertEquals(“3”, res1)”,它的意思是“res1一定等于3”。如果res1=3,那么我们的单元测试就会成功,否则就会失败。 我们可以看看单元测试运行成功的效果: ![图片](https://static001.geekbang.org/resource/image/b1/b1/b1c48ab4dd9c900bc3ee2e020da187b1.gif?wh=1348x960) 如果这时候,我们将“1+2”改成大数的加法,比如“2333333333333332+1”,并且将断言也修改一下: ```plain val res1 = calculator.calculate("2333333333333332+1") assertEquals("2333333333333333", res1) ``` 那么,你觉得单元测试的结果会是怎么样的呢? ![图片](https://static001.geekbang.org/resource/image/b9/bc/b968fc9666cefc95545cd022e7a523bc.gif?wh=1348x960) 单元测试失败了! 具体原因相信你一定也能猜到,因为“2333333333333332”这个数实在太大了,已经远远超出了Int类型的范围,如果不做特殊处理的话,我们的程序是无法正常运行的。而这正好就是我们下一个阶段要做的事情:支持大数的加法。不论多大的两个数字相加,我们都要算出正确的结果。 这个单元测试的代码我们先留着,等我们实现“大数的加法”后,我们再重新运行一遍,这样一来,我们就可以借此验证代码是否正确。 ### 第二阶段:大数加法 大数的加法,其实是我们程序员面试当中的一道高频题。它的解题思路也很简单,就是通过模拟我们**手写加法竖式**的方法,从个位、十位、百位、千位,一直累加,超过10的时候,我们需要进位。 我们以“135+99”为例: ![图片](https://static001.geekbang.org/resource/image/f7/83/f777d39dd91641f5fb0b58a43e9bfc83.gif?wh=405x434) 从上面的手写加法竖式的过程我们可以看出,这个计算其实就是分了三个步骤在进行,分别是个位、十位、百位: * 个位计算,“5+9=14”,出现进位,这时候我们用carry来存储进位:carry=1,个位结果为4; * 十位计算,十位相加“3+9=12”,由于之前有过进位,所以应该是“3+9+1=13”,十位结果为3; * 百位计算,由于99不存在百位,我们自动补零,所以就应该是“1+0+1=2”,百位结果为2。 最终,我们将每一位的结果拼接起来,就得到了最终的结果。有了这样的思路后,我们的代码就很容易实现了。 ```plain fun addString(leftNum: String, rightNum: String): String { // ① val result = StringBuilder() // ② var leftIndex = leftNum.length - 1 var rightIndex = rightNum.length - 1 // ③ var carry = 0 // ④ while (leftIndex >= 0 || rightIndex >= 0) { // ⑤ val leftVal = if (leftIndex >= 0) leftNum.get(leftIndex).digitToInt() else 0 val rightVal = if (rightIndex >= 0) rightNum.get(rightIndex).digitToInt() else 0 val sum = leftVal + rightVal + carry // ⑥ carry = sum / 10 result.append(sum % 10) leftIndex-- rightIndex-- } // ⑦ if (carry != 0) { result.append(carry) } // ⑧ return result.reverse().toString() } ``` 上面的代码一共有8处注释,代表了整体的程序流程,让我们一步步来分析: * 注释①,我们**创建了一个StringBuilder对象,**用于存储最终结果,由于我们的结果是一位位计算出来的,所以每一位结果都是慢慢拼接上去的,在这里,为了提高程序的性能,我们选择使用StringBuilder。 * 注释②,我们**定义了两个可变的变量index**,它们分别指向了两个数字的个位,这是因为我们的计算是从个位开始的。 * 注释③,**carry**,我们用它来存储每一位计算结果的进位。 * 注释④,这个 **while循环**当中,我们会让两个index从低位一直到高位,直到遍历完它们所有的数字位。 * 注释⑤,这里的逻辑是**取每一位上的数字**,其中有个细节就是补零操作,比如当程序运行到百位的时候,99没有百位,这时候rightVal = 0。 * 注释⑥,当我们的程序计算出结果后,我们要**分别算出carry,以及当前位的结果**。这时候我们分别使用“除法”计算carry,使用“取余”操作计算当前位的结果。 * 注释⑦,这里是**为了兼容一个特殊的场景**,在“99+1”的情况下,我们的while循环最多只会遍历到十位,如果不做特殊处理的话,结果将变成“99+1=00”。这并不是我们想要的,所以,为了兼容这种特殊情况,我们**在while循环结束后增加了一个判断**,如果carry=1,那就说明在最大的那一位数计算完以后,仍然有进位,我们要手动添加。 * 注释⑧,对于一个算式“135+99”,我们的result拼接其实是倒叙的“432”,这时候我们需要**将其翻转**一下,才能得到正确的结果“135+99=234”。 到这里,我们的大数加法功能,就算实现了。让我们回过头,再去运行一次单元测试,来验证下我们的代码是否正确: ![图片](https://static001.geekbang.org/resource/image/4d/eb/4dd2dyyf2b31cd5379a7134b0f6822eb.gif?wh=1348x960) 果然,在我们兼容了“大数加法”以后,单元测试就可以成功通过了。至此,我们的计算器3.0版本就算是完成了。 在这个版本的开发过程中,首先我们引入了单元测试,通过这种方式,我们可以测试代码逻辑是否正确,并可以辅助我们排查问题。接着,我们写了一个“2333333333333332+1”的测试用例,并且失败了,不过在完成大数加法的功能后,这个测试也最终通过了。 这里我需要特殊说明的是,为了不偏离本次实战课的目的,我们的单元测试只写了两个,但在实际的开发工作当中,单元测试是需要尽量覆盖所有情况的。换句话说,仅仅只是测试“1+2”“2333333333333332+1”这两种情况,是无法保证我们的计算器逻辑正确的。一般来说,一个应用于商业的计算器,它的单元测试用例数量会达到几百上千个。 ## 小结 到这里,我们就通过完成三个不同版本的四则运算计算器,一起梳理了前面课程中,学习的那些重要的Kotlin的知识点,比如不可变的变量val、when表达式、数据类、枚举类,等等。并且也在实操过程中,一起思考了代码实现时可能会出现的问题。 你也可以在动手操作的过程中,具体感受下跟Java代码的不同,同时也看看自己的思路与课程的思路有什么不同,课程当中的代码还有哪些可以改进的地方。 最后我想让你注意的是,在3.0版本中,我们引入了**单元测试功能**。实际上,单元测试的作用,不仅仅可以验证新开发的功能,同时它还可以用于保证旧的功能不受影响。在实际开发工作中,我们很容易因为对功能A的改动,导致功能B出问题。然后往往由于时间限制,测试人员只测试了功能A,忽略了功能B,最终导致线上故障带来经济损失。 而借助单元测试,在每一次的开发工作完成以后,我们就统一跑一遍所有的单元测试,只要单元测试通过了,我们就能保证,新的功能没问题,而旧的功能也没问题。 ## 动手实操 在3.0版本的开发当中,我们仅仅只实现了“大数的加法”,其余的“大数的减法”“大数的乘法”“大数的除法”都没有实现。请你挑其中一个功能,尝试自己实现,参考答案我会在之后放出来。 欢迎你在评论区分享你的实现思路,我们下节课再见。