从前一天的故事及开发范例,我们发现随着新需求的出现,我们必须回去修改既有的逻辑,这样子的情形,并不符合开放封闭原则,也很容易漏思考一些东西。
由於程序码的执行顺序是从上至下,从外到里。
理所当然的,排在前面 if / else statement
有命中,就会先进去里面执行,而忽略 else
之後的内容。
然而这看起来却只是问题的表象
“Fix the cause, not the symptom.” – Steve Maguire
或许你会说,错误有什麽关系、下次改版修回来就好了啊,QA 也应该要测到,或者 PM 新需求也应该想清楚或是要记得回去把旧规格书补好。
但假设你今天开发的是一个低容错率的系统,如:
随着不断加入的新功能,我们或许会越改越没信心,很怕改一些东西,其他地方会不会坏掉?(改 A 坏 B)
看医生时,我们希望医生不要治标不治本;
身为程序码医生,我们也要避免治标不治本。
所以追根溯源,究竟问题的本质是什麽?为什麽有这个现象发生?这是今天要来探讨的
下面的举例,或许是一些常见的组合?
if(isLogin && !isLoading && hasData) // 登入、资料载完、有资料,画出特定 UI
if(isLogin && isBuyer && isPaid) // 登入、身份是买家、已付费,执行特定动作
透过
if/ else
中或是// From 旧功能
if(a && b)
// To 新功能
if(a && b && c)
if / else
及 else if
的开发特性// From 旧功能
if(a && b)
else
// To 新功能
if(a && b)
else if (新条件)
else if (超新条件)
else
就会造成新功能开发时,就要往上、往前去进行检查
除了可读性会越来越差之外,程序码意义是由许多破碎的小单元组成,是隐含的、命令式的!
if(flagA && flagB && flagC && flagD)
? (背後代表的意义)if(flagA && flagB && flagC && flagD)
? (组合产生的原因,像昨天的一些防御、防呆:跳跃不能直接进到匍匐)充满命令式(imperative) 的逻辑越来越多(当你 flag 越多),发生错误时的除错也会越来越困难。
if(flagA) doSomething()
举例 flagA,遇到错误时,我们就是只有 2 种可能 flagA is true
or flagA is false
,此时我们只要去检查 flagA 为啥会判断错误即可
if(flagA || flagB) doSomething()
今天多了 1 个 flagB,发生错误时的检查,变成 4 种可能
A | B | A or B |
---|---|---|
true |
true |
true |
true |
false |
true |
false |
true |
true |
false |
false |
false |
再多一个 flag 这里的逻辑组合性,就变成 ,8 种可能。
再换一个角度想想,其实这些 flag 是不是都代表一些小状态,我们想透过这些小状态组合出一个大的状态。我们可以说将这些小状态堆叠出大状态的过程是,由小至大,由下而上(抽象层次)。
随着功能、需求不断新增,我们需要的 flag 可能就越来越多,程序码就越来越容易出 bug。
if(isLogin&&!isLoading&&hasData) // 登入、资料载完、有资料,画出特定 UI
if(isLogin&&isBuyer&&isPaid) // 登入、身份是买家、已付费,执行特定动作
其实 isLogin&&!isLoading&&hasData 就只是想表达有「登入载完资料後的画面」,
这已经是 happy path,但你还要去思考 not Login + loading + noData ... 这 2 x 2 x 2 总个 8 种可能。
扣掉第一种,其他七种状态的意义是什麽?或是说你可能不需要那麽多种,但防呆判断有没有可能漏掉
这也代表着我们状态的描述性是很差的
同样以上面例子,或许在你的系统内 loading 就只有一种整页是满版转圈圈的画面,只要 isLoading === true
,其他 isLogin, hasData 都是多余的。
(isLogin && !isLoading && hasData) ? <DataTable /> : <EmptyWrapper />
这样是不是也增进未来阅读的负担,要多停下思考
比如 isLoading: true, isLogin: false, hasData: true 这个语句可能对你的系统是无意义的
没看过的人可能还要思考在 isLoading: true, isLogin: false, hasData: true 的意义是什麽?
正在 Loading 但有资料、却没登入???这什麽意思?
但假设设计师今天仅来得及先出 2 种画面,loading:<LoadingSpinner /> 及 loaded:<DataTable /> ,一时专案赶或是为求程序码简洁,直接抽换掉後面的元件
(isLogin && !isLoading && hasData) ? <DataTable /> : <LoadingSpinner />
有发现哪里怪怪的吗?
isLogin | isLoading | hasData | 意义 | |
---|---|---|---|---|
true |
true |
true |
Loading | 除了isLoading 另外2 flag 冗余 |
true |
true |
false |
Loading | 除了isLoading 另外2 flag 冗余 |
true |
false |
true |
Loaded /(List View) | 冗余(假如设计师没出 2 种区别,通通用 Table 装起来而已) |
true |
false |
false |
Loaded / (Empty View) | 冗余(假如设计师没出 2 种区别,通通用 Table 装起来而已) |
... | ... | ... | 无意义 | |
... | ... | ... | 无意义 |
如果依照这个逻辑写下去,当登入後、拿回 API response 时,是空的没有资料,现在这组逻辑就会永远只看到转圈圈 <LoadingSpinner />
,就变成是等设计师出完空资料画面<EmptyWrapper />
之前,要拔掉 hasData
这个 flag ,因为这阶段根本不需要它的存在。
有发现哪里怪怪的吗?
其实这个逻辑语句太强了,只关注到 3 个 flag 都是 true
的结果,当不是 true
时的意义没有被正确定义时,用三元运算後面回传什麽都很怪
(isLogin && !isLoading && hasData)
当 hasData===false
回传 <LoadingSpinner />
... 怪
(isLogin && !isLoading && hasData)
当 isLogin===false
回传 <LoadingSpinner />
或 <EmptyWrapper />
... 可能都怪 (应该要 redirect 到登入页)
为了避免这个情形,如果又改使用 巢状 if else if{if{}else{}}
,开发起来很难读,随着需求增删也要来来回回检查
同理在思考状态转换时,很容易 miss 小东西,比如说有个 isLogin, isAdmin 的 flag 组合,照理说当 isAdmin 是 true
时,isLogin 一定要是 true
,但我们在开发时,很有可能漏加到。
isLogin | isAdmin | 意义 |
---|---|---|
false |
true |
Buggy 不应该存在,但现实很容易就会漏改 |
上面一堆 flag 组合起来的结果,光是工程师们都要花一段时间消化跟思考...
试问如何快速跟新人交接?
或是当 bug 发生时,我们如何跟工程师以外的人解释?
而且除错时,也很难快速发生问题、或很难跟 PM 讨论需求厘清是否存在逻辑矛盾或是漏处理到的 case。
新功能的开发,我们能不能避免建立过多的 flag、减低巢状、一长串的逻辑判断?
有没有办法更从使用者的角度出发?
(虽然是依据规格设计,但前面我们是以程序码的逻辑出发,长出一串逻辑判断)
先不管攻击、防御等等,以角色移动而言,仔细观察我们的 站、跳跃、匍匐前进(左右负责控制移动的方向),其实跳跃、匍匐前进等等都不会同时存在,我们是不是可以视彼此为一种独立存在的状态。
isJumping
, isCrawling
其实也只是为了判断以上的行为而存在的中介状态(暂时存在的状态)。
以一张电商的订单而言,买家提交订单(给卖家先确认库存),卖家确认有货可成立点选同意,订单进入等待付款,付款完成收帐後进入等待发货仓储人员配货、安排物流,送达目的地後等待取件,取件完成後订单交易完成
等待发货、等待取件不会同时存在(就像是跳跃跟俯卧不会同时存在),看起来我们需求中,订单的每个阶段,好像都能视为一种状态。
还没付款不能跳到等待发货的状态,
俯卧不能直接跳跃(要先站起来)
我们观察到,状态与状态间、有明确的转移路径(或是你刻意要限制明确的路径)
状态转移 | 合理性 |
---|---|
站 → 跳 → 站 | O合理 |
站 → 俯 → 站 | O合理 |
跳 → 俯 → 站 | X不合理 |
俯 → 跳 → 站 | X不合理 |
状态转移 | 合理性 |
---|---|
提交 → 待付款 → 待发货→ 待取件→ 交易完成 | O合理 |
提交 → 待取件→ 交易完成 → 待付款 → 待发货 | X不合理 |
提交 → 待发货 → 待取件 → 交易完成 → 待付款 | X不合理 |
我们现在发现了这几个有趣的现象,明天一起继续往下探索吧!
解释了昨天的解决方案带来了什麽困扰及难题
if / else
判断状态、进行防呆,当新增、修改功能时
<<: 每个人都该学的30个Python技巧|技巧 17:Python容器—元组(Tuple)(字幕、衬乐、练习)
>>: 从 JavaScript 角度学 Python(16) - pip
Math Object methods Math.PI : 3.14 Math.LOG10E : 以...
架构图 https://imgur.com/gRWBf3i DIR-818 的路由表 https:/...
我在这篇文章中介绍战略管理。我的书《有效的CISSP:安全和风险管理》中有详细信息。 政策(Poli...
开头,先跟追踪此系列的读者道歉, 我失败了。 是的,我决定在这天为我的系列划下一个不是很好的句点,却...
今天来写一个输入框,以下是html内的程序码 <input type="text&q...