Day02 - 观察:自由的程序码?有什麽蛛丝马迹、现象?

自由的潜在风险 -( 问题发生的原因 )

前一天的故事及开发范例,我们发现随着新需求的出现,我们必须回去修改既有的逻辑,这样子的情形,并不符合开放封闭原则,也很容易漏思考一些东西。

由於程序码的执行顺序是从上至下,从外到里。
理所当然的,排在前面 if / else statement 有命中,就会先进去里面执行,而忽略 else 之後的内容。

然而这看起来却只是问题的表象

“Fix the cause, not the symptom.” – Steve Maguire

过於自由带来的负担 - ( 问题可能产生的後果 )

或许你会说,错误有什麽关系、下次改版修回来就好了啊,QA 也应该要测到,或者 PM 新需求也应该想清楚或是要记得回去把旧规格书补好。

但假设你今天开发的是一个低容错率的系统,如:

  • 飞机操纵系统,一飞上天,可是几百条人命在空中的;
  • 金融系统,一个小错误可能就会瞬间损失大量现金等等...。

随着不断加入的新功能,我们或许会越改越没信心,很怕改一些东西,其他地方会不会坏掉?(改 A 坏 B)

https://ithelp.ithome.com.tw/upload/images/20210917/20130721zJ0BdQ3LRE.jpg

看医生时,我们希望医生不要治标不治本;
身为程序码医生,我们也要避免治标不治本。

问题的本质是什麽?

所以追根溯源,究竟问题的本质是什麽?为什麽有这个现象发生?这是今天要来探讨的

问题可能产生的後果

下面的举例,或许是一些常见的组合?

if(isLogin && !isLoading && hasData) // 登入、资料载完、有资料,画出特定 UI
if(isLogin && isBuyer && isPaid) // 登入、身份是买家、已付费,执行特定动作

透过

  1. 不断新增 flag 放入 if/ else 中或是
// From 旧功能
if(a && b)
// To 新功能
if(a && b && c)
  1. 不断增添 if / elseelse 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 越多),发生错误时的除错也会越来越困难。

除错难易度上升

每多一个 flag ,可能存在的状况就 x 2 倍,除错难度提高

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 这里的逻辑组合性,就变成 https://chart.googleapis.com/chart?cht=tx&chl=2%5E3 ,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 种可能。

扣掉第一种,其他七种状态的意义是什麽?或是说你可能不需要那麽多种,但防呆判断有没有可能漏掉

这也代表着我们状态的描述性是很差的

过於自由奔放的 code ,冗余的负担

同样以上面例子,或许在你的系统内 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 及 if/else 区块

新功能的开发,我们能不能避免建立过多的 flag、减低巢状、一长串的逻辑判断?
有没有办法更从使用者的角度出发?
(虽然是依据规格设计,但前面我们是以程序码的逻辑出发,长出一串逻辑判断)

我们再换个角度来看看需求,这次多靠近使用者观点一点。

独立存在的状态 (同一时间,只会有一个状态,不使用多个 flag 判断)

先不管攻击、防御等等,以角色移动而言,仔细观察我们的 站、跳跃、匍匐前进(左右负责控制移动的方向),其实跳跃、匍匐前进等等都不会同时存在,我们是不是可以视彼此为一种独立存在的状态。

isJumping, isCrawling 其实也只是为了判断以上的行为而存在的中介状态(暂时存在的状态)。

以一张电商的订单而言,买家提交订单(给卖家先确认库存),卖家确认有货可成立点选同意,订单进入等待付款,付款完成收帐後进入等待发货仓储人员配货、安排物流,送达目的地後等待取件,取件完成後订单交易完成

等待发货、等待取件不会同时存在(就像是跳跃跟俯卧不会同时存在),看起来我们需求中,订单的每个阶段,好像都能视为一种状态。

旧状态 → 新状态,有明确的转移路径

还没付款不能跳到等待发货的状态,

俯卧不能直接跳跃(要先站起来)

我们观察到,状态与状态间、有明确的转移路径(或是你刻意要限制明确的路径)

状态转移 合理性
站 → 跳 → 站 O合理
站 → 俯 → 站 O合理
跳 → 俯 → 站 X不合理
俯 → 跳 → 站 X不合理
状态转移 合理性
提交 → 待付款 → 待发货→ 待取件→ 交易完成 O合理
提交 → 待取件→ 交易完成 → 待付款 → 待发货 X不合理
提交 → 待发货 → 待取件 → 交易完成 → 待付款 X不合理

我们现在发现了这几个有趣的现象,明天一起继续往下探索吧!


小结

解释了昨天的解决方案带来了什麽困扰及难题

  • 状态不具描述性、可读性低
  • 违反开放封闭原则
  • 透过一系列 if / else 判断状态、进行防呆,当新增、修改功能时
    1. 要回去改动旧功能的程序码
    2. 每新增一个状态 flag ,除错复杂度就大幅提升
    3. 状态描述性下降、可读性降低
    4. 沟通成本提升
    5. 必须注意逻辑之间的连动性、相依性
    6. 容易漏思考东西
    7. 容易产生冗余的逻辑语句
  • 换个角度观察
    1. 独立的状态
    2. 旧状态 → 新状态,有明确的转移路径

参考文献


<<:  每个人都该学的30个Python技巧|技巧 17:Python容器—元组(Tuple)(字幕、衬乐、练习)

>>:  从 JavaScript 角度学 Python(16) - pip

Day 29 - Math Object & Date Object

Math Object methods Math.PI : 3.14 Math.LOG10E : 以...

IPv6 路由问题

架构图 https://imgur.com/gRWBf3i DIR-818 的路由表 https:/...

战略管理(strategic management)

我在这篇文章中介绍战略管理。我的书《有效的CISSP:安全和风险管理》中有详细信息。 政策(Poli...

Day23 - 中断...

开头,先跟追踪此系列的读者道歉, 我失败了。 是的,我决定在这天为我的系列划下一个不是很好的句点,却...

html 输入框

今天来写一个输入框,以下是html内的程序码 <input type="text&q...