我是如何将业务代码写优雅的

35,483

0x00 前言

我是一名来自蚂蚁金服-保险事业群的前端工程师,在一线大厂的业务部门写代码,非常辛苦但也非常充实。业务代码不同于框架代码、个人项目或者开源项目,它的特点在于逻辑复杂、前后依赖多、可复用性差、迭代周期短,今天辛辛苦苦写的代码,上线运行一周可能就下线了。能熟练书写框架代码、构建底层基础设施的工程师不一定能写好业务代码。

有人说,业务代码无非就是按部就班,优不优雅?who care?。但实际业务规则复杂得多,不是依葫芦画瓢就能轻松解决的,写一段糟糕的代码,可能要用双倍的时间去发现和解决问题,麻烦了自己、也难受了和你并肩作战的战友。

有时候为了减少重复开发的成本,反复提炼和沉淀有复用价值的功能,就需要我们对业务代码进行合理抽象、甚至精雕细琢,要把业务代码写得优雅并非易事。我一直认为,程序设计和搬砖的最大区别在于设计二字,写代码也是一门艺术活。今天就借此机会,站在前端工程师的视角,给大家分享关于书写业务代码的最佳实践。

0x01 渐进式重构

渐进式重构是不断地对既有代码进行抽象、分离和组合。做代码重构之前需要回答两个问题:

1、什么样的代码需要重构?
2、何时进行重构?

设计不是一蹴而就的,有时候写着写着才发现某些代码可以抽离出来单独使用,需要重构的代码需要满足几个条件:

1、代码后期可复用
2、代码无副作用
3、代码逻辑单一

过早重构可能会因需求变化太快白白浪费许多时间;过晚重构会因为代码逻辑复杂、相似代码积压过多导致变更风险太高,难以维护。渐进式重构如下图所示(红色部分为增加的代码):

enter image description here

首先我们在同一个源文件中新增功能,发现部分代码无副作用且可分离,因此在同一个文件中进行代码分割,形成许多功能单一的模块。如此往复后发现单文件的体积越来越大,此时就可以将功能相关联的模块抽出来放到单独的文件中统一管理,如 helpers、components、constants 等等。

0x02 高内聚低耦合

高内聚低耦合一直是软件设计领域里亘古不变的话题,重构的目标是提高代码的内聚性,降低各功能间的耦合程度,降低后期维护成本,特别是写业务代码,这一点相当重要。

举个栗子,比如新需求希望在现有的产品页面上增加发红包功能,以吸引用户开通某个功能,按照正常逻辑,我需要:

1、在当前页面中引入相关依赖
2、初始化,查询红包相关信息
3、用户点击时,触发红包发送

白色部分表示上个版本的代码,红色部分表示完成这个需求需要变更的代码:

enter image description here

这样一来,这个发红包功能就和以前的代码严重耦合,如果这是个只需要上线一周的临时需求,下线代码的时候就是一个高风险的动作;如果上线运行期间还需要对产品页面进行迭代,越往后就越搞不清楚谁是谁了。合理的设计应该是下面这个样子的:

enter image description here

将和产品代码无关的功能性代码拆分出来,放到另一个文件中内部维护好整个生命周期状态,对外只暴露少量的接口或是方法,这样一来对产品页面的改造只需要:

1、引入红包组件
2、用户点击时,调用红包组件的发奖方法

这样的变更是极小的、明确的、可控的。换句话说,整个红包功能是高内聚的,与产品代码是低耦合的。这样实践也带来另一个好处:我得到了一个可复用的红包组件!

0x03 合理冗余

业务需求是多变的,写出来的代码也是如此,频繁地抽象很可能导致过度设计,一个抽象很可能随着迭代次数的增多变得十分复杂。在存在多个变量的分支业务场景,比如同时包含活动是否过期、是否已参加活动、是否完成一次任务这样的情况,会存在多个嵌套 if-else 结构,这时将代码冗余设计是个不错的选择。下面举一个例子来说明什么是合理冗余:

e.g. 有这样一个需求,一开始很简单,需要设计两个运营展位:

enter image description here

那么抽象一个组件:

const Item = ({ title, content }) => (
  <div>
    <h4>{title}</h4>
    <p>{content}</p>
  </div>
);

现在需求要求在第一个展位的标题上增加热文标记:

enter image description here

也很容易:

const Item = ({ title, content }, index) => (
  <div>
    <h4>{title}{index === 0 && <span>hot</span>}</h4>
    <p>{content}</p>
  </div>
);

需求又变了,要求:在第一个展位去掉内容,并且在下方加个按钮;第二个展位的标题右边增加一个超链接以及增加一个副标题:

enter image description here

这下有点恶心了:

const Item = ({ title, content }, index) => (
  <div>
    <h4>
      {title}
      {index === 0 && <span>hot</span>}
      {index === 1 && <a href="xxx">去看看</a>}
    </h4>
    {index === 1 && <h5>副标题</h5>}
    <p>
      {index !== 0 && content}
      {index === 0 && <button>领福利<button>}
    </p>
  </div>
);

