关于高德地图:经验总结-重构让你的代码更优美和简洁

6次阅读

共计 6852 个字符,预计需要花费 18 分钟才能阅读完成。

1. 前言

最近,笔者有幸对高德打车订单 Push 我的项目进行了重构,与大家分享一下代码重构相干的工作教训,心愿对大家有所启发。

​有时候,咱们在做某个性能需要时,须要花掉大量的工夫,能力找到和需要有关联的代码。或者,咱们在浏览他人写的代码、接手他人的我的项目时,总是“头皮发麻”,当你面对构造凌乱、毫无章法的代码构造,词不达意的变量名、办法名时,我置信你基本没有读上来的情绪。这不是你的问题,而是你手中的代码须要进行重构了。

2. 何为重构

每个人对重构都有本人的定义,我这里援用的是“Martin Fowler”的,他从两个维度对重构进行了定义。

作为名词:重构是对软件内部结构的一种调整,目标是在不扭转软件可察看行为前提下,进步其可了解性,升高其批改老本。

作为动词:重构是应用一系列重构手法,在不扭转软件可察看行为的前提下,调整其构造。

我在本文提到的代码重构,更偏差它作为动词的定义 ,而依据重构的规模水平、工夫长短,咱们能够将代码重构分为 小型重构 大型重构

小型重构:是对代码的细节进行重构,次要是针对类、函数、变量等代码级别的重构。比方常见的标准命名(针对词不达意的变量再命名),打消超大函数,打消反复代码等。个别这类重构批改的中央比拟集中,绝对简略,影响比拟小、工夫较短。所以难度绝对要低一些,咱们齐全能够在日常的随版开发中进行。

大型重构:是对代码顶层进行重构,包含对系统结构、模块构造、代码构造、类关系的重构。个别采取的伎俩是进行服务分层、业务模块化、组件化、代码形象复用等。这类重构可能须要进行准则再定义、模式再定义甚至业务再定义。波及到的代码调整和批改多,所以影响比拟大、耗时较长、带来的危险比拟大(我的项目叫停危险、代码 Bug 危险、业务破绽危险)。这就须要咱们具备大型项目重构的教训,否则很容易犯错,最初得失相当。

其实大多数人都是不喜爱重构工作的,就像没有人违心给他人“擦屁股”一样,次要可能有以下几个方面的担心:

  • 不晓得怎么重构,不足重构的教训和方法论,容易在重构中犯错。
  • 很难看到短期收益,如果这些利益是久远的,何必当初就付出这些致力呢?久远看来,说不定当我的项目播种这些利益时,你曾经不负责这块工作了。
  • 重构会毁坏现有程序,带来意想不到的 Bug,不想去接受这些意料之外的 Bug。
  • 重构须要你付出额定的工作,何况可能须要重构的代码并不是你编写的。

3. 为何重构

如果我纯正为明天工作,今天我将齐全无奈工作。

程序有两面价值:“明天能够为你做什么”和“今天能够为你做什么”。大多数时候,咱们都只关注本人明天想要程序做什么。不论是修复谬误或是增加个性,都是为了让程序力更强,让它在明天更有价值。然而我为什么还是提倡大家要在适合的机会做代码重构,起因次要有以下几点:

  • 让软件架构始终保持良好的设计。改良咱们的软件设计,让软件架构向无利的方向倒退,可能始终对外提供稳固的服务、从容的面对各种突发的问题。
  • 减少可维护性,升高保护老本,对团队和集体都是正向的良性循环,让软件更容易了解。无论是前人浏览前人写的代码,还是预先回顾本人的代码,都可能疾速理解整个逻辑,明确业务,轻松的对系统进行保护。
  • 进步研发速度、缩短人力老本。大家可能深有体会,一个零碎在上线初期,向零碎中减少性能时,实现速度十分快,然而如果不重视代码品质,前期向零碎中增加一个很小的性能可能就须要花上一周或更长的工夫。而代码重构是一种无效的保障代码品质的伎俩,良好的设计是保护软件开发速度的基本。重构能够帮忙你更疾速的开发软件,因为它阻止零碎腐烂变质,甚至还能够进步设计品质。

4. 如何重构

小型重构

小型重构大部分都是在日常开发中进行的,个别的参考规范即是咱们的开发标准和准则,目标是为了解决代码中的坏滋味,咱们来看一下常见的坏滋味都有哪些?

