精益求精!记一次业务代码的优化探索

全栈前端精选

共 7552字,需浏览 16分钟

 ·

2021-09-10 16:01

关键词:需求实现、设计模式、策略模式、程序员成长

本篇文章由淘系新人喜橙同学撰写。

承启:

本篇从业务场景出发,介绍了面对一个复杂需求,拆解重难点、编码实现需求、优化代码、思考个人成长的过程。

  • 会介绍一个运用策略模式的实战。
  • 需求和编码本身小于打怪升级成长路径。
  • 文中代码为伪代码。

场景说明:

需求描述:手淘内“充值中心”要投放在饿了么、淘宝极速版、UC浏览器等集团二方APP。拿到需求之后,来梳理下“充值中心”在他端投放涉及到的核心功能点

  • 通讯录读取 不同客户端、操作系统,JSbridge API实现略有不同。
  • 支付 不同端支付JSbridge调用方式不同。
  • 账号体系:集团内不同端账号体系可能不同,需要打通。
  • 容器兼容 手淘内采用PHA容器,淘宝极简版本投放H5,饿了么以手淘小程序的方式投放。环境变量、通信方式等需要兼容。
  • 各端个性化诉求 极速版投放极简链路,只保留核心模块等。

解决方案

需求明确了:充值相关核心模块,需要兼容每个APP,本质是提供一个多端投放的解决方案。那么这个场景如何编码实现呢?

1、方案一

首先第一个想法💡,在每个功能点模块用if-else判断客户端环境,编写此端逻辑。下面以获取通讯录列表功能为例,代码如下:

// 业务代码文件 index.js
/**
 * 获取通讯录列表
 * @param clientName 端名称
 */

const getContactsList = (clientName) => {
  if (clientName === 'eleme') {
    getContactsListEleme()
  } else if (clientName === 'taobao') {
    getContactsListTaobao()
  } else if (clientName === 'tianmao') {
    getContactsListTianmao()
  } else if (clientName === 'zhifubao') {
    getContactsListZhifubao()
  } else {
    // 其他端
  }
}

写完之后,review一下代码,思考一下这样编码的利弊。

:逻辑清晰,可快速实现。

:代码不美观、可读性略差,每兼容一个端都要在业务逻辑处改动,改一端测多端。

这时,有的同学就说了:“把if-else改成switch-case的写法,把获取通讯录模块抽象成独立的sdk封装,用户在业务层统一调用”,天才!动手实现一下。

2、方案二

核心功能模块,抽象成独立的sdk,模块内部对不同的端进行兼容,业务逻辑里统一方式调用。

/**
 * 获取通讯录列表 sdk caontact.js
 * @param clientName 端名称
 * @param successCallback 成功回调
 * @param failCallback 失败回调
 */

