跳转至

用户故事 只要方向对了,就不怕路远

你好,我是小5,一个有多年经验的Java软件工程师。

先简单说说我的工作情况。我基本上一直在和遗留系统打交道,毕竟现实里我们很难遇到从0开始的项目。我经历过的最古老的遗留系统,是那种有JSP页面的大单体系统:JSP里面嵌套着Java代码,Java还嵌套着HTML代码和JS代码。这样的系统,业务逻辑错综复杂,牵一发而动全身,用过的人都知道,工作效率很差。

当然,还有一些局部看起来比较新的项目(模块),但是随着时间的累计,也渐渐有了遗留系统的味道。

所以,当时在极客时间上浏览专栏,遗留系统这个标题一下就吸引到我了,点进去看到开篇词标题“你现在写的每一行代码,都是未来的遗留系统”,只凭这个题目好像就已经戳到我了。

后来继续往下看,发现整个专栏一直在直击我实践中的痛点。整个专栏就像是帮我这些年的工作做了一个复盘,不但分析了问题的原因,还梳理了解决方案。所以,借着这次用户故事的机会,我想分享一下学这个专栏的一些感想和收获。

学习方法分享

我们都明白,往往学到最后,就会发现最好的学习方法就是行动。但行动之前,我们也常常有许多问题。很多曾经困扰我的问题,学着学着就不再是问题了。这里也把我的想法分享给你,希望也能带给你一些启发。

如果问学习最重要的是什么,我觉得用一个关键词概括就是“坚持”。但问题在于怎么坚持呢?从我的经验看,无论是带着兴趣学习,还是带着工作中的问题与困惑去学习,或者带着未雨绸缪的心态去学习,都能让我们坚持得更久。

坚持的态度有了,但我们大概率还会纠结工作这么忙,什么样的学习资源值得选择?学习时间又怎么来呢?

我个人花时间最多的学习APP是极客时间和微信读书。这里也要感谢公司给我开通了极客时间账号,让我看到了很多高质量的文章,欣赏到了评论区里各位优秀同学的精彩发言。微信读书我也算深度用户了,大部分图书都可以用免费卡阅读。

有了资源,接下来就是何时学习或阅读的问题。我个人习惯是想看的时候就看看,并不会要求自己一定要每天都学习,但只要有空了,我常常会开启学习模式。

我一般集中在早上七点到九点学点东西,不过这种时间不太连续,所以我的另一个常用时间段是晚上十一点到十二点,如果要涉及到写代码,那一般都是晚上来做。再加上平时一些零碎的时间,总的算下来,最近半年也累计学习了160小时,平均下来每天不到一小时。

聊到这,你可能还有疑问:时间短、学习不连续,那如果学的内容没记住多少,会不会没啥效果呢?实际上,很多专栏我也是看的多,记的少,有很长一阵子,这也让我有点受打击。

直到后来,我从《认知觉醒》这本书看到这样一段话:

我们根本不用在意自己读后记住多少内容,即使整本书都记不起来了也没关系,只要有一个点、一句话触动了自己,并让自己发生了改变,这本书就没有白读。

看完以后我觉得释然了许多。我看过很多专栏,真正掌握的我算了下,肯定不到百分之十。但是,有些专栏里的内容,的确对我真正产生了影响(后面会举一些例子),并且使我的思维和行为发生了改变,让我的工作效率得到提升,这对我来说也是极大的鼓励。

这里我也顺便说说,我怎样衡量自己的学习效果。

一个方面是技术水平的增长,具体来说就是写代码的质量明显感觉提升了,对代码怪味道很敏感,对编码技巧、重构技巧越来越熟练,简直要“飞起”。最直接的效果就是新写的代码bug很少,而且扩展性很好,刚好前段时间我做了一个相对比较大的新功能,效果明显。时间久了,我甚至觉得写代码的过程由被动的实现需求,现在变成了一个主动享受的过程。

