前言
很久没有写心得,一来是懒了,二来是想写的东西难,乱。早就很想写一点有关业务逻辑和模式、算法的关系。可以找到很多理论,但却很少有理论实际相结合的文章。以至于许多人认为服务端代码就是CRUD,算法无用,设计模式无用。还有一种人他们认为设计模式已经融入自己日常的开发中,不需要特别去在意。这两种情况我都经历过,在写了这么多年代码后才意识到,自己思考的还是太浅。这篇文章就算是抛砖引玉吧。
抽象
抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。
在编码工作过程中,我们或多或少、有意无意的会进行一些抽象,其中最常见的,就是将事物转为变量。一个简单的变量就可以完成大量的抽象,对于一个书店,设计一个“book”变量,就可以代表所有的书。我们换一下它的名字改为“goods”,它就能代表任何可以销售的商品,而不仅仅是书。这一切看起来非常简单,太简单了,那么为什么业务越来越好,代码越写越苦?
本质
在写代码的时候,尤其是互联网常见的服务端业务代码,通常是将现实行为做数字化的过程。例如在超市结算的时候,交钱,拿货走人,在淘宝上要复现这个行为,就得将钱和商品数字化,可以被代码表达。那么,万一有人要插队怎么办?万一网银突然无法支付怎么办?万一商品中有赠品怎么判断,有活动商品怎么判断,临时有商品因为码扫不出(类似缺货)怎么办,全部都需要考虑,而这些现实中可能发生的问题,在线上也几乎都会发生,代码就要考虑。
无穷大的问题
那么理论上有多少问题需要考虑呢?这让我回想起第一节概率论的课上,老师问的一个问题:2个人约好10点见面,10点同时到达的概率是多少?因为到达的情况是无穷多,所以同时到达概率是0。这个问题也一样,现实中有无穷多种问题需要考虑,而这正是我们重构噩梦的开始。看过《银河系漫游指南》的同学应该知道有个叫“沉思”的超级电脑,它为了计算出宇宙的究极问题,设计了一台比他更强大的计算机:“地球”。事情就是这样,你要用程序表达这个世界,你需要的资源就是这个世界本身,除非你能从更高的维度空间来设计。
妥协
既然我们不可能表达完整的世界,我们就需要想办法去表达部分。
骨架
现代科学可以根据恐龙的骨骼化石来估计肌肉的走向和大小,从而还原它的外形。我们不能表达整个世界,但可以想办法表达它的骨架。选择性的表达是最常见的手段,它的重点在于:骨架要在它准确的位置上,不要因为肌肉的缺失而偏离。例如钱,无论在数字钱包的时代还是纸币的时代,它在生产生活中的价值是不变的。我们就认为我们找对了骨架:价值。
我们可以用 y = ax + b表达任意一条实数域内的直线,但是现实中没有哪条线是直的,但”直“这件事作为骨架是对的。
程序就是这个世界的“骨架”。
边界
很多问题在有限的边界内是是有限的,比如人际关系,在一代直系血亲中,只会出现父母和子女,但是扩展到整个人类社会,会出现父亲的叔叔,父亲的叔叔的爷爷,父亲的叔叔的爷爷的老婆的娘家。。。通过边界约束问题范围,正是很多DDD设计和微服务架构建设的目的。只不过微服务是一种“死缓”的做法,而DDD只是用于帮助思考,不直接解决问题。为什么说微服务是死缓?很多微服务在一开始是“微”服务,随着业务复杂,就变成“肿服务”。拆分?不好意思,业务等不起。
边界的合理性
当我们给问题画上了边界,怎么才知道边界是合理的?合理的边界应该是:
- 边界内元素可以被完整描述,也就是全集。
- 边界内元素是互斥的。
完整的描述有2种方法
枚举
比如:边界 = 性别,值 = [男,女,男改女,女改男,男改女又改男...],每次提到性别这个属性,总有那么几个坏小子要怼我,虽然不是恶意,但是也说明这个边界有点问题。
边界改 = 当前性别,值 = [男,女]。
看,通过“当前”2个字修饰的性别,就好多了。
使用公式
之前直线上的点已经说明了这种方法,而公式描述又可以通过分段函数、取样函数进一步将边界合理化,比如年龄,只有正整数。
互斥
被描述的内容在边界的约束下,元素是唯一的,例如年龄,1岁和2岁就是互斥。如果要描述一群人的年龄,往往会有同岁的,那么边界约束就要增加一个:某人。人在人类现实社会的边界内,是不会重复的,而一个人的当前年龄只有一个,所以一群人中某个人的年龄是唯一的。年龄 + 人 共同约束了一个边界范围,就像分段函数有多个约束条件。你可以看到,慢慢的我们发现在使用数学方法来描述问题,而这些数学方法的可以通过“算法”最终实现,这就是算法的作用。
妥协后的抽象
有点经验的研发很快就会发现,上面的例子反应在关系型数据库中,就是唯一索引和复合唯一索引,这没什么稀奇的。接下来我要举一个栗子来说它到底怎么毁了我们幸福的coding生活。有一个项目,要研究不同的食草动物吃什么植物。研发团队根据OOP思想,抽象了动物,动物行为,植物。又把植物和动物都抽象为“生物”以描述共性,完美,开始写代码:
动物->吃(植物);//return 要死 or 健康;
好了,一切都很正常,微生物要吃什么,食肉动物要怎么吃都没有问题。还有点扩展性,不错。
同时,在隔壁的一个部门,接了另一个需求,他们要研究植物,以及他们可以作为那些动物的食物,于是他们写了另一种代码:
植物->被(动物)->吃();//return 可以 or 不可以;
老板觉得两个部门做的有点重复,你们合并吧,反正都是吃,为啥要写两个?两边研发团队当晚打了一架,要求对方放弃自己的方案。最终动物研究部门因为体力比较好,植物研究部门放弃了自己的方案。第二天老板走进办公室说:我们今天研究一种植物叫猪笼草,它吃动物。。。动物研究部门一咬牙,行,我还有办法,凡是生物它都有吃,他们可以相互吃。。。
新的抽象
生物->吃(生物);//return 可以 or 不可以;
到这里,我们才看到了真正的骨架,研发团队发现之前的边界都是有缺陷的,动物、植物都只是生物圈的一部分,这还没囊括微生物。重构问题的发生,往往是我们没有找到骨架,没有摸清边界。在我们研发的过程当中,太多人喜欢写“动物->吃(植物);”这样的代码,很简单,也满足业务当前需求,如果抽象到“生物->吃(生物);//return 可以 or 不可以;”便可能被戴上“过度设计”的帽子。久而久之,大家就怕了。
沿着骨架重构或者扩展
如果你在纸上画了一条短一点的直线,如果需要一条长直线,延长便可,这很容易。如果一开始画的是弧线,要得到一条长的直线,你就得擦干净重画(重构),或者换个地方(重写)。以生物行为研究为例,如果一开始就把吃的行为赋予“生物”这个基础类,然后只实现动物的“吃”,而不关心植物的吃,比如猪笼草。这就降低了研发压力。后来出现猪笼草的需求,在植物类中覆盖掉吃方法,植物就有不同的吃法了,微生物也一样。被吃是一个道理,食物链顶端的动物也会被微生物吃掉,同样可以放在生物类中。这样我们的代码就是循序渐进的,可不断叠加。
设计模式
终于说到设计模式,几乎所有的设计模式,都在帮助我们解决猪笼草问题,只是方式不同罢了。但是他们不能解决边界划分和骨架寻找问题。这就是为什么很多人觉得,用了设计模式,扩展性也不是很好。那么说DDD,DDD其实是在提醒你,你要去寻找边界和骨架,它也不是告诉你如何去找。之前有个研究DDD的大牛来分享怎么划分边界,有一个简化版工序,大概有19道吧,反正我是记不得,总之不是一件简单的事情。
总结
“业务“分析来得到抽象。而”抽象“需要数学来描述,”数学”需要算法来编码,”编码“需要模式来避免重构。