找回密码
 会员注册
查看: 15|回复: 0

从DDD架构和设计模式读懂业务

[复制链接]

6

主题

0

回帖

19

积分

新手上路

积分
19
发表于 2024-10-6 18:40:12 | 显示全部楼层 |阅读模式
质量?从DDD架构和设计模式读懂业务毫无疑问地说,作为测试是一定要熟悉业务的。熟悉业务有很多种方式,比如产品会有prd——“产品需求文档”,也许还会有什么mrd——“市场需求文档”;不过看这类文档往往会有两个问题:一是,每个字都看得懂,连起来确不知道到底说了一件什么事情;二是,文档归文档,具体实现的逻辑通过这些文档往往难以全面地表现出来,从而仅仅通过文档,难以分析出测试中可能需要尤其关注的风险点。而别的方式诸如,询问该领域较为资深的研发或测试同学,则是一种完全依靠“人”这个不稳定因素的方式。有的东西想起来了、说清楚了,就能够了解得比较全面;如果有想不起来、说不清楚的地方,日后很可能就是个坑,上线的风险也就潜伏在这里。所以直接读研发同学写好的业务代码,其实也是一种对于了解业务的一种补充方式。不过实践中,读懂业务代码似乎并不是一件那么一帆风顺的事情,所以在此我总结了一些有可能阻碍读懂代码的小知识点进行分享。DDD架构<>如果对于DDD架构听起来比较生疏,那么起码对于MVC架构一定是有所了解的。大的方向上来说,两者还是有一些共同之处的,如实现系统的职能分工,从而达到低耦合的目的。DDD架构的一个核心目标便是,降低各个层之间的耦合度,从而降低对外部依赖的耦合度。作为测试,很容易忽视掉整个系统的架构设计,但其实架构设计的优秀与否,很大程度上决定了测试的难易程度和完善程度(当然难易程度也决定了完善程度,如果易于测试,则基本上可以说,测试会覆盖得更完善)。这里的测试,不仅仅包括了单元测试,也包括了类似于接口测试这样的集成测试。如果整个架构上就强依赖于数据库、中间件,或是其它一些第三方服务,那么想要通过一个测试用例的条件是,这些所有的依赖都没有问题。只要有一个有问题,用例就会失败,且如果日志等内容不够清晰,排查到问题发生在哪里也会花费相当的一段时间。甚至,耦合度高还可能导致用例的能否通过,依赖于某个服务处于某个特定的状态,而如果对应的服务不处于这个状态,则结果难以预期。如果要对这方面进行更进一步地测试,就需要更多的用例进行覆盖。进一步的,强依赖于外部服务会导致测试过程中实质上是IO密集的,而IO常常意味着花时间,这就会导致整个用例的执行时间会被拉长。1秒跑完一个用例,和10秒跑完一个用例,感受是完全不一样的,更不用说在一个“历久弥新”系统中,用例的数量往往会相当庞大,进一步提升了全面回归测试的成本。而回归测试的成本越大,其覆盖就可能越不全面,上线就越可能有潜在的风险点。怎么降低耦合度呢?核心的两个字是“抽象”。这里以Repository为例,进行持久化层的抽象。在传统的MVC框架中,DAO(Data Access Object)层常用来进行数据库相关操作的封装。在DAO层进行了相应的操作后,则在服务层中对DAO输出的实体进行一定的转换,再给到控制层。Repository则位于实现业务的服务层,和实现持久化的DAO之间。从理论上而言,领域驱动设计中,对象有许多种:VO(View Object,视图对象)、DO(Domain Object,领域对象)、PO(Persistent Object,持久化对象)、DTO(Data Transfer Object,数据传输对象)……实际上,为了正确使用DDD架构,也确实需要很多种对象,在数据持久化层和业务层使用不同的对象,来避免出现所谓的“贫血领域模型(Anemic Domain Model)”。要具体说明“贫血领域模型”,可能又不得不引入“数据模型(Data Model)”和“领域模型(Domain Model)”。数据模型其实就是数据是如何用ER关系表示,如何进行持久化的;而领域模型则是应用于业务层中,根据业务需要进行过了相应的转换。因此,原则上来说,数据模型应该位于持久化层,而领域模型则使用于业务服务层。贫血领域模型简单来说,就是混淆了数据模型和领域模型,在领域模型中也缺少了业务逻辑的介入。从概率上来说,将数据在不同的对象之间进行传递、转换的过程中,确实是有可能发生数据出现差错或是遗漏的,不过只要进行合理(并且往往不是太过于复杂)的测试,是能够发现的。这样大体上而言,在服务层,也就是xxxService中如果需要进行数据库相关的调用,则是通过一个xxxRepository来实现的。xxxRepository通过调用xxxDao来更进一步,在xxxDao中又可能通过xxxMapper来实现对于数据持久化的直接操作,而xxxMapper所操作的就是直接与数据库进行映射了的数据模型,这里姑且称呼它们为xxxTemp。而xxxRepository中吐出的则往往是稍微多了一些业务关系的xxxEntity,直至最终与其它服务或是前端直接交互的xxxDTO(或是xxxResponse,从另一个方向上来说,xxxRequest其实也是DTO)。再拆分到各个module中,上文中的xxxDTO/xxxRequest/xxxResponse则常常在xxx-api,xxx-interfaces这样的与外部直接交互的模块中,而xxxEntity则可能位于xxx-domain这样偏向于内部一些的模块中,另外xxx-application可能用于处理xxxEntity到xxxDTO的转换,位于xxx-domain(或是xxx-infrastructure)和xxx-api/xxx-interfaces之间。至于xxxTemp则往往是位于最底层、直接进行持久化操作的xxx-infrastructure中,由Repository进行转换给出xxxEntity。这仅仅是对于数据库操作的抽象,更进一步的对于如MQ、配置中心、其它服务等也会做出类似的操作。这样,再看到项目里各种花里胡哨的包名,希望能够稍微清晰一些思路。设计模式设计模式是个很大的课题,不同的研发同学在实现相似的逻辑中,可能会使用不同的设计模式(当然也可能不使用设计模式……),且对于相同的设计模式,具体的实现也常常有所区别。如果想从头到尾把策略模式说的清清楚楚其实是一件很大的工程,在这里仅仅是谈谈比较常见,且如果不了解的话可能阻碍读懂业务的一些。毕竟本文的目的是从需求出发,而需求就是能读懂业务代码。? 策略模式?<>既然叫做策略模式,不难想到,我们是对于某一类问题有若干个策略去执行。怎么去决定对于某个具体的场景去执行哪个策略呢?每个策略应该有其具体的对应规则,只有满足规则的才会得到执行。实践中,由于最终往往只执行一个策略,不同策略类之间的规则应当是互斥的,避免某个场景无法判断究竟会执行哪一个策略类。那么如何对于这么多的策略类进行统筹规划的调度呢?一个Context类可以被用到,来实现调用和具体实现之间的解耦。在网络上搜索策略模式相关的内容,得到的例子大概是这样的:// 1. 创建接口 Strategy.javapublic interface Strategy { ? ?public xxxResponse execute(xxxRequest request);}// 2. 创建接口的实现类// 2.1 StrategyA.javapublic class StrategyA implements Strategy { ? ?@Override ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// do something A ? ? ? ?return xxxResponse; ? ?}}// 2.2 StrategyB.javapublic class StrategyB implements Strategy { ? ?@Override ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// do something B ? ? ? ?return xxxResponse; ? ?}}// 3. Context类用以外部调用public class Context { ? ?private Strategy strategy; ? ?public Context(Strategy strategy) { ? ? ? ?this.strategy = strategy; ? ?} ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?return strategy.execute(request); ? ?}}// 4. 进行外部调用...Context context = new Context(new StrategyA());context.execute(request);...这样的例子个人感觉不是太好,因为具体使用的是哪个策略是由实例化Context类的时候就决定了,而如果想根据入参request中的内容去选择相应的策略,则需要进行一些修改。一个可行的方法是利用Spring的ApplicationContext接口的getBeansOfType方法,获取spring容器中Strategy.class类型的bean,返回的是Map的键值对Map,在Strategy接口中具体添加条件判断方法,而每个策略类中进行实现,这样子算是真正展现了策略模式的作用。一个改进版本的实现如下:// 1. 创建接口 Strategy.javapublic interface Strategy { ? ?public boolean condition(xxxRequest request); ? ?public xxxResponse execute(xxxRequest request);}// 2. 创建接口的实现类// 2.1 StrategyA.javapublic class StrategyA implements Strategy { ? ?@Override ? ?public boolean condition(xxxRequest request) { ? ? ? ?if(...A...) { ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} ? ? ? ?@Override ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// do something A ? ? ? ?return xxxResponse; ? ?}}// 2.2 StrategyB.javapublic class StrategyB implements Strategy { ? ?@Override ? ?public boolean condition(xxxRequest request) { ? ? ? ?if(...B...) { ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} ? ? ? ?@Override ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// do something B ? ? ? ?return xxxResponse; ? ?}}// 3. Context类用以外部调用public class Context { ? ?@Resource ? ?private ApplicationContext applicationContext; ? ? ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?Map map = applicationContext.getBeansOfType(Strategy.class); ? ? ? ?// 进行遍历什么的,得到需要的策略类 ? ? ? ?Strategy strategy = ...; ? ? ? ?return strategy.execute(request); ? ?}}// 4. 进行外部调用...Context context = new Context();context.execute(request);...个人理解,策略模式可以用于“消灭”一些if-else,从而增加代码的可读性且降低耦合。不过,there is no silver bullet,使用策略模式会带来的一个问题就是“策略类膨胀”,用人话说就是策略类太多了。不过策略类就算过多,起码也比一堆if-else放一块儿看着舒服点儿,并且经过上述的改进,策略类通过condition方法,相对于原版来说还是比较容易进行管理的。? 观察者模式?<>之所以会注意到“观察者模式”,是因为有一天在看代码的时候,看到个ApplicationEventPublisher,然后运用我的“command键+鼠标左键”大法,竟然找不到具体的实现。观察者模式可以理解成“广播”,即一个对象的状态改变,可以让其它有依赖关系的对象感知到,从而执行相应的操作。同样地,一个基于spring的实现如下:public class xxxServiceImpl { ? ? ? ?@Resource ? ?ApplicationEventPublisher applicationEventPublisher; ? ? ? ?public void doSomething(String msg) { ? ? ? ?// 使用applicationEventPublisher发送了一个广播。 ? ? ? ?// 这里广播的内容是xxxClass,所以只有入参为xxxClass的监听方才会响应 ? ? ? ?applicationEventPublisher.publishEvent(new xxxClass(msg)); ?}}public class xxxListener { ? ?// 使用@EventListener注解进行监听 ? ?@EventListener ? ?public void process(xxxClass event) { ? ? ? ?//do something... ? ?}}观察者模型一个可能出现问题的地方在于,如果单进程顺序执行每个监听者,如果某个监听者出现问题,整个系统都会收到影响。所以可以将监听者做成异步的来解决这个问题,比如使用@Async。目前我找观察者模式的实现的方式是,全局搜索@EventListener注解……这并不是一个高效的方法,不过在观察者模式没有在项目中过于广泛地应用的情况下,还是够用的。? 责任链模式?<>责任链模式以一种链式结构,将不同的对象操作串联在一起。当前节点进行操作(也可能不操作)后,交付下一节点。在责任链模式中,有两种具体的操作方式。一是,每个执行者在执行了对应的处理工作后,继续将向下传递。这样,将会有若干个执行者都有可能会对这个对象进行操作。另一种操作方式是,一个处理者在接收到请求后,会进行判断是否能够进行处理,如果能够处理则不再继续传递。在这种情况下,一个请求要么被一个处理者执行,要不就不被执行。使用jsp和servlet实现的web应用中的Filter类其实就是一个典型的责任链模式的实现。责任链模式的一个实现如下:// 1. 抽象类Handlerpublic abstract class AbstractHandler { ? ? ? ?// 责任链中下个节点 ? ?protected AbstractHandler nextAbstractHandler; ? ? ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?if(xxx) { ? ? ? ? ? ?doSomething(request); ? ? ? ? ? ?// 这里可以return,也可以不return ? ? ? ?} ? ? ? ?if(Objects.nonNull(nextAbstractHandler)) { ? ? ? ? ? ?nextAbstractHandler.execute(request); ? ? ? ?} ? ? ? ?... ? ? ? ?return xxxResponse; ? ?} ? ? ? ?abstract protected xxx doSomething(xxxRequest request);}// 2. 具体的Handler实现类public class AHandler extends AbstractHandler { ? ?@Override ? ?protected xxx doSomething(xxxRequest request) { ? ? ? ?// do something A ? ? ? ?return xxx; ? ?}}public class BHandler extends AbstractHandler { ? ?@Override ? ?protected xxx doSomething(xxxRequest request) { ? ? ? ?// do something B ? ? ? ?return xxx; ? ?}}// 3. 调用...AbstractHandler aHandler = new AHandler();AbstractHandler bHandler = new BHandler();aHandler.setNextAbstractHandler(bHandler);aHandler.execute(request);...类似的,经过ApplicationContext的改造,也可以不在具体的实现类中记录下一个责任类节点是什么,而在外层处理类中进行遍历,更进一步,经过根据一个level值的排序,即可确定责任链中的节点执行顺序。// 1. 创建接口 Handler.javapublic interface Handler { ? ?boolean condition(xxxRequest request); ? ?xxxResponse execute(xxxRequest request); ? ?int level();}// 2. 创建接口的实现类// 2.1 AHandler.javapublic class AHandler implements Handler { ? ?@Override ? ?public boolean condition(xxxRequest request) { ? ? ? ?if(...A...) { ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} ? ? ? ?@Override ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// do something A ? ? ? ?return xxxResponse; ? ?} ? ? ? ?@Override ? ?public int level() { ? ? ? ?return 0; ? ?}}// 2.2 BHandler.javapublic class BHandler implements Handler { ? ?@Override ? ?public boolean condition(xxxRequest request) { ? ? ? ?if(...B...) { ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} ? ? ? ?@Override ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// do something B ? ? ? ?return xxxResponse; ? ?} ? ? ? ?@Override ? ?public int level() { ? ? ? ?return 1; ? ?}}// 3. Context类用以外部调用public class Context { ? ?@Resource ? ?private ApplicationContext applicationContext; ? ? ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?Map map = applicationContext.getBeansOfType(Handler.class); ? ? ? ?// 进行遍历什么的,依次执行condition方法,得到一个Handler ? ? ? ?// 也可以根据每个的level进行排序,从而实现可控制的优先级 ? ? ? ?Handler handler = ...; ? ? ? ?return handler.execute(request); ? ? ? ?// 或者得到一个集合,比如,一个List ? ? ? ?List handlers = ...; ? ? ? ?return ...; ? ?}}// 4. 进行外部调用...Context context = new Context();context.execute(request);...? 责任树模式前两天刷公众号看到了[1],大致上来说是一个“策略模式”与“责任链模式”的融合。本着策略模式和责任链模式都谈了也不差这一个的态度,在这里也略微进行一些整理。需要融合策略模式和责任链模式去解决的场景是怎样的?简单来说,就是既能像策略模式那样进行不同的路由,也可以像责任链模式那样去实现一个多层次的处理。图2: 责任树模式在上图中,Root、Strategy 1、Strategy1.3除了担负了执行者的角色,还担任了路由的角色,即决定了下一步会被执行的具体策略。根据这样的模式,就可以在实现策略选择的同时,完成逐级委托的任务。以下为一个责任树模式的具体实现,这里为了和之前的(伪)代码风格一致,进行了一些修改。如果想要看原版,可以直接去[1]中的链接。// 1. 创建接口 Strategy.javapublic interface Strategy { ? ?/** ? ? * 下一个节点 ? ? */ ? ?Strategy strategy = null; ? ?public xxxResponse execute(xxxRequest request);}// 2. 创建接口的实现类// 2.1 StrategyA.javapublic class StrategyA implements Strategy { ? ?private Strategy strategy = ...; ? ?@Override ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// do something A ? ? ? ?return xxxResponse; ? ?}}// 2.2 StrategyB.javapublic class StrategyB implements Strategy { ? ?private Strategy strategy = ...; ? ?@Override ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// do something B ? ? ? ?return xxxResponse; ? ?}}// 3. Context进行策略分发public abstract class Context { ? ?private Strategy strategy; ? ?public Context(xxxRequest request) { ? ? ? ?// 根据request做一些判断 ? ? ? ?this.strategy = xxx; ? ?} ? ?public xxxResponse execute(xxxRequest request) { ? ? ? ?// 也可以将策略分发的逻辑放在这里 ? ? ? ?return strategy.execute(request); ? ?}}对于具备策略分发功能的节点而言,需要继承Context类,并根据需求实现分发逻辑;除根节点外,都需要实现Strategy接口。如果是叶子节点,由于不会再有策略分发的逻辑,自然也不需要再去继承Context类。其实所谓的“责任树模式”,本质上还是一种广义上的“责任链模式”。最后以上的内容仅是对于一个目的:读懂业务代码,所进行的一些小知识点的总结。所以对于比较铺垫的一些东西,比如为什么需要设计模式,到底什么是DDD架构之类的内容并没有作讨论。当然,内容上本文也可能有一些遗漏或是错误,如果你有所发现,能够在下方留言就再好不过了。关注我们得物技术携手走向技术的云端参考[1]《如何优化你的if-else?来试试“责任树模式”》 寻奕,阿里技术https://mp.weixin.qq.com/s/Wib0Ly45te00HMUnIG-tbg文|PokemonFish
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-11 08:56 , Processed in 0.679804 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表