每期必做的开发设计,来一场合理性探索吧【玩转前端】

举报
叶一一 发表于 2023/07/27 12:40:53 2023/07/27
【摘要】 而作为开发者的我,也一直期待能在编写程序的过程中,设计出令自己“拍案”的“神之一手”。但是设计的过于细致不利于后期维护,设计的过于粗糙,不利于提升代码的复用率。如何寻找合适的设计边界,是本文的核心内容。本文通过三个例子,解答前面的困惑,让我们一起来探索一下吧。

想找「神之一手」?

《棋魂》是我很喜欢的一部电视剧,里面的主人公之一褚嬴,作为曾经的南梁围棋第一人,一直在找寻领悟围棋的最高境界“神之一手”。

“神之一手”,即棋手在下棋的过程中,领悟到了如同神一般的技艺,在关键时刻走出的影响全盘棋局的一步。

我不会下棋,却喜欢观看竞技比赛。

而作为开发者的我,也一直期待能在编写程序的过程中,设计出令自己“拍案”的“神之一手”。

每期都做设计,聊胜于无?

很长一段时间内,我都在寻找我的“神之一手”。

我不停的尝试,每一期功能都细致的做方案。聚合、分离、组合、中间件...

大圣有七十二种变化,我有七十二种设计模式。

开发设计确实帮助我提升了效率,并且简化了复杂的交互,提升了代码的复用率和可读性。

但是,也带来了三个问题

  • 复杂场景过于追求逻辑复用,前期确实节省了开发量,但是后期额外增加了阅读代码的时间,以及可能发生新增功能有遗漏的情况。
  • 设计焦虑。如果某一期的功能过于简单,或者重复开发,没有新的设计方案,我会有焦虑感。
  • 过渡设计。粗略的功能开发没效率,但是过于颗粒化的设计又会导致出现深层的嵌套。本来简单的修改,但是需要读十几分钟代码。

合理性探索

"纸上得来终觉浅",我举几个例子,帮助大家有更具体的观感。

场景1

场景描述

一个统计数量的功能的功能,根据需要在页面展示统计总量或失败数量。

  • 总量:数量相加之和。
  • 失败数量:数量相加之和乘以失败率。

功能设计

两个类型虽然不同,但是大致的逻辑是相似的,可以找到相似之处,做统一处理。

  • 先定义一个公共方法:commonHandle。
  • 在 commonHandle 里又调用了一个处理运算的方法:getAdd。
  • getAdd 中根据操作类型进行计算,将得到的计算结果赋值到对应的赋值方法中。
/** @name 计算总量  */
const [count, setCount] = useState(0);
/** @name 失败的数量  */
const [num, setNum] = useState(0);

/**
 * 加法
 */
const add = (x, y) => {
  return x + y;
};

/**
 * 处理运算
 */
const getAdd = (type, params) => {
  let sum;
	// 条件判断
  if (type === 1) {
    sum = add(params.a, params.b);
  }
  if (type === 2) {
    sum = params.k * add(params.a, params.b);
  }
  // 或 
	// 枚举
  const sumObj = {
    1: add(params.a, params.b),
    2: params.k * add(params.a, params.b),
  };
  sum = sumObj[type];
  params.setFunc(sum);
};

/**
 * 公共处理
 */
const commonHandle = (type, params) => {
  getAdd(type, params);
};

/**
 * 操作-全部
 */
const allOk = (a, b) => {
  // 公共处理
  commonHandle(1, { a, b, setFunc: setCount });
  // 其他处理
};

/**
 * 操作-失败
 */
const failed = (a, b, k) => {
  // 公共处理
  commonHandle(2, { a, b, k, setFunc: setNum });
  // 其他处理
};

上面代码中,将计算单独抽离出来,根据 type 值的不同,增加不同的条件判断和计算处理。

咋一看,没什么毛病,或者说把条件判断改成枚举的方式也可以。

实际关键的点,在于,简单的计算真的需要再抽离一次吗?

如果,我需要改获取失败数量的计算规则,我寻找的链路是:

failed→commonHandle→getAdd

这还是举例简单的情况,链路层数看着不是很多。

在这里例子中,不做功能的抽离,代码实现也很简单:

/** @name 计算总量  */
const [count, setCount] = useState(0);
/** @name 失败的数量  */
const [num, setNum] = useState(0);

/**
 * 加法
 */
const add = (x, y) => {
  return x + y;
};

/**
 * 操作-全部
 */
const allOk = (a, b) => {
  const countSum = add(a, b);
  setCount(countSum);
  // 其他处理
};

/**
 * 操作-失败
 */
const failed = (a, b, k) => {
  const numSum = k * add(a, b);
  setNum(numSum);
  // 其他处理
};

场景2

场景描述

选择商品的弹窗。可根据关键字进行搜索,在搜索的结果中选择需要购买的商品,并在选择之后关闭弹窗,页面回显商品信息。

功能设计

这个功能并不难实现。里面包含的变量和操作略多。

对于弹窗中的多个变量,采用对象变量的方式,这样赋值函数可以用一个就好。