export default function (clientName, successCallback, failCallback{
  switch (clientName) {
    case 'eleme':
      getContactsListEleme()
      break
    case 'taobao':
      getContactsListTaobao()
      break
    case 'zhifubao':
      getContactsListTianmao()
      break
    case 'tianmao'
      getContactsListZhifubao()
      break
    default:
      // 省略
      break
  }
}

// 业务调用 index.js
<Contacts onIconClick={handleContactsClick} />

import getContactsList from 'Contacts'
import { clientName } from 'env'
const handleContactsClick = () => {
  getContactsList(
    clientName,
    ({ arr }) => {
      this.setState({
        contactsList: arr
      })
    },
    () => {
      alert('获取通讯录失败')
    }
  )
}

惯例,review一下代码:

:模块分工明确,业务层统一调用,代码可读性较高。

:多端没有解藕,每次迭代,需要各个端回归。

上面的实现,看起来代码可读性提高了不少,是一个不错的设计,可是这样是最优的设计吗?

3、方案三

熟悉设计模式的同学,这时候可能要说了,用策略模式啊,对了,这个场景可以用策略模式。这里简单解释一下策略模式:策略模式,英文全称是 Strategy Design Pattern。在 GoF 的《设计模式》一书中,它是这样定义的:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。

难免有些晦涩,什么意思呢?我个人的理解为:策略模式用来解耦策略的定义、创建、使用。它典型的应用场景就是:避免冗长的if-else或switch分支判断编码。

下面看代码实现:

/**
 * 策略定义
 */

const strategies = {
  eleme() => {
    getContactsListEleme()
  },
  taobao() => {
    getContactsListTaobao()
  },
  tianmao() => {
    // 省略
  }
}
/**
 * 策略创建
 */

const getContactsStrategy = (clientName) => {
  if (!clientName) {
    throw new Error('clientName is empty.')
  }
  return strategies[clientName]
}
/**
 * 策略使用
 */

import { clientName } from 'env'
getContactsStrategy(clientName)()

策略模式的运用,把策略的定义、创建、使用解耦,符合设计原则中的迪米特法则(LOD),实现“高内聚、松耦合”。当需要新增一个适配端时,我们只需要修改策略定义Map,其他代码都不需要修改,这样就将代码改动最小化、集中化了。

能做到这里,相信你已经超越了一部分同学了,但是我们还要思考、精益求精,如何更优呢?这个时候单从编码层面思考已经受阻塞了,可否从工程构建角度、性能优化角度、项目迭代流程角度、后期代码维护角度思考一下,相信你会有更好的想法。

下面抛砖,聊聊我自己的思考:

4、方案四

从工程构建和性能优化角度出发:如果每个端独立一个文件,构建的时候shake掉其他端chunk,这样bundle可以变更小,网络请求也变更快。

等等... Tree-Shaking是基于ES静态分析,我们的策略判断,基于运行时,好像可能没什么用啊。

方案三使用策略模式来编码,本质是策略定义、创建和使用解藕,那可否使用刚才的想法,把每端各个功能模块兼容方法聚合成独立module,从更高维度,将多端业务策略定义、创建和使用解藕?

思考一下这样做的收益是什么?

因为每个端的适配,聚合在一个module,将多端业务策略解藕,某个端策略变更,只需要修改此端module,代码改动较小,且后续测试链路,不需要重复回归其他端。符合“高内聚、松耦合”。

代码实现:

/**
 * 饿了么端策略定义module
 */

export const elmcStrategies = {
  contacts() => {
    getContactsListEleme()
  },
  pay() => {
    payEleme()
  },
  // 其他功能略
}
/**
 * 手淘端策略定义module
 */

export const tbStrategies = {
  contacts() => {
    getContactsListTaobao()
  },
  pay() => {
    payTaobao()
  },
  // 其他功能略
};
// ...... (其他端略)
/**
 * 策略创建 index.js
 */

import tbStrategies from './tbStrategies'
import elmcStrategies from './elmcStrategies'
export const getClientStrategy = (clientName) => {
  const strategies = {
    elmc: elmcStrategies,
    tb: tbStrategies
    // ...
  }
  if (!clientName) {
    throw new Error('clientName is empty.')
  }
  return strategies[clientName]
};
/**
 * 策略使用 pay
 */

import { clientName } from 'env'
getClientStrategy(clientName).pay()

代码目录如下图所示:



index.js是多端策略的入口,其他文件为各端策略实现。

从方案四的推导来看,有时候,判断不一定是对的,但是从多个维度去思考,会打开思路,这时,更优方案往往就找上门来了~

5、方案五

既要解决眼前痛点,也要长远谋划,基于以上四种方案,再深入思考一步,如果业务有投放在第三方(非集团APP)的需求,比如投放在商家APP,且商家APP获取通讯录、支付逻辑等复杂多变,这个时候如何设计编码呢?例如:拉起别端的唤端策略,受多方因素影响,涉及到产品壁垒,策略攻防,怎样控制代码改动次数,及时提高唤端率呢?在这里简单抛砖,可以借助近几年很火的serverless,搭建唤端策略的faas函数,动态获取最优唤端策略,是不是一个好的方案呢?

沉淀&思考

以上针对多端兼容的问题,我们学习并运用了设计模式——策略模式。那么我们再来看看策略模式的设计思想是什么:一提到策略模式,有人就觉得,它的作用是避免 if-else 分支判断逻辑。实际上,这种认识是很片面的。策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入 bug 的风险。实际上,设计原则和思想比设计模式更加普适和重要。掌握了代码的设计原则和思想,我们能更清楚的了解,为什么要用某种设计模式,就能更恰到好处地应用设计模式。还有一点需要注意,在代码设计时,应该了解他的业务价值和复杂度,避免过度设计,如果一个if-else可以解决的问题,何必大费周折,阔谈设计模式呢?

总结

理一下全文的核心路径,也是我此篇文章想要主要传达的打怪升级成长路径。

接到一个复杂的需求--> 理清需求 --> 拆解技术难点 --> 编码实现 --> 代码优化 --> 设计模式和设计原则学习 --> 举一反三 --> 记录沉淀。

当下,前端工程师在工作中,难免会陷入业务漩涡中,被业务推着走。面对这种风险,我们要思考如何在保障完成业务迭代的基础上,运用适合的技术架构,抽象出通用解决方案,沉淀落地。这样,既能帮助业务更快更稳定增长,又能在这个过程中收获个人成长。

浏览 23
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报