看似简单但容易忽视的编程常识

好的代码首先是逻辑正确的

好的代码首先是逻辑正确的

如何用编程语言表述正确的代码逻辑,这个问题好像很少有人单独拎出来讲,因为这个问题的答案很简单,简单得你都懒得去思考它,因为你肯定觉得,用编程语言正确的表述代码逻辑无非就是if 、while 之类的东西,有什么好探讨的,其实我要分享的并不是这些关键词的本身在逻辑中表达的含义,而是这些关键词的背后,编写程序的过程中,是否真的认真思考过背后的逻辑。我曾不止遇到过很多有年编程经验的程序员,犯下类似的错误,也见过很多年轻的同学,反复强调纠正后,逻辑上还是会漏洞百出,这几年,我会经常组织我组里面的同学对代码进行走读,总结这些编码中的逻辑错误,很大一部分也是因为编程逻辑背后的思考是不够的。所以我要讲的,是很简单的知识,但是往往是最容易忽略的思考点。

我先给大家看一个例子:

if(userInfo != null){

//给用户派发一个100单位的优惠券

couponing(userInfo,100);

}

这段代码为的目的是判断userInfo不为空串的时候couponing,看起来这段代码非常简单,判断上似乎还算比较严谨,其实这段代码只是看到了眼前要做的事情,但是并没有看到整体逻辑,为什么这么说呢,请看下面几行代码,也许会引发最这个简单问题新的思考。

if(userInfo == null){

//思考尝试恢复不存在的userInfo情况

userInfo = fetchUserInfo(userId);

}

if(userInfo != null){

    couponing(userId);

}else{

   //思考userInfo不存在的情况下,是否是符合整体的业务逻辑,如果整体上不符合业务逻辑,应该立刻异常终端程序。

    throw new RuntimeException("userInfo not exist.");

}

这段代码虽说相比之前的代码长了一些,但是反映出来的是逻辑思考的严谨性,从这两个例子比较我们可以很明显的感觉到,第一段代码的问题,我们看到的只是为了保护是否能做couponing的条件,但是并没有去思考,条件不满足的时候,如何去做,是否有能力去恢复这个错误,确实无法恢复的时候,我们是否还要在错误的道路上越错越远呢,这一点非常重要,也很容易忽略,需要在编码的过程中,进行完整的思考才会意识到这个问题的,如果让错误继续执行下去,直到程序运行到下一个我们不期望的点,如果下一个不期望的点,代码上也遵循这个风格,简单的判断不为null,就跳过执行,这样下去,就会有无穷的隐患,代码整体上看上去,就漏洞百出了。所以从这里要给大家一个建议:

【要有一颗勇敢的心,程序不要害怕抛出错误,越害怕,错误越多】

·

我们应该都知道,错误越是早发现越好处理,其实程序在执行过程中也是一样的,越早发现错误,执行中就越容易处理。我一般称这种代码为代码的盲目容错,看上去这行代码很健壮,不会报错,但是不报错,不能影响错误的客观存在性,错会还是会存在的,遇到错误的时候,我们应该首先想到的是恢复这个错误,对容错问题,是需要进行非常深入很全局的思考才能做的决定,盲目的容错,只会让情况变得更加不可控制。

这一小节只是拿一个小例子来说明我们需要有更多的思考,介绍到这里,我相信大家都理解这些思考的重要性了,但是最关键的问题我还没有给大家说清楚,就是如何保证我们的思考是完整的。我给大家介绍一个我朴素有效的方法,这也是我在做CodeReview中最能发现问题的方法

【千万不要忘记else的思考】

每当你要用到一个条件表达式的时候,切记要思考这个条件不成立的情况。 尽可能的不要出现只有if 没有else的情况,多组条件用 else if 连接使用,最后再加一个else去做大兜底。 其他的条件表达式类似,比如switch case 最后总有一个值得我们深思default。严谨的代码其实就提现在else上面的思考。


容易造成思考不足的条件语句

条件有两面性,思考要完整

有效降低逻辑的复杂度