const [goodInfo, setGoodInfo] = useState({
  goodList: [], // 搜索到的商品列表
  good: {}, // 选择的商品
  goodName: '', // 搜索关键字
});
const [show, setShow] = useState(false);

/**
 * 打开选择商品弹窗
 */
const openGoodModal = () => {
  setShow(true);
};

/**
 * 关闭选择商品弹窗
 */
const closeGoodModal = () => {
  setShow(false);
};

/**
 * 选择操作
 * @param {Object} item 选择的商品对象
 */
const chooseGood = item => {
  setGoodInfo({
    ...goodInfo,
    good: item,
  });
  setShow(false);
};

/**
 * 输入框change事件
 * @param {Event} e
 */
const inputChange = e => {
  let value = e.target.value;
  setGoodInfo({
    ...goodInfo,
    goodName: value,
  });
  getGoodList(value);
};

/**
 * 根据输入的关键字获取商品列表
 * @param {string} value
 */
const getGoodList = value => {
  // 通过远程接口获取商品列表:res.list
  let list = res.list;
  setGoodInfo({
    ...goodInfo,
    goodList: list,
  });
};

对于上面代码中的多个操作中,使用 setGoodInfo 函数赋值的逻辑,可复用也可不复用。

从代码阅读理解、可扩展、维护成本,上面的设计和下面做了复用之后的设计,相差不大。

/**
 * 设置商品对象的值
 */
const initGoodInfoValues = (params = {}) => {
  setGoodInfo({
    ...goodInfo,
    ...params,
  });
};

/**
 * 输入框change事件
 * @param {Event} e
 */
const inputChange = e => {
  let value = e.target.value;
  initGoodInfoValues({ goodName: value });
  getGoodList(value);
};

/**
 * 根据输入的关键字获取商品列表
 * @param {string} value
 */
const getGoodList = value => {
  // 通过远程接口获取商品列表:res.list
  let list = res.list;
  initGoodInfoValues({ goodList: list });
};

场景3

场景描述

有一个商品不同性质的介绍的聚合页面,每个模块都可以跳转到对应的详情页,而每个详情链接都需要通过请求对应的远程接口获取。

功能设计

  • 每个获取函数中,使用对应的远程接口进行异步请求,获取返回的结果。
  • 拿到结果中的链接变量:url。
  • 使用 window.location.href 方法打开该链接。
/** @name 实际的商品id  */
const id = 1;

/**
 * 性质1-获取的详情页接口
 */
const goToDetail1 = () => {
  getApi1({ id }).then(res => {
    let { url } = res;
    window.location.href = url;
  });
};

/**
 * 性质2-获取的详情页接口
 */
const goToDetail2 = () => {
  getApi2({ id }).then(res => {
    const { url } = res;
    window.location.href = url;
  });
};

/**
 * 性质3-获取的详情页接口
 */
const goToDetail3 = () => {
  getApi3({ id }).then(res => {
    const { url } = res;
    window.location.href = url;
  });
};

/**
 * 性质4-获取的详情页接口
 */
const goToDetail4 = () => {
  getApi4({ id }).then(res => {
    const { url } = res;
    window.location.href = url;
  });
};

/**
 * 性质5-获取的详情页接口
 */
const goToDetail5 = () => {
  getApi5({ id }).then(res => {
    const { url } = res;
    window.location.href = url;
  });
};

看这个代码,多么的板正,除了接口函数不同,其他一模一样。

等等,一模一样,那岂不是可以做点设计。

/**
 * 获取的详情页接口的公共方法
 * @param {Function} api 请求接口
 */
const goToDetailCommon = api => {
  api({ id }).then(res => {
    let { url } = res;
    window.location.href = url;
  });
};

/**
 * 性质1-获取的详情页接口
 */
const goToDetail1 = () => {
  goToDetailCommon(getApi1);
};

/**
 * 性质2-获取的详情页接口
 */
const goToDetail2 = () => {
  goToDetailCommon(getApi2);
};

/**
 * 性质3-获取的详情页接口
 */
const goToDetail3 = () => {
  goToDetailCommon(getApi3);
};

/**
 * 性质4-获取的详情页接口
 */
const goToDetail4 = () => {
  goToDetailCommon(getApi4);
};

/**
 * 性质5-获取的详情页接口
 */
const goToDetail5 = () => {
  goToDetailCommon(getApi5);
};

总结

上面一共列举了三个实际业务场景,分别对应三种不同的逻辑复用建议:不建议抽离的过于细致、可抽离也可不抽离、建议抽离。

某些情况下,抽离的过于细致,函数嵌套过深。再次修改时,不容易想找到修改的位置。

有时候不做抽离,功能也十分简单。

所以:

  • 逻辑复用,并不是抽离的越精细越好。
  • 如果做完复用,增加了额外的条件判断或者枚举,需要考虑其必要性。

作者:非职业「传道授业解惑」的开发者叶一一
简介:「趣学前端」、「CSS畅想」系列作者,华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。