前端架构:从入门到微前端
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.4 成长优化期:技术债务与演进

经历一两次上线后,项目进入一个稳定上线、交付的阶段。笔者将其称为成长优化期,这是一个技术提升开发体验,技术带来更多业务价值的阶段。不过,实际上这已经是一个稳定的时期——我们可以抽时间来解决各种各样的问题。

在设计架构和完善业务的过程中,会暴露出团队的一系列问题:架构不完善、开发流程不便利等。短期内,这些问题并不会影响我们的开发。然而就长期而言,这些问题还是有可能会影响开发进度。先前我们在追赶业务时也遗留了一些技术问题,尤其是代码的质量问题,这些问题会随着时间的推移和业务代码的堆砌变得越来越严峻。当然,如果一个项目的时间短,那么它就不会遇到这些挑战。不论怎样,程序员作为一个“匠人”,总得有点“追求”,要不断地提高自己的水平。

2.4.1 偿还技术债务

在技术准备期,我们在构建技术基础方面花费了大量时间;在业务回补期,我们在支持业务的开发方面花费了大量时间。在这两个时期,我们都或多或少地采取了一些妥协方案,为的是能加快速度开发流程。这些问题都将在未来成为我们的开发负担。这种方式就和债务一样,可以在短期内得到好处,但是在未来必须偿还它们。这部分开发负担,我们称其为技术债。

技术债包含的内容有如下几个方面:

(1)代码质量。常见的问题有接口、函数的重复实现,即a成员在自己的功能内,实现过这个功能,但是b成员又实现一份,没有提取到公共的方法中。实现方式或模式不统一,比如我们采用某个框架来解决问题,但是在真实场景下,可能又会采取过去的实践方式,诸如使用Lambda、RxJava、Rx.js、Ramda等框架来进行函数式编程。少部分代码未按规范进行实践,这个问题更加常见,不仅存在于没有代码检视(Code Review)的项目,还存在于拥有代码检视的项目。未检视过的代码,往往容易被遗漏。

(2)测试覆盖率。在面对技术不熟悉、业务又过急的情况时,UI自动化测试、单位测试往往是最先被抛弃的一环。一方面,自动化测试的目的在于,保持功能不被破坏;另一方面,大部分的项目都拥有专业的测试人员进行测试。值得商榷的是,国内的互联网公司都不会有测试这种东西,所以它们就存在这种长期的债务了。

(3)依赖问题。依赖对于短期项目来说不是问题,对于长期项目来说,依赖没有及时更新是一个很严重的问题。例如,我们使用Redux 3.0的版本,当4.0版本发布的时候,按照语义化版本的规则,它可能修改了大量的代码,而我们不得不追随这个变化——除非,我们决定在未来重写该应用,否则大量依赖过旧的问题,会导致我们难以对代码进行重构,因而不得不重写应用。

有些问题不是一天两天造成的,比如测试覆盖率低的问题,要解决这些问题也不是两三天就能完成的。这往往需要制定一个长期的计划,才能将它们一个一个地修正过来。多数情况下,我们并没有足够的时间在短期内修复,相关的技术债都是项目在不断演进的过程中,一步步提升的。比如测试覆盖率,它依赖所有的人编写测试,并不断限定测试覆盖率的下限。这样一来,在经历了几个迭代之后,我们便会拥有不错的测试覆盖率。

想要改善代码质量也不是一件容易的事,改进代码质量要依赖开发人员的水平,以及团队的能力。如果团队中的代码质量不忍直视,充满了各种Code Smell(代码的坏味道),那么可以通过代码检视的方式来不断提高团队成员的水平。然后,有针对性地进行相应的培训。

值得注意的是,与日常的业务代码编写相比,改进过去的代码会带来更多的成长和技术挑战——我们更容易从错误的代码中学习,而不是从成功的经验中学习。举个例子,我们直接看别人写的与设计模式相关的代码,并不会直接学到相关的内容,如果从过去写的代码重构看到设计模式,就能更深刻地理解相应的技术实践。

2.4.2 优化开发体验

提升开发体验,也是在稳定时期值得考虑的另外一个因素。在我们的日常工作中,有很多是手动完成的,我们可以通过自动化来减少重复性工作。

