Excelize在开源后得到了越来越多开发者的关注,也在不断添加对新功能的支持,持续优化性能并改善兼容性。总的来说,在设计理念上,Excelize体现了如下4项原则,下面我们来逐一讨论。
在Excelize的设计过程中,通过最小可用和结合场景做设计决策的原则来保障基础库的易用性。
最小可用 :基础库的开发和应用的开发有一些不同,从基础库设计的角度,需要明确哪些功能是基础库可以支持的,哪些功能是不支持的,这是一种对架构的约束。在软件设计中,如果没有这样的约束,会使项目愈加冗余,且缺乏边界感和层次感,这将带来更多问题。在基础库的设计上,无时无刻都会以“最小可用”的设计理念力求精简,最大限度地减少对外提供的函数与配置项的数量,仅提供基础功能,尽可能避免引入除电子表格文档以外不必要的概念或名词术语。在源代码的物理文件结构组织上,没有过早地对目录层级做多层嵌套的设计。项目早期阶段,在考虑了模块划分的合理性、可维护性等因素后,除测试所需的文档之外,数十项关键源代码均位于同一根目录下,并根据模块名称进行命名,对开发者来说,基础库中各模块以及各模块所对应的源代码一目了然。对于最小可用理念较为简单的判断标准是:假设要为基础库添加某项功能,如果开发者能够简单地通过组合使用已有的基础功能函数来实现该功能,那么这项功能不应该纳入基础库中,而应该交给开发者实现,这样做的好处是降低基础库的复杂度、保持基础库的轻量和简洁、降低开发者学习和使用基础库过程中的成本。这一理念可以用奥卡姆剃刀原则来概述:如无必要,勿增实体。例如,基础库是否应该提供一个根据给定HTTP地址打开网络上的电子表格文档的功能?答案是否定的。从网络上获取文档的工作应该交给开发者来实现,这个过程超出了基础库的核心功能范畴,开发者可以通过HTTP、FTP、RPC和SMB等各种网络协议或其他数据源获取文档的内容,而基础库需要做的是提供一个带有io.Reader类型形参的打开数据流函数。
结合场景做设计决策 :在基础库的设计过程中,时常面临“鱼和熊掌不可兼得”的问题,当中处处体现着权衡与取舍(严谨性与灵活性的平衡、功能与性能的平衡、依赖与重用的平衡等)。当我们在思考某一个设计是否合理时,需要参考特定的场景来做出判断,谨慎地为基础库添加对外提供的函数(这些函数也被称为可导出函数,函数名称的首字母大写)。
电子表格文档犹如一个便携式数据库,文档中的数据规模可大可小,根据数据规模和常用操作进行分析,有以下两类典型处理场景。
● 对包含数千单元格的小规模数据电子表格文档进行复杂功能的设置,例如样式定义、筛选、数据透视和数据可视化等操作,此类文档通常对结构化的数据源进行分析或加工,最终使用电子表格应用打开查看。
● 对包含数万行、千万级以上单元格的大规模数据电子表格文档进行简单的读取和生成,此类文档常作为不同系统之间进行数据交换的媒介,以程序自动化读写为主要使用方式,较少被桌面应用打开查看。
这两类场景对功能和性能的需求优先级各有侧重,在基础库设计的过程中,既要满足对复杂文档的功能支持,又要兼顾在处理大规模数据电子表格文档时对CPU计算资源、内存和磁盘存储资源的使用。为此,Excelize提供了流式读写系列函数,用于高效地读写大型文件。由此可见,在功能与性能之间做权衡,要结合具体应用场景做合理的设计。
在基础库的设计过程中,尽可能地保证代码可被复用,一段可被复用的代码被定义为新的函数。函数能够被多大程度地复用,与其功能划分的粒度、内聚程度有关,但是划分得过细将导致定义的函数过多,依赖复杂性过高,导致反优化。Go语言的作者之一罗布·派克(Rob Pike)曾说过:“少量的复制胜过引入依赖”(A little copying is better than a little dependency), 不要重复自己 (Don't Repeat Yourself,DRY)原则很好,但需要结合场景在依赖与重用之间做取舍。
对于电子表格文档基础库和办公文档格式处理这类基础库,它们需要支持庞大而复杂的文档格式国际标准,不仅需要严格对照标准内容实现,还需兼顾不同办公软件所生成文档内部的特殊情况,从而保障文档在各个办公应用之间能够进行良好的互操作。因此,兼容性显得十分重要,其重要性甚至高于功能和性能,如果没有良好的兼容性,将无法保证文件被正确地打开和使用。为了保证良好的兼容性,我们需要做更多工作,例如对文档内存模型进行校验和纠错等,显然这些工作对性能将会造成一定的影响,但这是值得的。
在Excelize的设计过程中,通过“一项具体功能尽可能采用单一方式实现”和“左移思想”来保障基础库的可维护性。
一项具体功能尽可能采用单一方式实现: 这看似缺少灵活性,但会大幅降低使用基础库的学习成本以及修复缺陷的成本,同时提高可维护性。例如,当你使用基础库在工作表中创建一个浅蓝色的带有单下画线样式的超链接时,将涉及设置超链接、创建样式和设置单元格格式3个函数。你可能会疑惑,为什么不再添加一个设置带有默认样式超链接的函数呢?在这个场景下,我不建议添加这个函数,原因是单元格的超链接属性、样式定义和单元格格式是3个独立的部分,同一个样式可以被复用于多个单元格,添加这个函数看似灵活且便利,却埋下了潜在的问题,不久之后,开发者可能会在项目代码中、讨论组中或者网络上的某篇文章里看到同一项功能的多种实现方式,但这些方式将使初学者产生困惑:不同实现方式有哪些区别,我该选择哪种方式?这也会给基础库的维护带来阻碍,假设基础库出现样式创建的问题,需要进行修复,维护者可能会收到设置单元格超链接相关函数的反馈,而如果将这3项功能以3个不同的函数提供给用户组合使用,当其中任何一个函数失效或需要修复时,基础库的维护者可以用更小的粒度对问题进行分析和验证,使问题更容易被修复和测试,从而提高可维护性。
左移思想 :这是软件工程中的一项基本设计思想,其表达的观点是:如果开发者能够将缺陷控制在软件开发交付过程的早期,将能够大幅度地降低未来发现和修复缺陷的成本。软件设计开发过程从提出概念到设计、开发,再到测试、提交代码,最终通过集成或其他方式被应用到产品中,为终端用户提供服务,缺陷修复成本与软件研发时间线的对应关系如图3-1所示。
图3-1 修复缺陷的成本与软件研发时间线
由此可见,我们应该尽早发现问题,让缺陷修复发生在这条时间线的左侧。Excelize基础库最终发布的是面向开发者的软件包,Excelize基础库以依赖库的形式被开发者的程序或框架所引用,最终被集成到应用、服务或产品中,交付给用户使用。对于一些企业应用,更新应用的周期相对较长,缺陷修复的成本会更高。
为了最大限度地减少Excelize基础库给开发者带来的缺陷诊断成本和修复成本,基础库的开发者和贡献者需要对左移思想建立共识,并通过有效的自动化流程和工具等应对手段减少研发过程中缺陷的产生。
首先,需要有完善的单元测试,并对单元测试的行覆盖度做度量。目前Excelize基础库的单元测试代码行覆盖度大于98%,对于迭代过程中的缺陷修复和新增功能都应保证有与之对应的单元测试,做到测试覆盖度只增不减。在代码评审和提交时通过自动化工具对代码行覆盖度的变化进行检查,在进行代码评审时也要严格把关。
其次,为了保证在不同Go语言版本和不同操作系统下的兼容性,Excelize基础库的开源代码仓库设置了多种环境下的单元测试,在提交代码时,将自动触发对所有Excelize已声明兼容的Go语言版本在32位和64位平台的macOS、Linux、Windows操作系统环境下的单元测试,如果这些测试有任何一项未通过,代码将不会被合并到主干分支,从而保证代码的兼容性。
再次,在提交代码时,自动化流水线在进行单元测试前,还将对源代码进行基于go vet和gofmt的静态分析,通过工具验证代码中是否存在语法问题,以及是否符合Go语言官方推荐的统一代码风格。除使用Go语言提供的内建静态分析工具之外,Excelize基础库还通过Go Report Card服务对源代码做进一步的静态分析,该分析将对代码的 圈复杂度 (Cyclomatic Complexity)、变量的赋值、开源协议和变量的拼写做检查。圈复杂度是一种代码复杂度的衡量标准,除特殊情况外,我们建议函数的圈复杂度不超过 15。在Excelize的贡献指南(本书源代码仓库中的CONTRIBUTING.md文件)中,为所有希望参与基础库代码贡献的开发者提供了一份详细的代码提交流程规范,这份规范建议我们:在创建“Pull Request”之前的本地开发阶段,就使用这些静态分析工具检查代码,一些编辑器和 集成开发环境 (Integrated Development Environment,IDE)支持通过安装插件或配置的方式集成这些静态分析工具。通过静态分析工具帮助我们及早发现问题,在代码评审之前捕获缺陷。
安全性对基础库来说也十分重要,在提交代码时,自动化流水线还将对Excelize的代码进行基于CodeQL的代码安全性审计扫描,以及时发现潜在的安全问题。此外,通过设置周期性任务,有专门的机器人对Excelize基础库的依赖库版本进行漏洞监测与检查,并进行升级提示。
这些自动化检测流程和工具并不是完美的,依然不能杜绝缺陷的产生,但是能够最大限度地减少缺陷的产生,让发现缺陷的时机“左移”,从而保障基础库的质量,减少后期检测和修复缺陷的成本。严格的代码管理虽然在前期会花费更多的时间,投入更多的成本,但可以获得具有良好可读性的高质量代码,随着基础库代码寿命的延续,将在未来更长的时间里持续获得收益。