另一个方面是思考力的提升,看待问题拥有更多的思考角度、分析问题时更能切中要点,思考变得更深刻、全面和长远。

课程内容推荐

学习方法我就点到为止,其实还有个重要的学习方法刚才没说,那就是动手实践,然后再结合自己的理解“二次分享”。

接下来,我就从《遗留系统现代化实战》课程里,选一些我印象最深的内容和你聊一聊。

代码重构

首先是第九节课《代码现代化:如何将300行代码重构为一行》。这个章节的内容我练习了好几遍,阶段重构法很受用,不愧是倚天剑,威力无穷。当时我记得自己是照着课程的例子练习了几遍,最后还分享给了同事,分享的时候又敲了一遍。

这里我有一个比较复杂的例子,因为代码不大好简化,这里就不贴代码了,我就说说大致情况。

这个功能搁置了很久都没发到线上,后来又分到了我这里。原来的方法有一点问题,没有完全实现需求功能,所以需要我来修改。我看了很久,感觉全局变量很多,理不清逻辑,于是,我就直接用拆分阶段的方式重写了。当然,重写的自信主要是来自于对需求的充分理解。

重写后,代码层次看上去清晰很多,分为获取原始数据、数据预处理生成中间结果,根据中间结果生成最终结果三个阶段。

另外,这节课里还举了一个for循环重构的例子,我刚好在工作也遇到了,就用这个方法优化了下,我找了下,没找到我当时优化的那段代码。不过,类似的可优化代码还挺多的,这里我选了另一段代码,如下所示:

@Test
public void should_pass() {
    //...
    String dimensionality = getDimensionality();
    UnitEnum unitEnum;
    String dateFormat;
    switch (dimensionality) {
        case "month":
            unitEnum = UnitEnum.ONE_MONTH;
            dateFormat = "yyyy-MM";
            break;
        case "day":
            unitEnum = UnitEnum.ONE_DAY;
            dateFormat = "yyyy-MM-dd";
            break;
        case "hour":
            unitEnum = UnitEnum.SIXTY_MINS;
            dateFormat = "yyyy-MM-dd HH:mm";
            break;
        //...
        default:
            unitEnum = UnitEnum.THREE_MINS;
            dateFormat = "yyyy-MM-dd HH:mm";
            break;
    }
    doSomething(unitEnum, dateFormat);
    //...
}

虽然这里贴出来的的代码看上去不复杂,但实际上,原本代码的前后,还有还有好几十行代码。
虽然这个不是for循环,但是看起来是不是非常相似?原文的优化方法如下:

复制一下这个 for 循环,分别删掉两个 for 中的 volumeCredits 和 totalAmount 的赋值语句,用两个 for 循环分别计算 totalAmount 和 volumeCredits

我用一模一样的方法,把这里的switch复制一份,然后分别删掉unitEnum和dateFormat,然后Ctrl + Alt + M,就得到了如下代码:

@Test
public void should_pass() {
    //...
    String dimensionality = getDimensionality();
    UnitEnum unitEnum = getUnitEnum(dimensionality);
    String dateFormat = getDateFormat(dimensionality);
    doSomething(unitEnum, dateFormat);
    //...
}

你看,几十行代码就变成了2行,是不是清爽了很多?别看性能上确实可能会差那么几毫秒,但这和换来的可读性相比就不值一提了。

这里再分享一下,对于inline操作我还发现了一种新的用法,用来删除调用某个方法的所有的代码,就是先把某个接口的实现类的方法删掉,然后在接口实现默认方法,方法中是空的,这样inline之后所有调用的地方都变成空的了。当然这种场景非常少,我是在开发新功能并且重构的时候有这个需求,就尝试了下,居然还真可以。

对了,这节课里还提到了很多快捷键,经过我的亲身体验,确实感觉可以大大提高效率。后来我还看了TDD专栏的视频课程,做了练习,就更熟练了。

这里顺便稍微说说TDD专栏的学习。不过对于TDD中的内容,我还只学了一部分,跟着课程尝试写了一遍代码,还是很花费时间的。