有这样一些例子:我们想创建一些测试数据,需要在数据库中手动创建,但是缺少对应的批量创建脚本或命令;在调试时经常需要手动输入相关的账号,这也可以通过插件来自动化登录;在开发移动应用时,一旦提交了代码,就应该有相应的工具来自动化构建应用,并在构建成功后上传到某个包管理中心,同时安装到对应的测试机。相似的,还有其他各种方式的自动化流程,它们所做的便是减少重复的工作,以不断提升开发体验。

在这个过程中,我们可能写了大量的接近重复的代码。而这些代码表面看上去并不是重复的,但是从抽象层来看是重复的。为了应对这种问题,我们可以进行代码重构,也可以通过诸如创建领域特定语言的方式来进一步抽象出内部DSL的代码。这样,我们就可以减少花费在业务代码上的时间。

此外,还可以思考怎样将一些代码的编写实现自动化,例如在UI层通过采用Sketch2Code来生成模板页面,或者编写相应的拖曳生成UI界面的工具。一旦我们优化了这些开发流程——尤其在相应的自动化功能完成之后,开发人员就开始面对一些新的挑战。

2.4.3 带来技术挑战

堆砌业务代码对大部分技术人员来说是一件难熬的事情——每天都在重复工作,总想寻找一些挑战。而对于周期长的应用来说,这种事情更为可怕。不论怎样的项目,技术人员都需要获得一定的能力增长。如果不能满足这种诉求,那么就不能进步,也就相当于是一种能力方面的退步。因为技术在不断地进步,新的技术总会很快地淘汰旧的技术。一个长期项目一旦结束,新的技术体系就可能与旧的技术体系完全不一样了。

面对这种情况,一种常见的做法是引入新的技术栈。它既可以是一个框架,又可以是一门语言。我们可以引入Rxjs进行响应式(Reactive)开发,或者引入Rambda.js进行函数式编程。也可以采用新的语言,如在开发Android应用时,除了使用C++,还可以使用Kotlin语言来完成部分功能;在开发iOS应用时,可以结合Objective-C来使用Swift语言进行开发。

对于前端应用而言,我们还可以尝试使用新的前端框架。目前维护大型的前端应用用的是Angula框架,如果要开发一些新的简易的应用,那么可以尝试使用React或者Vue框架来实现。

此外,对于稍有余力的项目团队来说,可以尝试进行一些小型的模拟项目。在这些小型的模拟项目上,我们关注于使用新的技术来开发现有的应用。一方面,可以为以后的架构演进做准备;另一方面,让我们的技术与主流接轨。在笔者经历过的项目中,曾经有一个项目,每隔几个月开展一次Hackday活动,活动内容是使用新技术来重写旧的应用。此外,我们还会做一些Workshop来练习使用项目潜在的新技术栈。

当然,对于项目管理者来说,堆砌业务代码会轻松很多,毕竟出现新技术风险的可能性较低。但是在保证进度的情况下,也需要适当地带来一些成长的机会。

2.4.4 架构完善及演进

经过大量的业务沉淀,我们也会发现架构中存在的一些问题。这些问题大都是一些架构设计上的偏差,经过调整,有的纠正到原来的设计上,有的使用新的架构设计。如果使用新的设计,我们还需要将那些使用过去设计的部分进行相应的修改,这意味着会有一定的返工工作量。

我们还可能遇到由于业务变更导致架构需要修改的情况,这时不仅需要调整,还需要进行全新的设计,才能适应新的业务需求。比如,如果在前端应用中包含大量的第三方应用,就要考虑插件化的方案,设计一个新的插件化架构。再举一个例子,如果我们最开始开发应用的时候使用Cordova的WebView容器,那么出于安全考虑,会迁移到自己开发的混合应用框架上。这两个例子都只是在架构设计上发生一些改变,但是从代码的角度来看,所需要的修改并不会太多。

当然,这也并不是说我们预先设计的架构有问题,因为最初的架构满足的是创建架构时设计的业务场景。当业务发生一些变化时,架构也需要做相应的调整。我们不可能让过去的架构一直适应新的业务变化,在变化发生的时候演进系统的架构才是一种正常的形态。

此外,在更低级别的代码层里,我们会发现代码中存在一些复杂、混乱的相互调用,如果不加以规范并重构代码,那么会使得新增的代码加剧这个问题的产生。比如在项目中后期参与开发的人员编写的代码,可能与我们最初的架构风格不一致。如果原先的架构风格更符合需求,那么还需要帮助这些后来的开发人员解决相关的问题。