上一节的例子中,肯定会有人觉得这样写代码,是不是觉得太复杂了,已经思考了这些问题,一定要用这么复杂的方式表达出来吗?这是另外一方面的问题,我们要让代码逻辑变得简单,这一节中,我尝试分享一些我如何降低代码复杂度的方法和经验。

还是用上面的例子,我尝试将代码变得更加简单,请看下面的代码,是不是感觉舒服很多。

userInfo = withDefault(userInfo,fetchUserInfo(userId));

Assert.assertNotNull(userInfo,"userInfo not exist.");

couponing(userInfo,100);

这段代码中,表达了上面所有的逻辑,而且没有引入分支,其实这里我想强调的是

【减少分支就是降低复杂度】

我一般的编码思想是,尽可能的不要用分支处理异常,也不要因为异常引入分支,分支的使用场景最好是业务逻辑所需要的,应该用分支尽可能的表达清楚业务逻辑,而尽量不要用分支去适应异常的处理。这里进一步又引入了一个被忽略的尝试。

【不要混淆分支和异常的概念】

`

这一点看起来很难做到,但是根据我的实际经验,我们是有办法做到的,通过优雅的定义和处理异常,是可以比较容易的明确异常和业务分支的区别的。不过在本味中,我还是希望能将减少分之的方法说清楚,关于如何优雅的处理和定义异常,本文先不做过多描述。

我想说的是,一个分支,最好是能表达一层业务的含义,用分支标示是分支的条件以及条件成立或不成立的时候,要做的动作。所以,还是基于上面的例子,我们引入一个业务条件,“当用户是VIP用户的时候,我们才能给用户发放优惠券,否则,我们不发放优惠券”,我们分支代码标示如下

userInfo = withDefault(userInfo,fetchUserInfo(userId));

Assert.assertNotNull(userInfo,"userInfo not exist.");

if(userInfo.isVip()){

   couponing(userInfo,100);

} else {

   return;

}

这段代码正常的表述的业务的含义,注意其中的else,这里else 进入之后是直接return的,写上这一句就是上一节中,说明的一样,保证我们的代码逻辑是完整的,这一句有很明确的语义,就是表示条件不成立的时候,我们不做,如果不写的话,其实这部分语义是丢失的或是不明确的。

上面的代码能正常满足当前的业务需求,但是业务是复杂的,比如业务上我们有了新的需求,需要对发放优惠券的规则进行调整,调整会后的规则为,增加白名单可以不是VIP也要发优惠券,或者这个用户的用户UID是以00结尾,所以这时候,我们条件代码成了下面这个样子

if(userInfo.isVip()||inWhiteNameList(userInfo)||StringUtil.endWith(userInfo.getUserId(),"00"){

   couponing(userInfo,100);

} else {

   return;

}

这段代码中,我们逻辑一下就变得复杂了,虽说我们只用了一个if else 表达式,但是这里的分支复杂度其实是2的3次方,但是我们处理的情况就是两种,一种是成立,一种是不成立,所以,我们更加关心的是成立或是不成立的情况,而不是所有条件的组合形式,通过观察,我们发现,所有的逻辑都是由“或”进行连接,根据这个特性,其实我们可以提炼出逻辑工具方法,更好的表达我们更加关系的成立或不成立的条件。我们提取一个命名为any的逻辑方法来表述刚才的逻辑,这个方法接收一个不定长的参数,值要有一个为真,则返回为真。其他场景,我们也可以自己峰值其他的逻辑方法,比如all。notAll notAny。

则代码修改为

if(any(userInfo.isVip(),inWhiteNameList(userInfo),StringUtil.endWith(userInfo.getUserId(),"00")){

    couponing(userInfo,100);

} else {

    return;

}

这段代码有效的减少了代码的分支数量,注意,这里仅仅是从分支数量上进行了减少,增加了一点点可读性,这样做的好处是,多数情况下,我们关注的业务分支的动作本身,而对于进入这个分组形成的的组合情况做所有讨论,所以,这样做,可以有效的降低分支的数量,减少用例的个数(写过单元测试的同学都知道,这样的逻辑要覆盖有多痛苦)。

这一节中,用了一个看上去有些鸡肋的方法去封装逻辑组合,其实,在现在日常生产中,想办法去封装逻辑表达式进行封装是非常有效果的,这里只是举了一个逻辑封装的例子,还有很多其它场景,比如从一个Map中,根据一组key逐个取值,如果取到值不为null,则放入到另外一个Map中,这里其实可以写一个putNotNull的方法来封装逻辑,这种做法非常有效。所以这一节我想给大家传递的一个思想,就是尽你最大的可能,对逻辑表达式进行封装

【减少分支数量就是减少复杂度】

代码和业务解耦

上一节的例子中,大家可以很容易看出来,不管逻辑怎么封装,代码是始终不稳定的,其实这里就引出了我们要强调的一个常识,就是能力要和业务解耦。

如何将能力和业务解耦,我对这个问题的理解是,首先我得把这个能力定义出来,这里我暂且定义为这个能力为发优惠券(其实定义一个能力是最难做的事情,深的思考,会发现这个问题难到需要重新思考人生,我这里不拉开篇幅讲了,结合这个例子,大家暂且先有一个模糊的理解,后面在慢慢讨论能力定义这个大的课题),有了这个能力定义之后,我们根据这个能力定义做一个面向能力的条件判断,代码示例如下:

if(canCouponing(userInfo)){

    couponing(userInfo,100);

} else {

    return;

}

从这几行代码中,可以看出,这里好像已经好了很多,我们将发优惠券的能力和判断条件canCouponing进行耦合,看上去这段代码已经稳定了,但是仔细观察后发现,canCouponing这个方法中依赖了userInfo,这个依赖貌似还是会存在很多问题,因为如果判断条件超出了userInfo的范畴,则这个地方又会变得难以解决,能力判断的要素看起来还是不可控的,为了解决这个问题,我们就要用到运行上下文或是领域模型的概念了,用一个运行时的上下文,作为数据信息载体,承载我们业务执行过程中所需要的模型数据,领域模型的发放则是我们对系统能力和业务有了足够深入理解之后,抽象出来的,能更加准确表述业务属性和行为的模型定义,在没有很好的理解和抽象之前,本节中我们还是先用运行上下文这样相对松散的概念来解决这个问题。根据这个思想,我们将代码进行修改:

if(canCouponing(runtimeContext)){

    couponing(userInfo,100);

} else {

    return;

}

在上面代码中,让runtimeContext中包含userInfo,通过一个更松散的对象来传递对象,交给canCouponing这个方法处理,这里也许有人会问,canCouponing这个方法内部还不是一堆逻辑,整体上还是控制不住复杂度。其实这类问题,我们将关键的业务点从硬代码中剥离出来,并且将业务逻辑集中起来进行管理的话,就可以使用规则引擎来处理了。通过规则引擎和专家系统,将这些规则交给业务人员或是运营人员统一进行管理就可以了,而我们的功能性代码可以做到非常的干净和稳定。

也许有另外的人会问,为什么couponing(userInfo,100);这行代码中没有用runtimeContext,而是直接使用的userInfo,在实际编程中,你可能真的需要用到runtimeContext,但是这里的目的是让大家理解如何让业务代码和能力解耦,关于能力本身这块如何更好的设计,这一方面的内容也有很多值得我们思考的,本文暂不做过多探讨。

【思考能力的定义,用代码描述能力,将业务从代码中抽出来,交给规则引擎或是专家系统处理】

原文链接:代码湾-看似简单但容易忽视的编程常识

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 《小鸟有意戏珠帘》 寒冰 小区疏林石径斜 叶上雨珠压枝丫 晶莹剔透翠欲滴 胜似珍珠爱有加 小鸟...
    eceff7a5c042寒冰阅读 315评论 0 1
  • 2017年12月19日 星期二 晴 今天,孩子继续请假休息,还有感冒的症状,但是,看着轻快多了。她休...
    美妙宝贝阅读 106评论 0 0
  • 风轻轻吹,带有一丝丝的妩媚, 驱车行驶在回家的路上, 溅起一片片落叶纷飞。 劳碌了一夜的工作, 眼圈乌黑,身心疲惫...
    洁汝大弟阅读 207评论 11 8