摘要:// 模板1 publicResponsecreateOrder(Requestreq){checkParam(req);Orderorder=convertTo(req);saveData(order);returnbuildResponse(order);
最近看了一些项目代码,清一色的就3个模板: (代码示例省去开启事务的部分)
// 模板1 publicResponsecreateOrder(Requestreq){checkParam(req);Orderorder=convertTo(req);saveData(order);returnbuildResponse(order);}// 模板2 publicResponseupdateOrder(Requestreq){checkParam(req);Orderorder=findOrder(req.getOrderId);Assert.notNull(order,"cannot find order");updateOrder(order,req);saveData(order);returnbuildResponse(order);}// 模板3 publicResponsequeryOrder(Requestreq){checkParam(req);Orderorder=findOrder(req.getOrderId);returnbuildResponse(order);}细心的你一定看出来了,这不就是增改查吗?(为啥没有delete 因为现在大厂对数据管得严,基本上不允许进行delete操作)
出现这样的代码也是一种必然性,因为这种模式容易复制,对程序员的要求低,不管是谁,只要看懂了需求,直接就能上手干。
我之前还在菊厂打工的时候,还有人把这样的代码封装成模板,很贴心有没有,可以省掉一次copy!!(菊厂躺枪,但其他人也别笑,90%的人都写过这样的代码)
写这样的代码,工作是完成了,但长此以往,编程能力是没长进的,凭心而论,这样的代码写出来有成就感吗?或许刚入行的同学会觉得这种代码看起来很“干净”,事实上这样的代码完全没有结构可言,长期一定是难以维护。
怎样让代码有“结构性”,看看这一篇《为什么说用例设计在软件开发中很重要》,或许对你有些帮助
我认为程序员应该是最富有创造力的一类人,千万别把自己变成一个只会ctrl+c、ctrl+v的机器,工作8年、10年还只会CRUD,还谈什么提升?今天教大家三招,只需在代码中融入一些架构思维,瞬间让你的代码提升一个档次。
1. 领域内聚上面提供的范例都称为“面条式代码”,为什么这种面条式代码会难以维护?
试图用技术思维来解决复杂的业务问题。软件是为了解决业务问题而存在的,一个好的软件程序,需要对现实世界进行建模,让模型尽可能去贴近业务,而不是为了把数据写进数据库里。类似checkParam、saveData这种纯技术的思维对业务建模没有一点帮助这种代码看起来干净,实际上里面非常“脏”,因为主要的领域逻辑不内聚checkParam可能带有业务逻辑(校验业务合法性、校验余额、校验状态等)、convertTo也有业务逻辑(核心模型order的构造就是业务逻辑)、甚至有些saveData里也有逻辑(save的时候不放心前面的检查结果,有时还要再检查一遍),业务规则是零散的。
另外,如果业务上有多种不同的createOrder呢?例如自营渠道下单、合作渠道下单、自营还分成线上/线下模式,这些看起来很类似又有些许不同的逻辑就会先copy一份,再微调一下,导致规则难以维护。或者干脆不copy,直接在原来的代码上写if/else,屎山代码不就是这么来的?
举一个订单状态校验的例子,原来的状态流转:订单创建->待支付->已支付->已发货->已签收,后面业务规则变了,要支持先用后付,这要改多少处代码?至少要把各种不同的createOrder入口都检查一遍,checkParam要改,convertTo要改,saveData可能也要改。
一个好的做法是,给订单建立一个领域模型,而且是充血模型,业务规则都放在模型内部:
// 不提供setter方法,保证逻辑内聚 @Getter@BuilderpublicclassOrder{privateOrderIdorderId;// 订单状态,通过状态机组件来管理 privateOrderStatusstatus;privateListitems;privateTimestampcreateTime;privateUserIdbuyerId;privatePayOrderIdpayOrderId;//... // 订单支付 publicvoidpay(TimestamppayTime,longpayAmount,PayOrderIdpayOrderId){// 1.校验订单状态 // 2.校验支付金额是否正确 // 3.修改payOrderId // 4.修改订单状态 }}通过这样封装以后,如果订单状态的逻辑发生变更,就只需修改Order即可。
还可以进行一些其他调整:
checkParam分成两种,如果只是校验协议的,例如字段长度、是否必填、枚举值合法性,这些都交给框架来做;如果是业务规则校验,封装到领域模型里上面的范例代码属于AppService层,这一层通常没有逻辑,只是把DomainService像胶水一样粘在一起,可以根据不同的模块封装一些领域服务,例如:@ServicepublicclassOrderAppService{// 库存领域服务 InventoryServiceinventoryService;// 用户领域服务 UserServiceuserService;// 订单领域服务 OrderServiceorderService;publicOrderAppService(...){// 依赖注入,省略...} // xx渠道下单 // AppService层需要开启事务 @TransactionalpublicResponsecreateOrderByXxxChannel(@ValidCreateOrderRequestreq){// 参数检查由框架层做了,业务代码里就不需要重复做 // 构建订单的逻辑,包装在factory里 // 创建订单的同时进行订单规则校验,生成订单号,判断是否幂等 Orderorder=OrderFactory.newOrder(...);// 校验用户的状态 // 具体要不要引入缓存,由用户模块来决定 booluserAvailable=userService.available(order.getBuyerId);Assert.true(userAvailable,"用户状态异常");// 锁定库存 // 具体是通过数据库实现还是缓存实现,由库存模块来决定,这里不需要关心 inventoryService.lock(order.getItemType,order.getQuantity);// 订单领域服务,负责处理订单 // 包括订单信息入库,入库前的必要检查,发送订单创建的领域事件等 // 入库前的必要检查也不是直接在service里面写,可能是调用order.readyToPay来更新订单状态,核心的逻辑是在order里。往下翻有代码示例 orderService.createOrder(order);returnbuildResponse(order);}}为了让大家更容易看懂,我添加了很多注释,但实际上把中文注释都去掉,是不是基本上也能看懂?这里去掉了所有技术的术语,而是替换为有业务语义的函数名称。
或许会有人质疑,你这样改完以后不还是变成了另一种代码模板吗?非要这么说的话也可以,这是一种无法copy的模板,因为每个业务的逻辑都不相同,你的思维方式已经转变为不再是为了把数据存进数据库,而是把业务逻辑搞明白,使其更加内聚,这是一个非常大的转变。其实,做好“高内聚、低耦合”不就是走向架构师的第一步吗?
2. 隔离变化如果你要问我作为一名架构师最需要的思维方式是什么? 我会告诉你:识别,并隔离变化。
把容易变的和不变的隔离开把业务规则和技术实现隔离开把业务主流程中的强依赖和弱依赖隔离开短短三句话,其实很考验架构师的基本功,很多代码的性能、可维护性、可扩展性有问题,追到根上就是隔离没做好。而隔离变化常见的方式有:
划分子模块或子领域通过AOP(Aspect Oriented Programming)实现关注点隔离,例如为了做全链路追踪,需要在每个请求进来之前打印一下traceId,使用AOP就可以避免这种硬编码。AOP实际上并不是一定要用很重的AOP框架,很多go语言开发者跟我聊说go里面没有AOP这样的东西所以做不到,这种说法是不对的,后面我想专门用一篇文章介绍一下这种理念,它实际上与语言无关巧用IoC(Inversion of Control 控制反转),可能在Java中这个词出现的概率比较高,go里面不太常见,但其实IoC也是与语言无关的。好些同学对IoC的理解很片面,后续我也会专门针对这个写一篇介绍。使用领域事件举几个例子:
上面第一节的领域内聚实际上也是在做隔离,把库存、用户管理模块从订单隔离出去,一开始上线时并发量不高,我们或许会直接通过数据库来锁库存,随着业务量越来越大,就会考虑做一些库存的缓存等,这样就只需改动库存模块,而订单模块完全不用改动(这里用的是划分子领域)AOP的例子我想后面单独用一篇文章来说典型的IoC用于隔离变化的例子就是DDD的Repository(仓储模式),数据访问层(DAL)有很多与技术相关的逻辑,例如分库分表、数据路由、关键数据加密、热点表缓存等,如果按传统思路让service层依赖DAL,就不得不让业务和技术代码互相耦合,通过引入repository模式进行IoC可以很好地解决这个问题对于主流程和非主流程,使用领域事件来隔离是非常合适的。例如下单成功以后,需要给用户发送一个通知,这个通知并不属于主流程,即便失败也没有关系,可以使用事件来隔离。// 领域事件的例子 // 订单领域服务 @ServicepublicclassOrderService{OrderRepositoryorderRepository;EventPublishereventPublisher;publicOrderService(...){// 依赖注入,省略...} // 领域服务层的入参通常是Entity或ValueObject publicvoidcreateOrder(Orderorder){// 调用实体的readyToPay,做一些事前校验,变更订单状态等 order.readyToPay;// app service已经开了事务,这里直接调用repository的save // 数据存储的细节由repositoryImpl负责,领域层不需要关心 orderRepository.save(order);// 发布领域事件,这个事件由订阅器消费,至于后面是发通知还是其他,就不关心了 // 事件并不一定都是异步的,更多是为了解耦和隔离 // 具体是同步还是异步,在事件组件里去配置,领域服务中不需要关心 Evente=Event.createEvent(EventCode.ORDER_CREATED,order);eventPublisher.publish(e);}// 订单支付,也同样以Entity作为入参 publicvoidpay(Orderorder,PayOrderpayOrder){// ... }}到这里我介绍了一些方法,但也别忘了我们最终的目的:隔离变化。好些同学学会了这些招式以后进入另一个极端,逮住一个地方就开始做拆分,加各种AOP,导致代码反而变得越来越复杂。切记时刻思考隔离的本质(上面说的三句话),才能让架构思维得到进一步提升。
3. 抽象思维抽象能力也是衡量一个架构师水平的尺子。
之前听过一个段子:把大象放进冰箱需要分几步?答案是三步:1. 打开冰箱,2. 把大象放进去,3. 关上冰箱。 这从一定程度上说也是一种抽象,但这种抽象就显得很生硬,没法落地。
抽象分为对过程的抽象和对结构的抽象。
前者多数人是熟悉的,上面提到的CRUD模板还有把大象装进冰箱,都是对过程的抽象,但需要注意抽象不能脱离了业务流程,否则就会像CRUD模板那样生搬硬套,不解决业务实际问题。
这里所说的“业务流程”是泛指,如果你恰好在做一个跑批框架,可以认为你现在面对的“批处理”这件事就是业务流程,虽然它看起来是个技术的东西。这里就提供一个对批处理进行抽象的例子:
// 抽象的批处理任务 @Slf4jpublicabstractclassAbstractTask{privateStringtaskId;privateTaskTypetype;privateTaskStatusstatus;privateStringparam;privateintretryTimes;publicAbstractTask(...){// ...} // 任务的执行入口,支持传入参数 publicvoidExecuteTask(Stringparam){// 前置条件判断 boolcanRun=preCheck;if(!canRun){return;}log.info("task started")try{doExecute;status=TaskStatus.SUCCESS;}catch(BusinessExceptione){ErrorCodec=e.getErrorCode;// 根据错误码判断是否可以重试,更新定时任务状态 if(c==ErrorCode.XXX){status=TaskStatus.WAITING_TO_RETRY;retryTimes++;}else{status=TaskStatus.FAILED;log.error("task run failed!",e);}}catch(Throwablee){status=TaskStatus.FAILED;log.error("task run failed!",e);}// 后置处理,不影响任务执行结果,例如发送通知等可以放这里 try{afterSuccess;}catch(...){// ... }}protectedabstractvoidpreCheck;protectedabstractvoiddoExecute;protectedabstractvoidafterSuccess;}经过这样抽象之后,对“批处理”这个业务流程就可以进行统一,而不需要所有的批处理任务都实现一遍。具体的任务可以继承AbstractTask,实现3个抽象方法即可。
另一类对结构的抽象,举一个例子说吧,这个例子可能不一定恰当。例如现在有多种不同的订单类型:实物订单、虚拟物品订单,这两种订单的库存判断方式不一样,发货方式也不一样,虚拟物品不涉及物流。之前锁定库存的接口是:InventoryService.lock(ItemType type, int quantity),因为增加了订单类型,接口就要改为InventoryService.lock(ItemType type, int quantity, OrderType t),将来有没有可能再增加别的判断要素?这样库存的接口要改,订单模块作为调用方也要改。这时候可以考虑对订单做一个抽象:publicinterfaceOrder{ItemTypegetItemType;intgetQuantity;OrderTypegetOrderType;}// 抽象之后库存接口就可以改为 publicclassInventoryService{publicvoidlock(Orderorder){OrderTypeorderType=order.getOrderType;if(orderType==OrderType.VIRTUAL){// 虚拟订单 }}}这样订单模块就不需要再感知库存模块的变化,订单实体加了一个getOrderType方法也并没有破坏订单这个实体的内聚性。(不过这个例子确实有些不太好,我再考虑一下给一个更好的例子)
4. 总结架构能力非一朝一夕之功,需要刻意练习,不要抱怨说“我都没有做大项目的机会,没机会锻炼架构能力”。其实我们的日常工作就是锻炼架构能力最好的机会,努力写好每一行代码,自然就能成为优秀的架构师。
来源:天哥教育