泛型擦除

//{"param1":"v1", "param2":"v2", "param3":30, ……}
Map map = JSON.parseObject(msg); //【1】……
// 将 map 作为参数传递给底层接口
xxxService.handle(map); //【2】// 看一下底层接口定义
void handle(Map<String, String> map); //【3】

【2】处将曾经泛型擦除的 map 传递给底层曾经泛型限定的接口中,置信在接口实现中都是应用“String value = map.get(XXX)”这种形式获取值的,这样一旦 map 中有非 String 类型的值,这里就会呈现类型转换异样。读者必定和我一样好奇,为何该业务零碎中未抛出类型转换异样,起因是业务零碎取值的形式并未转换成 String 类型。可想而知,一但有人应用规范的形式获取值时,就会踩雷。

// 文本 1${param1}文本 2${param2}文本 3${param3}
String[] terms = ["文本 1","$param1", "文本 2", "$param2", "文本 3", "$param3"];
StringBuilder builder = new StringBuilder();
for(String term: terms){if(term.startsWith("$")){builder.append(map.get(term.substring(1)));
  }else{builder.append(term);
  }
}

无病呻吟

Config config = new Config();
// 设置 name 和 md5
config.setName(item.getName());
config.setMd5(item.getMd5());
// 设置值
config.setTypeMap(map);
// 打印日志
LOGGER.info("update done ({},{}), start replace", getName(), getMd5());


......

ExpiredConfig expireConfig = ConfigManager.getExpiredConfig();
// 为空初始化
if (Objects.isNull(expireConfig)) {expireConfig = new ExpiredConfig();
}

......
Map<String, List<TypeItem>> typeMap = ……;   
Map<String, Map<String, Map<String, List<Map<String, Object>>>>> jsonMap = new HashMap<>();