可以看到,之前抽象的好好的,现在需求一变,代码就面目全非了,中间混杂着两个状态(第一个、第二个)的判断逻辑。实际情况很可能比这个更复杂,在多状态交织逻辑难以通过一套代码表达清楚时,进行合理冗余就是个不错的选择,将上面的例子用两个 if 重写如下:

// 第一个展位
if (index === 0) {
  return (
    <div>
      <h4>标题一<span>hot</span></h4>
      <p><button>领福利<button></p>
    </div>
  );
}
// 第二个展位
if (index === 1) {
  return (
    <div>
      <h4>标题二<a href="xxx">去看看</a></h4>
      <h5>副标题</h5>
      <p>内容</p>
    </div>
  );
}

合理冗余其实也是一种重构,根据业务逻辑和代码规模,做相似抽象还是代码冗余,这其实也是渐进式重构的一种体现。无论采用何种方式,只要能把业务逻辑表达清楚,让代码始终保持良好的可读性和可维护性,就OK。

下面介绍一个过度抽象的例子。

0x04 拒绝过度抽象

在 JavaScript 代码中进行深度抽象有时并非好事,有 OOP(面向对象编程)背景的同学很容易先入为主设计:所有数据结构都想封装成一个类 (Class) 。实际上 Class 在 JavaScript 中是个不好的设计,它并非真正的类。几年前,我曾看到一位 Java 转前端的同学写出了类似这样的代码:

class DataItem {
  constructor(id, name, value) {
    this.id = id;
    this.name = name;
    this.value = value;
  }
}

class DataCollection {
  constructor() {
    this.items = new Array();
  }
  insert(item) {
    this.items.push(item);
  }
}

const item1 = new DataItem(1, 'name1', 100);
const item2 = new DataItem(2, 'name2', 200);
const list = new DataCollection();
list.insert(item1);
list.insert(item2);
...

一股浓浓的 Java 味道扑面而来。上面的代码并没有发挥出 JavaScript 的语言优势,也增加了不少理解成本,如果用面向对象编程的思路去写前端代码,特别是业务代码,可真是一场噩梦。正确的写法如下:

const list = [{
  id: 1,
  name: 'name1',
  value: 100
}, {
  id: 2,
  name: 'name2',
  value: 200
}];

由于 JS 属于弱类型语言,弱类型语言就要发挥弱类型的优势,无需过多类型定义和 Class 抽象,用最原始的 object 和 function 足以胜任从简单到复杂的业务场景。这里特别想提及前端所熟知的 Redux 状态管理器,Redux 中,state 就是普通的 object,reducer 就是普通的 function,action 也是普通的 object,不加任何类型约束。因为简单,所以强大。

0x05 眼观六路

用弱类型语言编程意味着无需编译,无需编译的语言天生存在一个问题是在运行前缺少必要的类型检查,将问题暴露在运行时往往会导致非常严重的故障。这就要求开发者能在写代码的阶段严格保证代码质量,特别是写业务代码。

集成开发环境(IDE)对 JavaScript 代码的智能提示能力有限,很多时候不能通过 IDE 查找某个变量或者函数的所有引用,这时就要善用 Ctrl + F 进行全局查找来保证自己的单点变更不会影响到其他地方。如果使用 TypeScript,在类型检查、引用查找上的帮助会更好。

0x06 总结

今天给大家分享了关于书写业务代码的一些实践经验:对代码进行渐进式重构是提升代码健壮性的有力武器;设计高内聚低耦合的代码可以让你在做需求的过程中沉淀出一套通用解决方案;合理冗余可以简化复杂的场景,让开发变得高效、测试变得容易;拒绝过度抽象,拥抱简单,灵活变化。保持 眼观六路 的好习惯能让代码质量提升一个台阶。

最后,希望大家能在实际开发过程中去体会和学习,不断思考和总结,将业务代码写优雅,是个很大的挑战。


关于我们:

我们是蚂蚁保险体验技术团队,来自蚂蚁金服保险事业群。我们是一个年轻的团队(没有历史技术栈包袱),目前平均年龄92年(去除一个最高分8x年-团队leader,去除一个最低分97年-实习小老弟)。我们支持了阿里集团几乎所有的保险业务。18年我们产出的相互宝轰动保险界,19年我们更有多个重量级项目筹备动员中。现伴随着事业群的高速发展,团队也在迅速扩张,欢迎各位前端高手加入我们~

我们希望你是:技术上基础扎实、某领域深入(Node/互动营销/数据可视化等);学习上善于沉淀、持续学习;性格上乐观开朗、活泼外向。

如有兴趣加入我们,欢迎发送简历至邮箱:yiyuan.lpy@antfin.com


本文作者:蚂蚁保险-体验技术组-祎远

掘金地址:micooz