老师一个视频可能10分钟,跟着写完可能要1到2小时。为什么要花这么久呢?一般可能要先看一遍,理清楚大概的逻辑,然后一边看、一边练习,有时候不明白的地方,还要反复看几遍,偶尔出一个bug,调试还要花更多时间。

总之,这两门课都推荐你边学边实战练习,虽然这样更挑战自己,但收获也更大。

认知负载

另一节印象深刻的是第三节课——《以降低认知负载为前提:为什么遗留系统这么难搞》

开头一段开发人员和经理battle的例子,初读朗朗上口,听着语音rap我忍不住笑出了声,但又是笑中带点泪。心理活动是:扎心了,这不就是前几天,发生在我身上的事情吗(ー_ー)!!

透露一下,我只涉及到4个微服务,这也没什么,关键是初步评估后觉得只要两小时就能做完。实际确实也“做完了”,但是接下来花了整整两天时间来改bug。

工作中还有很多奇奇怪怪的问题,都可以用“认知负载”这个名词解释。比如说。我们的迭代或者我们的版本发布总是延期,那我们是不是简单地归结于时间不够就行了?是不是要求领导加工期就行了?当然,加大预估工期,确实能让我们的按时发布率好看点,但是已经违背了我们的初衷。

遇到这些问题的时候,我一直在想究竟是哪里的问题,还有怎么去解决这个问题,不过之前一直没想清楚是需求的问题,还是详细设计的问题,或者是开发能力的问题?好像每个环节都有些问题,当时感觉理解得还是有些片面。

直到在这门课里看到“认知负载”这个解释,有一种如获至宝、发现新大陆的感觉。众里寻她千百度,原来它在这里啊。认知负载,很好地解释了这些奇奇怪怪、又无厘头的现象。

认知负载包含三方面,内在认知负载、外在认知负载和相关认知负载。找到了问题所在是不是可以对症下药了?我们不妨想象一下,如果在我们的工作中可以从这三方面加强,那“后果”简直不可想象,绝对是一个高执行力、高效率的团队。

这三个方面里,我觉得最需要加强的是外在认知负载和相关认知负载。

先说外在认知负载,在工作环境中,通常文档方面是比较欠缺的,业务知识基本上没有文档,不过原型还是有的,但是也不详细。从没有过开发同学看了原型就完全弄清楚了,每次还是要追着产品问,而有时候产品也会说:“有时候,代码比我更清楚”。

代码确实清楚,但是代码的外在认知负载很高,看代码的人,就像文中说的那样,不陪葬一把头发休想拿到。

这一点我也深有体会。对于前端开发来说,旧的项目连swagger接口文档都没有,新的项目倒是有文档,但是信息也非常有限。

另外在现在的代码中,由于不需要的旧逻辑没能清理,所以很多无用代码仍然存在,阅读理解代码时很不方便(因为还要分析理解一些理论上永远执行不到的条件分支),修改代码时也常常不敢随便动手。

对于相关认知负载,我是这样理解的,需要开发同学充分熟悉业务场景,熟悉现有的系统。这样的话,就很容易发现不合理的地方,也更容易一次做对,不用多次返工。

从持续集成到持续部署

第十六节课第十七节课主要讲了持续集成到持续部署的内容。

我对这里提到的关于分支模型的各种问题深有体会。虽然主干分支模型是最理想的,不过仔细思考后,我觉得目前团队使用主干分支模型的条件不成熟:一是缺少持续集成的文化以及相应的配套基础设施和流程;二是现在采用的分支模型,虽然使用流程相对复杂一点,但是可以应对各种情况,更像是GitLabFlow和AoneFlow的结合体。

我也梳理了一下现在团队存在的各种分支:

1.master分支:(长期存在)master分支上的代码一定是已经发到生产环境的代码,可能会稍有延迟(发到release分支的代码,不一定马上合到master)
2.test分支:(长期存在)相当于GitFlow中的develop分支,是集成测试环境分支,会包含各种开发中的功能。
3.pre_prod分支:(长期存在)预发环境分支,在线上生产环境代码的基础上加上本次需要发布的内容。
4.release/master分支:(长期存在)线上生产环境分支。
5.release/canary分支:(长期存在)灰度环境分支
6.feature/feature01分支:功能分支,在发布后可以删除。
7.hotfix分支:线上bug修复分支,其实和feature分支操作一样,该分支也是从master分支新建出来。

团队从开发到发布的过程如下:

1.从master分支新建一个分支feature001;

2.开发一部分后或者开发完后,把分支feature合到test分支,如果提测了,测试会在这个分支先测试;

3.完成测试后,如果有预发环境会发布到预发环境;

4.预发通过后,会合并到发布分支release/v1.1分支,可能是多个feature分支都合过来;

5.release/v1.1分支合并到release/master分支,至此就发布到线上环境了;

6.如果发布完后没有问题,就把release/master合并回master(这个合并操作相当发布时间一般会存在延迟,下面会说明原因);

7.bug修复,在master分支新建hotfix/hotfix001,改完后合并到test分支,测试通过后合并到最新的release/v1.1分支上,或者新建release/v1.1_bugfix001分支,然后再合并到release/master分支。最后将release/master合并回master。

如果发布后有问题需要撤回,就把release/master 回退到上次发布时间点的代码,重新发布。这也是我们发布完后,并不会马上把release/master合回master的原因,否则的话,如果撤回还需要把master分支撤回,假如已经有人从master分支新建了开发分支,就会更麻烦。

如果这样那master并不一定是最新的代码,所以我们团队规范中要求每次从master分支新建分支的时候都要检查下release/master分支是否有更新的代码,如果有的话要先合回master。因为这个时候一般也离上次发布完一般有一段时间了,还需要回滚的概率就极小了。

另外,如果多个featuer分支同时开发,但是一个分支对另一个分支有依赖关系怎么办呢?

我们会根据计划发布顺序:如果feature1会先于feature2发布,那feature2分支就从master新建,然后把featuer1合并过来,尽量定期做同步,可以避免很多的冲突。等feature1发布完合回master后,再把master分支合并到feature2分支。这样就能确保feature2分支上建立在线上环境代码的基础上,且只有feature2的改动。

写在最后

通过学习课程,对于遗留系统怎样现代化,我有了更多的思考。我也想再说点题外话,分享分享我最近的经历。

我们有一个比较旧的系统,服务是前后端一体的。总的来说,用的都是相对比较老的技术栈。大家用起来都不顺手,所以一提到要改这个服务都有点抗拒。团队也在有计划地替换这个服务,采用的方式也就是课程里说的绞杀植物模式,分模块逐步用新的服务来替换旧的服务。

而我刚好经历了新服务构建的过程,个人觉得这个现代化的过程还有很大的改进空间:虽然换了主流的Spring+MyBatis,但是代码层面业务逻辑的组织方式依然和以前的没什么区别,全部的业务逻辑都集中在service大类中,典型的过大类怪味道。

最近了解到。我们接下来很可能又要重构,把业务线公共的部分整合起来,想必课里学到的很多技巧能用上,不过估计这是个漫长的过程,如果接下来我还有什么新的思考或体会,也会考虑回到专栏里留言分享出来。

好了,到这里,我的分享就结束了,希望对你也有启发。最后再分享一句话:让我们成为更优秀的自己,只要方向对了,就不怕路远,加油!

精选留言(2)
  • aoe 👍(2) 💬(0)

    简化 switch 的操作很好,又学到了一招 没使用 TDD 之前,我的工作中有 1/3 时间是在找 Bug,使用之后结合 170 老师的课程大胆改造遗留系统,代码一天天好起来

    2023-02-02

  • 范飞扬 👍(1) 💬(0)

    赞,与工作息息相关,让我对专栏感兴趣了

    2023-01-13