每期必做的开发设计,来一场合理性探索吧【玩转前端】
想找「神之一手」?
《棋魂》是我很喜欢的一部电视剧,里面的主人公之一褚嬴,作为曾经的南梁围棋第一人,一直在找寻领悟围棋的最高境界“神之一手”。
“神之一手”,即棋手在下棋的过程中,领悟到了如同神一般的技艺,在关键时刻走出的影响全盘棋局的一步。
我不会下棋,却喜欢观看竞技比赛。
而作为开发者的我,也一直期待能在编写程序的过程中,设计出令自己“拍案”的“神之一手”。
每期都做设计,聊胜于无?
很长一段时间内,我都在寻找我的“神之一手”。
我不停的尝试,每一期功能都细致的做方案。聚合、分离、组合、中间件...
大圣有七十二种变化,我有七十二种设计模式。
开发设计确实帮助我提升了效率,并且简化了复杂的交互,提升了代码的复用率和可读性。
但是,也带来了三个问题:
- 复杂场景过于追求逻辑复用,前期确实节省了开发量,但是后期额外增加了阅读代码的时间,以及可能发生新增功能有遗漏的情况。
- 设计焦虑。如果某一期的功能过于简单,或者重复开发,没有新的设计方案,我会有焦虑感。
- 过渡设计。粗略的功能开发没效率,但是过于颗粒化的设计又会导致出现深层的嵌套。本来简单的修改,但是需要读十几分钟代码。
合理性探索
"纸上得来终觉浅",我举几个例子,帮助大家有更具体的观感。
场景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畅想」系列作者,华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)