// 循环一级 map
jsonMap.forEach((k1, v1) -> {
    // 循环外面的二级 map
    v1.forEach((k2, v2) -> {
        // 循环外面的三级 map
        v2.forEach((k3, v3) -> {
            // 循环最外面的 list, 哎!v3.forEach(e -> {
                // 生成 key
                String ck = getKey(k1, k2, k3);
                // 为空解决
                List<TypeItem> types = typeMap.get(ck);
                if (CollectionUtils.isEmpty(types)) {types = new ArrayList<>();
                    typeMap.put(ck, types);
                }
                // 设置类型
            }
       }
  }
}

代码自身一眼就能看明确是在干什么,写代码的人非要在这个中央加一个不关痛痒的正文,这个正文齐全是口水话,毫无价值可言。

if-else 过多

try {if (StringUtils.isEmpty(id)) {if (StringUtils.isNotEmpty(cacheValue)) {if (StringUtils.isNotEmpty(idPair)) {if (cacheValue.equals(idPair)) {// xxx} else {// xxx}
      }
    } else {if (StringUtils.isNotEmpty(idPair)) {// xxx}
    }
    if(xxxx(xxxx){// xxx}else{if(StringUtils.isNotEmpty(idPair)){// xxx}
      // xxx
    }
  }else if(!check(id, param)){// xxx}
} catch (Exception e) {log.error("error:", e);
}

这样的代码,让代码的浏览性大大降低,令很多人望而生畏。除非被逼的无可奈何,否则预计开发人员是不会动这样的代码的,因为你不晓得你动的一小点,可能会让整个业务零碎瘫痪。

其余坏滋味

这里就不再列举相干案例了,置信大家在日常也常常看到很多代码书写不合理,让人不适应的中央,总结一下代码中常见的坏滋味和解决办法:

反复代码

代码坏滋味最多的恐怕就是反复代码,如果你在一个以上的中央看到雷同的代码构造,那么能够必定:设法将它们合而为一,程序会变得更好。

最常见的一种反复场景就是在“同一个类的两个函数含有雷同的表达式”,这种模式的反复代码能够在以后类提取专用办法,以便在两处复用。

还有一种和这类场景类似,就是在“两个互为兄弟的子类含有雷同的表达式”,这种模式能够将雷同的代码提取到独特父类中,针对有差异化的局部,应用形象办法提早到子类实现,这就是常见的模板办法设计模式。如果两个毫不相干的类呈现了反复代码,这个时候应该思考将反复代码提炼到一个新类中,而后在这两个类中调用这个新类的办法。

函数过长

一个好的函数必须满足繁多职责准则,短小精悍,只做一件事。过长的函数体和身兼数职的办法都不利于浏览,也不利于进行代码复用。

命名标准

一个好的命名须要能做到“货真价实、见名知意”,直接了当,不存在歧义。

不合理的正文

正文是一把双刃剑,好的正文可能给咱们好的领导,不好的正文只会将人误导。针对正文,咱们须要做到在整合代码时,也把正文一并进行批改,否则就会呈现正文和逻辑不统一。另外,如果代码已清晰的表白了本人的用意,那么正文反而是多余的。

无用代码

无用代码有两种形式,一种是没有应用场景,如果这类代码不是工具办法或工具类,而是一些无用的业务代码,那么就须要及时的删除清理。另外一种是用正文符包裹的代码块,这些代码在被打上正文符号的时候就应该被删除。

过大的类

一个类做太多事件,保护了太多功能,可读性变差,性能也会降落。举个例子,订单相干的性能你放到一个类 A 外面,商品库存相干的也放在类 A 外面,积分相干的还放在类 A 外面……试想一下,乌七八糟的代码块都往一个类外面塞,还谈啥可读性。应该按繁多职责,应用不同的类把代码划分开。

这些都是比拟常见的代码“坏滋味”,理论开发中当然还会存在其余的一些“坏滋味”,比方代码凌乱,逻辑不清晰,类关系盘根错节,当闻到这些不同的“坏滋味”时,都应该尝试去解决掉,而不是放荡不管不顾。

大型重构

绝对小型重构,大型重构须要思考的事件比拟多,须要定好节奏,循序渐进的执行,因为在大型重构中,状况多变。

将大象装进冰箱的步骤个别能够分成三步:1)把冰箱门关上(事先);2)把大象推动去(事中);3)把冰箱门关上(预先)。日常所有的事件都能够采纳三步法进行解决,重构也不例外。

事先

事先筹备作为重构的第一步,这一部分波及到的事件比拟杂,也是最重要的,如果之前筹备不充沛,很有可能导致在事中执行或重构上线后产生的后果和预期不统一的景象。

在这个阶段大抵可分为三步:

  • 明确重构的内容、目标以及方向、指标

在这一步外面,最重要的是把方向明确分明,而且这个方向是经得起大家的质疑,可能至多满足将来三到五年的方向。另外一个就是这次重构的指标,因为技术限度、历史包袱等起因,这个指标可能不是最终的指标,那么须要明确最终目标是怎么样的,从这次重构的这个指标到最终的指标还有哪些事件要做,最好都可能明确下来。

  • 整顿数据

这一步须要对波及重构局部的现有业务、架构进行梳理,明确重构的内容在零碎的哪个服务层级、属于哪个业务模块,依赖方和被依赖方有哪些,有哪些业务场景,每个场景的数据输入输出是怎么的。这个阶段就会有产出物了,个别会积淀我的项目部署、业务架构、技术架构、服务上下游依赖、强弱依赖、我的项目外部服务分层模型、内容性能依赖模型、输入输出数据流等相干的设计图和文档。

  • 我的项目立项

我的项目立项个别是通过会议进行,对所有参加重构的部门或小组进行重构工作的宣讲,周知大略的工夫计划表(粗略的大抵工夫),明确各组次要负责的人。另外还须要周知重构波及到哪些业务和场景、大略的重构形式、业务影响可能有哪些,难点及可能在哪些步骤呈现瓶颈。

事中

事中执行这一步骤的事件和工作相对来说比拟沉重一些,工夫付出绝对比拟多。

  • 架构设计与评审

架构设计评审次要是对规范的业务架构、技术架构、数据架构进行设计与评审。通过评审去发现架构和业务上的问题,这个评审个别是团队内评审,如果在一次评审后,发现架构设计并不能被确定,那就须要再调整,直到团队内对计划架构设计都达成统一,才能够进行下一步,评审后果也须要在评审通过后进行邮件周知参加人。

该阶段产出物:重构后的服务部署、零碎架构、业务架构、规范数据流、服务分层模式、功能模块 UML 图等。

  • 具体落地设计方案与评审

这个落地的设计方案是事中执行最重要的一个计划,关系到前面的研发编码、自测与联调、依赖方对接、QA 测试、线下公布与施行预案、线上公布与施行预案、具体工作量、难度、工作瓶颈等。这个具体落地计划须要深刻到整个研发、线下测试、上线过程、灰度场景细节处包含 AB 灰度程序、AB 验证程序。

在方案设计中最重要的一环是 AB 验证程序和 AB 验证开关,这是评估和测验咱们是否重构实现的规范根据。个别的 AB 验证程序大抵如下:

在数据入口处,应用雷同的数据,别离向新老流程都发动解决申请。解决完结之后,将处理结果别离打印到日志中。最初通过离线程序比拟新老流程解决的后果是否统一。遵循的准则就是在雷同入参的状况下,响应的后果也应该统一。

在 AB 程序中,会波及到两个开关。灰度开关 (只有它开启了,申请才会被发送到新的流程中进行代码执行)。 执行开关(如果新流程中波及到写操作,这里须要用开关管制在新流程写还是在老流程中写)。转发之前须要将灰度开关和执行开关(个别配置到配置核心,能随时调整)写入到线程上下文中,免得呈现在批改配置核心开关时,多处获取开关后果不统一。

  • 代码的编写、测试、线下施行

这一步就是依照具体设计的计划,进行编码、单测、联调、功能测试、业务测试、QA 测试。通过后,在线下模仿上线流程和线上开关施行过程,校验 AB 程序,查看是否合乎预期,新流程代码覆盖度是否达到上线要求。如果线下数据样本比拟少,不能笼罩全副场景,须要通过结构流量笼罩所有的场景,保障所有的场景都能合乎预期。当线下覆盖度达到预期,并且 AB 验证程序没有校验出任何异样时,能力执行上线操作。

预先

这个阶段须要在线上依照线下模仿的施行流程进行线上施行,分为上线、放量、修复、下线老逻辑、复盘这样几个阶段。其中最重要最消耗精力的就是放量流程了。

  • 灰度开关流程

逐渐放量到新的流程中进行察看,能够依照 1%、5%、10%、20%、40%、80%、100% 的进度进行放量,让新流程逐渐的进行代码逻辑笼罩,留神这个阶段不会关上实在执行写操作的开关。当新流程逻辑覆盖度达到要求、并且 AB 验证的后果都合乎预期后,才能够逐渐关上执行写操作开关,进行实在业务的执行操作。

  • 业务执行开关流程

在灰度新流程的过程中合乎预期后,能够逐渐关上业务执行写操作开关流程,依然能够依照肯定的比例进行逐渐放量,关上写操作后,只有新逻辑执行写操作,老逻辑将敞开写操作。这个阶段须要察看线上谬误、指标异样、用户反馈等问题,确保新流程没有任何问题。

放量工作完结后,在稳固肯定版本后,就能够将老逻辑和 AB 验证程序进行下线,重构工作完结。如果有条件能够开一个重构复盘会,查看每个参与方是否都达到了重构要求的规范,复盘重构期间遇到的问题、以及解决方案是什么样的,积淀方法论防止后续的工作呈现相似的问题。

5. 总结

代码技巧

  • 写代码的时候遵循一些根本准则,比方繁多准则、依赖接口 / 形象而不是依赖具体实现。
  • 严格遵循编码标准、非凡正文应用 TODO、FIXME、XXX 进行正文。
  • 单元测试、功能测试、接口测试、集成测试是写代码必不可少的工具。
  • 咱们是代码的作者,前人是代码的读者。写代码要时刻扫视,做前人栽树后人乘凉、不做前人挖坑前人陪葬的事件。
  • 不做破窗效应的第一人,不要感觉当初代码曾经很烂了,没有必要再改,间接持续堆代码。如果是这样,总有一天本人会被他人的代码恶心到,“进去混迟早是要还的”。

重构技巧

  • 从上至下,由外到内进行建模剖析,理清各种关系,是重构的重中之重。
  • 提炼类,复用函数,下沉外围能力,让模块职责清晰明了。
  • 依赖接口优于依赖形象,依赖形象优于依赖实现,类关系能用组合就不要继承。
  • 类、接口、形象接口设计时思考范畴限定符,哪些能够重写、哪些不能重写,泛型限定是否精确。
  • 大型重构做好各种设计和打算,线下模仿好各种场景,上线肯定须要 AB 验证程序,可能随时进行新老切换。

正文完
 0