本文主要介绍使用卡诺图化简多变量逻辑表达式的原理与方法,此方法是一种逻辑计算思想,在任意技术平台类似的多元化场景中均可适用。
本文以客户端的一个业务场景为例,从举例分析到实际应用的步骤,介绍卡诺图工具的使用,让我们轻松应对复杂交互或多条件判断的编码。
添加音乐按键,滤镜按键,拍摄键,特效选择器,特效清除键
(图1.1 所示红圈处),它们在拍照就绪状态下是都显示的,然后用户可能的操作流程有2种:拍照片和拍视频,而这2种操作可以分别有3个不同的效果选择:增加音乐,增加滤镜,增加特效,则此例中共有 2 * 3 = 6
种不同操作方式的组合,这些操作又分别涉及不同的控件,需要它们配合完成相应操作。即 m 种操作和 n 种效果,共 m*n
种组合方法。还有许多类似的corner case,我们若把全部case分列出来,则它们的控制逻辑会像星辰一样散落在各个处理方法中,作为中介作用的容器也会层层叠叠地出现,然后我们还需要写一大坨处理各种子状态、容器控件的方法,再根据当前用户操作去相应地调用;当页面里的控件数量增多,或需求变化了,或控件要增多/减少时,整个流程就不再具有清晰的层级关系或先后逻辑了。
更麻烦的是,我们每次看似简单的增删改操作,都要把相应业务的上下游方法全部梳理一遍,甚至和其他功能有交互的地方,也要关注到,这样的逻辑产生过高的复杂度也容易产生疏漏。要维护好这么一堆面条代码将不是一件令人愉快的事;若是对业务不熟悉就做修改,极易踩坑,导致改了A处又坏了B处,修了B处又影响了C处,正是因为各子业务的方法存在过度耦合,出bug就没完没了。
那么我们还有更优雅的方式来处理这种场景么?
相关变量 --(确定)--> 逻辑表达式 --(确定)--> 控件状态
func
refreshCurrentStatusUI()
,用于在任意时刻需要刷新页面状态时获得所需状态,而不用关心具体当前的操作或流程是什么,以便把状态从业务流程中独立出来。func refreshCurrentStatusUI() {
// 获取当前变量状态
let A: Bool = isA()
let B: Bool = isB()
let C: Bool = isC()
let D: Bool = isD()
...
// 设置控件交互
view01.isHidden = ABCD表达式1
view02.isHidden = ABCD表达式2
view03.isHidden = ABCD表达式3
...
}
在实际应用的方法体里,把与业务场景相关的参数状态列出来,比方说,获取与罗列的控件相关的几个必需变量,为了表述方便,这里用符号ABC来代表参数,当然你也可以用其他的符号。
func refreshCurrentStatusUI() {
// 当前数据状态
let hasVideo: Bool = isProgressHasSections() // A: 是否已录制视频
let hasEffect: Bool = hasSpecialEffect() // B: 是否已选有特效
let hasSubEffect: Bool = hasSubStickers() // C: 是否带有子特效
let hasSubEffectPhoto: Bool = hasSubStickersGetPhoto() // D: 是否已拍照填充了子特效 subEffect
let hasMusic: Bool = currentMusicData != nil || musicID > 0 // F: 是否有音乐
...
}
然后我们只需要继续在这个方法里,使用上述变量当下的值,计算每个控件状态的逻辑表达式,根据结果来刷新各控件状态即可:
// 拍摄键
takeButton.isHidden = !(hasVideo || hasSubEffectPhoto)
// [删除视频片段]按键
deleteButton.isHidden = !(hasVideo || hasSubEffectPhoto)
// 特效选择器
specialEffectPicker.isHidden = hasVideo || hasSubEffectPhoto
// 特效浮层键
stickerSelectionButton.isHidden = !hasVideo && (hasEffect || hasSubEffect) || hasSubEffectPhoto // (!A)(B+C)+D
// 特效清除键
specialEffectClearButton.isHidden = !hasEffect || (hasSubEffect || hasSubEffectPhoto) // !B || (C || D)
卡诺图[2]可以用于表示和化简逻辑函数。
一个逻辑函数的卡诺图,就是将此函数的最小项表达式中的各最小项填入相应的特定方格图内,这样的方格图就是卡诺图。
举例,比如 4变量的卡诺图,记ABCD
为所用4个变量,行与列头为4个变量分别对应的取值(true 或 false,对应记为逻辑 1 或 0 方便计算),16个方格中相邻的数字0~15
依顺序代表方格逻辑相邻:
(A、B、C)
,或以非变量形式出现(!A、!B、!C)
十进制数 | 二进制数 | 格雷码 | 十进制数 | 二进制数 | 格雷码 | |
0 | 0000 | 0000 | 8 | 1000 | 1100 | |
1 | 0001 | 0001 | 9 | 1001 | 1101 | |
2 | 0010 | 0011 | 10 | 1010 | 1111 | |
3 | 0011 | 0010 | 11 | 1011 | 1110 | |
4 | 0100 | 0110 | 12 | 1100 | 1010 | |
5 | 0101 | 0111 | 13 | 1101 | 1011 | |
6 | 0110 | 0101 | 14 | 1110 | 1001 | |
7 | 0111 | 0100 | 15 | 1111 | 1000 |
表3.1 格雷码编码规则
图4.1 三变量卡诺图
此图即为基本卡诺图的形式,两侧变量依据格雷码形式,目的就是画卡诺圈时要将里面的 1 全都包括在内[5]。
类似的,有4变量卡诺图描述16种状态,其结构如下所示:
图4.2 四变量卡诺图
卡诺图填写完成后,就可以作卡诺圈了,通过圈选与合并状态,我们可以将逻辑多项式化简得到其最小项表达式。
let hasVideo // A: 是否已录制视频
let hasEffect: // B: 是否已选有特效
let hasSubEffect: // C: 是否带有子特效
let hasSubEffectPhoto // D: 是否已拍照填充了子特效 subEffect
let hasMusic // F: 是否有音乐
takeButton
,然后把与之相关的变量找出来,按照业务要求得知,它的显示与隐藏只和“是否有视频”、“是否填充子特效”
这两个参数有关,当然你也可以把看似相关的其他参数带进去,在化简完成后,无关参数最终会被消去。hasVideo
,hasSubEffectPhoto
,对应符号是 A,D
,分别取0和1值。在下图中,A为行,B为列,填充到2变量的卡诺图中:图4.3 初始化卡诺图
takeButton
的显示状态,并在相应空格里填入0或1,含义由我们来定,这里取0为隐藏
,1为显示
较直观,那么有:A = 0,B = 0
-->无视频 & 无填充子特效
时,拍摄键要隐藏,所以取0,则在坐标(0,0)处填0;A = 0,B = 1
-->无视频 & 有填充子特效
时,拍摄键要显示,所以取1,则在坐标(0,1)处填1;A = 1,B = 0
-->有视频 & 无填充子特效
时,拍摄键要显示,所以取1,则在坐标(1,0)处填1;A = 1,B = 1
-->有视频 & 有填充子特效
时,拍摄键要显示,所以取1,则在坐标(1,1)处填1;已是最简表达式,再把符号AB再换回代码中的变量,有:
(hasVideo || hasSubEffectPhoto)
因为代码中使用的是 .isHidden
属性,只要把所得表达式再取反即有 !(A+B)
,写为代码的逻辑式就是:
takeButton.isHidden = !(hasVideo || hasSubEffectPhoto)
至此我们就得到了这个控件的状态表达式,在任何时候调用这个刷新方法,就只依赖参数做显示。
图4.6 特效浮层按键示意
同样先来看下计算后的最终结果:
// 特效浮层键
stickerSelectionButton.isHidden = !hasVideo && (hasEffect || hasSubEffect) || hasSubEffectPhoto // (!A)(B+C)+D
是否录视频,是否有特效、子特效,是否拍了子特效照片
,这几个参数就能决定其状态。let hasVideo // A: 是否已录制视频
let hasEffect: // B: 是否已选有特效
let hasSubEffect: // C: 是否带有子特效
let hasSubEffectPhoto // D: 是否已拍照填充了子特效 subEffect
其中AB两个变量作为列写在一起,CD两个作为行写在一起,变量的顺序不是一定的,你也可以换着写,但格雷码0和1的位置在表头是固定的,且最终计算结果也是一样的。
AB列中,00代表AB取值 A=false; B=false
,01代表取值 A=false; B=true
,依此类推。
于是每个格子就对应一个状态,比如第0行第2列,ABCD取值为0011,代表A=false; B=false; C =true; D=true
这个状态。所以4变量卡诺图可以把这些变量全排列的16个状态都表示出来,不会漏掉某个状态。
然后对于每个参数状态,我们思考目标控件的显示状态。这里为方便配合使用属性 .isHidden
,我们定义逻辑填入1为需要隐藏
,0为不隐藏
。注意这里与上面例子2变量里的显示隐藏定义是反相的。参数含义与格子对应,比如,在0011
状态时,代表状态含义是:
0011 = 无录视频,无特效,有子特效,子特效已拍照
本例有个特殊点,因为在需求逻辑上,参数B“无特效”比CD有更高优先级:即无特效时不可能有子特效,也不可能给子特效拍照,所以综合考虑,在状态0011时需要显示控件,即不隐藏,则在0011对应格子里填入0。
按照上述方法,把全部16个格子都填上对应(控件需要隐藏)的状态,AB为列,CD为行:
再把符号换回代码中的变量,即有:
// 特效浮层键:!A(B+C)+D
P = !hasVideo && (hasEffect || hasSubEffect) || hasSubEffectPhoto
五变量以下最多十六方格,也可用上述方法解出得到。但在实际开发中极少需要到5变量,掌握4变量方法已足够应对大多数场景。此处仅作拓展了解。六变量以上方格过多,用此方法反倒麻烦[3]。
图5.1 五变量卡诺图
它是由四变量最小项图构成的,将左边的一个四变量卡诺图按轴翻转 180 °而成。左边的一个四变量最小项图对应变量 E =0 ,轴左侧的一个对应 E =1 。
这样一来除了几何位置相邻的小方格满足邻接条件外,以轴对称的小方格也满足邻接条件,这一点需要注意。图中最小项编号按变量高低位的顺序为 EABCD 排列时,所对应的二进制码确定。
此时要注意列上变量排列的左右对称关系,对于既不含 E非也不含 E 的与项,可以填入 E非四变量卡诺图
中然后以中间轴翻转 180 °,在 E 四变量卡诺图中对称位置也填上“ 1 ”。
五变量逻辑式化简的举例说明如下,每个步骤原理在上文已作详尽说明,此处不再赘述:
我们简要介绍了卡诺图原理与其化简方法,这个工具和思想可以用在任何多参数有竞争或多种情况的场景下决定某个操作是否执行。这个方法在参数较多且状态复杂时使用,若逻辑状态较简单,则可直接用表达式写出,代码逻辑力求更清晰明了。
在上述场景的操作流程中,任何时候调用这个刷新方法,我们都可获得符合当下参数的页面状态,因为交互就只依赖参数做显示,而不需要考虑其前置状态。
卡诺图逻辑化简法优点有:简化了多参数多控件等复杂情况下逻辑与方法难以直观处理的情况,且在后续维护迭代中,也可独立地修改某个控件的刷新逻辑,不用担心影响其他控件或业务流程,每个交互的控件状态修改简易灵活,方便计算。
同时,卡诺图的使用也有其局限性,它比较适合4变量或以下的场景,在5变量及以上用此方法反而复杂,且在实际开发中,4变量场景已然是比较少见,则此法已足够我们处理大多数2~4变量的交互逻辑情况了。
*文/Xin
关注得物技术,每周一三五晚18:30更新技术干货