问题
自古有个难题,到底代码写成什么样会是好的代码,这个问题千人千面,一万个人写一行代码可能写出一万种样子。
语义到底是啥,重不重要,代码里的语义怎么体现。
使用面向对象的语言,写的是面向对象的代码。这句话大家都会说,但是理解的人可能不多,经常写出来就是面向过程的代码还浑然不知(或者知道但是无所谓,能跑就行)。还有一个问题是代码的逻辑是没法消除的,只能转移,面向对象写的逻辑在对象里,面向过程的逻辑在哪里呢,这个界限不容易区分。
1.语义 & 面向对象
什么叫语义,简单来说:见文识义。可以很大,可以很小。大到一个大的项目,一个模块,小到一个类,一个方法甚至一个变量。大家通常对项目,模块能有比较好的语义,看到项目名就能知道是干啥的,但是到小了可能就并不清楚。
我们在看代码或者写代码的时候经常能看到一个方法里面就不用调用其他的代码,一个方法100行,觉得别扭但是也说不出来为什么,难以查看,难以复用,难以结构性测试。其实这就是面向过程的代码。
举个例子:我们在项目中,经常使用到redis。redis是什么呢?是一个中间件,不能代表具体的业务操作。在使用过程中我们会直接使用redisServer.addXXX,redisTemplate.putXXX。然后其他人来看的时候会需要先翻出这个key是什么,在哪里使用的,才能知道这个redis的key是起到什么作用。这暴露了的问题就是,redis只是一个中间件,不加场景的使用只会给人带来不便,又因为大家的水平参差不齐,业务复杂,很有可能使用出错。
我们可以在redisServcer的中间件层和业务层之间嵌套具有语义的一层。比如说新加一个单位缓存层,之后业务层的查询调用全部通过该层。该层暴露的方法包括了所有对于特定业务模块的操作,相较于以前的方式有非常大的不同,见文识义,即使新人来了也能看懂业务,也能知道如何修改。除了语义之外还可以对同一业务的操作进行收口,提高服用性、隔离变化、使代码可测试性变强。
对于以上的对中间件抽象业务的一层我称之为业务网关层,可以是缓存网关,消息网关,搜索网关,统计网关。除了语义化之后还可以隔离变化,单独测试。
话说回来,如何做到语义化/面向对象呢?
1.时序图分辨动作:我们可以想象一下一个方法的时序图,一个柱子到另一个柱子之间的连线,柱子自己到自己的连线等都代表了一个操作,这个操作就是需要单独拿出来成 方法/对象 的。
2.隔离说出来别扭的部分:你在当前类里面声明了一个方法,和你预设该类要干的事不相符。比如说:审批这个操作的审批通过/拒绝之后要发送消息,审批是一个方法,你在审批这个方法结束之后调用消息服务发出消息。“发消息”这个操作放在“审批”里就是不合理的,因为作用审批类我就只需要审批就好了,发消息不算是我需要做的。常用的做法是发出一个事件,有一个中间层接收该事件,然后再做操作。把“审批”和“发消息”隔离起来。
3.适时舍弃“大而全”:大而全的命名在小的项目里面是ok的,在大型项目里是不可取。在这里我想说的是没有必要教条主义,每个操作都要精细化,比如在一个小型项目里有一个任务类,里面聚合了查询,修改,审批,但是可能就200行代码,清晰明了,这个类叫TaskService无可厚非。但是随着项目越来越大,查询、审批都变得复杂起来,这个时候就可以将之分拆。TaskQueryService、TaskOperationService、TaskApproveService。不需要一开始就教条主义、未雨绸缪,但是随着项目演进这是必须重构的(只针对该点而言)。
4.抽象可以单独说出“作用”的代码块:这点大家可能有遇到过,但是也不知道如何总结,其实这就是面向过程和面向对象的区别。比如一个添加用户信息的操作,拿到了一个大对象,对象里面有个人信息,组织架构信息,地址信息。
常见的写法像:
UserInfo userInfo = new UserInfo();
userInfo.setxxx(allInfo.getxxx);
...
userInfoOperation.add(userInfo);
AddressInfo addressInfo = new AddressInfo();
addressInfo.setxxx(allInfo.getxxx);
...
addressInfoOperation.add(addressInfo);
我觉得这很面向过程,一段一段的堆代码,没有组织性,第一步怎样,第二步怎样,都堆在一个方法里。偏上层的方法应该是组织性代码,而不是具体细节代码,面向对象的方式:
UserInfo userInfo = this.userService.parseUserInfoFromAllInfo(allInfo);
this.userInfoOperation.add(userInfo);
AddressInfo addressInfo = this.addressService.parseAddressInfoFromAllInfo(allInfo);
this.userInfoOperation.add(userInfo);
大家在这里可能感觉不到好处,因为就这么简单的赋值而已,但是一旦用户信息/地址信息需要通过一些特出处理,比如调用第三方,比如请求到实体对象的字段适配这些复杂的操作就可以被抽象出来,隔离了变化。
再上层一点来说,这个方法其实是添加用户信息。用户信息包括多个子信息,方法要做的是对每个子信息模块的方法调用,而不是做子信息模块的逻辑。
大家也不要纠结上面伪代码的调用依赖,我只是为了体现这个面向对象的思想而已。
2.是否一定要DDD?
DDD全称领域驱动设计。大家可能都有听过,实体、值对象、服务、界限上下文、南向网关、北向网关、事件风暴、三色建模、充血模型、贫血模型,市面上也有一些DDD的框架,阿里云有提供DDD的代码脚手架。所以晦涩难懂,难以落地成了DDD的实现痛点。大家不知道这些概念都是为了解决什么而产生的。
真正接触到DDD还是在前公司的时候,那个时候我在直播小组,当时的代码就是逻辑堆成山,各种服务调用,状态机逻辑复杂,产品提出一个需求马上要开工,一个迭代完成是正常的速度。那个时候我们就想需要对代码进行重构,因为集团当时有几个框架,也有一些成功的case,所以在那个时候我们就开始学习、调研。因为一些原因我没能参与到项目重构,但是我对DDD也有一些见解,在这里分享给大家。
DDD最难的部分在哪里呢?其实是对业务领域的区分(呼应了领域驱动设计),充血模型是一种领域思想的体现。如果领域划分得当,充血模型是顺理成章的。
1).将所有的业务专家(产品,以前的项目开发者,测试)将他们关注的行为都列出来。
比如
主管(角色)在任务的审批阶段进行审批,通知对应的经理。
拆分上面这句话:
名词:主管,任务,审批阶段,经理
行为:审批,通知
这里我们可以将该操作涉及的领域划分出来。
1.角色域
2.任务域
2.1.任务状态
4.审批服务
5.消息服务
将所有专家的需求列出来,组合重合的 域/服务,这就可以构建我们的领域了。
2).不同领域的技术选型,中间件选型和交互方式(存储,队列,推送。。。)
3).写代码,逻辑集中于domain,服务层只做组织调用,这是代码层次的精髓。
下面我进行灵魂拷问,用了DDD又怎么样呢?其实给我最大的感觉就是,模块划分清晰,代码极度内聚,复用性极强。可能我替换了消息中间件,我只要修改一个类就完了。
按照我的理解这其实就是DDD就是让你写面向对象的代码。跟上一节没有本质的区别,它规范了领域的划分,规范了写代码的格式,分包方式和调用方式。是写面向对象的解决方案。
说到这里大家可能也就知道我想说什么了,没有必要说一定按照DDD的流程一步步来,我们需要学习的是它的思想,它是怎么做到领域拆分,如何集中逻辑,定义服务间交互方式,服务如何复用。
在现代大型互联网项目中很难有时间去完全按照DDD的形式实现功能,一般是在项目跑了一段时间后进行重构,而且这种重构难以量化价值,老板不认可。在每次迭代中利用DDD的领域划分思想,拆分新的领域,按照面向对象的形式写代码即可。纠结于不是纯正的领域驱动模型过程可能就有些形而上了。