从学习编程语言以来,我们会花大量时间去研究编程语言中比较流行的框架,但我们很少花时间从软件设计的角度去切入学习。
大部份的项目中,一般都会采用经典的三层模式:->->。
之所以三层模式足够流行,是因为它足够简单,就像与生俱来天生就会的技能一样。
当你接触项目越来越多、越来越复杂,不再是每张表的CURD的时候,这时你应该会在想:
- 这种软件工程难道没有一种更科学的、更有设计味道、更有思想高度的一种架构模式吗?
- UI层收到请求通知,然后一直到逻辑层,再到数据访问层,是否还有其它的模式?
- 贫血模式带来的失忆症该怎么解决?
- 一张表对象(PO)到处有赋值行为,时间一久我都忘了当初为什么要对这个字段赋值?
- A表依赖B表的数据,这种逻辑我到底放A逻辑层还是B逻辑层?再加一层?规则层?外观层?
- 需求一来,想好实现方式,紧接着直接建表?
- 100多张表了,这项目越来越乱了,是我表设计的不好?还是根本就没设计?
- 为什么加一个需求,我改了几十处的代码,原来没问题的代码都被我改出BUG了。
- 每次重构之后,我都觉得自己特牛逼。过个1年,又来重构,美约1.0、2.0、3.0?
如果您跟我一样有这些共鸣,那么希望这篇文章能给你带个启发。
网上已经有非常多关于领域驱动的一些概念讲解了,本篇的目的在于用来让小伙伴们快速认识领域驱动设计。
不管怎么说,我先把这些名词列出来,方便后面我们共同深入探讨。
这一层,狭义的可以理解为是三层的数据访问层,这么定义是方便大家理解与三层的对比。 随着我们的项目的复杂度越来越高,除了依赖数据库,还会依赖缓存,http,rpc,mq,es等等。
在,我们大部份会在。
但是你发现了吗,依赖的技术中间件只是为了解决海量数据、高并发、性能,跟业务逻辑其实一点关系都没有。
哪天mongodb换成es、ch,难道我们还要在逻辑层也跟着改吗?(哪怕是一个字符也是改)
所以,涉及到、,、,都应该放到基础设施层。
并且有一个很大的,逻辑层()是。(依赖倒置,)
我们项目中的、以及(动作)会产生时,都在。
领域层是充血模型,即带有行为的实体对象。
领域层通常会包含:、(聚合根、实体对象、值对象)、、、
是用来定义一些的。由。
本篇不是领域驱动设计的概念讲解,这里只是告诉大家有哪些概念,如果想深入学习的,网上有很多这方面的讲解。
可以理解为,在传统三层中的UI层与逻辑层之间多了一层。
的用途是为了将领域层做、与(如数据库事务),没有实战过的小伙伴,这里有可能会比较难理解。
很多时候无法区分应用层与领域层的职责。总之,业务逻辑、对象的属性实际赋值,放在领域层。而比如一个请求要做3个逻辑处理。那么这个1、2、3先后顺序的控制,由应用层组织编排。
像数据库事务也是由应用层来控制,为什么这么做?因为间接或直接的。
思考时间:通过这句话,大家是否有进一步的思考,为什么领域层不能间接或直接依赖基础设施层呢?欢迎评论区讨论。
这一层就可以理解为在三层里的UI层了。通常像我们做的API涉及到的MVC,就是放到了这一层。
由于框架的支持技术,是一种的技术实现。因此在后面的demo中,你会看到我们的接口层是没有代码的。
在api路由配置中,直接指向了应用层,这也是可以的。这样可以让我们少一层的转发,代码也更加容易维护。
在这个小章节,我给大家简略的讲述了领域驱动设计的分层概念,但我怕误导大家,因为领域驱动设计是分为两个部份的:、。
事实上涉及到编码部份(包括上面提到的分层)都是战术部份。。
那什么是战略设计呢?如果比喻成一个武功,、。
因此也可以知道(战略设计)是有多么的。可以让我们在一个项目中、,做整体的一个设计。
我们经常听到的微服务,你是否有想过一个问题:这个服务该怎么划分?微服务决非是RPC + 小接口,如果是这么理解,就大错特错了。
在这里突然说到微服务,是因为在划分服务与服务之间的界限时,正好的。
因此领域驱动是每一个做互联网、写服务API的开发人员都要学习的,哪怕你不用,起码也要懂其中的原理。
感谢你能坚持看到这,如果对你有用,请给个不要钱的小爱心,让作者的爱继续发扬光大。
有了前面的铺垫,接下来,我们马上进入代码部份。
使用到的框架:farseer-go 如果可以的话,请大家给farseer-go 1个star
项目开源代码:github
框架官方文档:文档
这是模拟了一个小型电商平台,用到的技术:
- ddd:使用领域驱动设计
- ioc:使用ioc/container,做解耦、注入、依赖倒置
- webapi:api服务,并使用动态api技术
- data:数据库操作
- redis:redis操作
- eventBus:事件驱动
按商品分类,浏览商品
商品详情,下单
下订单后,通过事件驱动来通知商品减库存、保存订单数据
- ProductPO:商品,表名: (技术点:实现了数据库的插入、查找、分页技术)
- ProCatePO:商品分类,表名: (技术点:与Redis缓存自动同步,redis没数据时自动从数据库读取并缓存到redis)
- OrderPO:订单信息,表名: (技术点:利用事件驱动,解耦下单时,商品、订单的耦合)
- 库存:key: (保存商品的库存,这里不考虑库存并发等问题,只演示如何使用Redis模块)
- 商品分类:key: (数据库表与Redis同步)
文件位置:shopping/main.go
首先,我们通过函数引导farseer-go框架启动。
接着,我们注册了6个api接口
让我们的接口返回值时,自动包裹API的状态码相关字段。
让我们支持静态目录来支持前端静态页面(这里是前后端分离)
注意看路由部份,依赖了、、3个包。他们分别指:商品应用服务、产品分类应用服务、订单应用服务。
这些路由,是直接指向到应用层。因为webapi模块支持动态api模式,这样我们不再需要mvc模式的controller方式。
productApp.ToList、orderApp.ToList这两个路由指定了参数名称,这是因为当入参超过1个时,需要显示的申明第1个参数名称,第2个参数名称,以此类推。
接着我们看一下这个服务。这是获取商品详情的信息。
文件位置:shopping/application/productApp/app.go
有3个入参:、、。
通过路由注册,我们知道请求的URL是:GET http://localhost:8888/api/1.0/product/info?productId=1 ,1是商品的ID值
我们可以从URL中看出来,那么后面两个参数是怎么来的呢?
先看下productRepository的定义:
文件位置:shopping/domain/product/repository.go
从文件位置上,可以看的出来,这是放在domain(领域层)的,并且是属于product目录。
也就是说,这是放在产品领域的。而是仓储的意思,其类型是,从定义的方法可以看出来,是针对数据库的。
这与传统的三层模式不一样。在领域驱动设计里多出这么一个接口定义。
思考时间:为什么要这么定义?是不是多余的?欢迎评论区讨论。
既然是有接口定义,那么就一定有实现,接下来,我们先不继续探讨应用服务层,而是先把这个仓储接口的实现给搞明白。
文件位置:shopping/infrastructure/repository/model/productPO.go
这是大家都熟悉的配方:数据库表的实体类
文件位置:shopping/infrastructure/repository/productRepository.go
就是一个普通的数据库操作,其完全实现了接口。
大家还记得前面,第1个参数,我们知道是前端通过URL请求传过来的。那么第2个、第3个接口参数,是怎么赋值的呢?
图中可以看到,他们都是已经被赋值了。这其实就是ioc/container的功能了。
可以让我们少写很多代码,不需要手动到基础设施层的仓储实现来手动初始化了,是不是很方便。
这是怎么做到的呢?
首先,在基础设施层先将实个结构体注册到。
文件位置:shopping/infrastructure/module.go
在Module模块的PostInitialize方法中,执行了
接着,执行了,这是将实现类注册到。
这里的注册,意思是将当前初始化接口的方式交给ioc来管理其生命周期。
关于farseer-go的使用,可以看IOC、container注入
当webapi发现请求的路由handle发现参数是接口类型(上面提到的第二个参数、第三个参数)时,就会尝试到去解析是否有其实现。
如果找到了,则执行注入。现在我们就能理解,ToEntity的入参赋值是怎么回事了。
文件位置:shopping/application/productApp/app.go
我们继续看:,这是将一个类型转成另一个类型的工具,比如PO转DTO。(代码这里是聚合转成DTO)
这个方法,返回的是类型,并不是表实现类:
虽然他们长的很像,但还是有区别的。我们知道数据库的字段,通常是扁平化的,也就是说字段都是展开来的。除非我们用JSON来保存,但这样对where条件不友好,且失去语义。
先来看一下类型
文件位置:shopping/domain/product/domainObject.go
我们再对比一下数据库表实体:
文件位置:shopping/infrastructure/repository/model/productPO.go
发现没有,他们长的很像,区别在哪呢?是不是DomainObject(聚合)更加OOP?并且还多了个Stock字段。
小黑板:事实上,如果我们用领域驱动设计来设计项目代码时,一般是不会直接去建表的,而是先考虑这个聚合根长什么样。(当前会先划分限界上下文、领域事件等)
我们不要被数据库表的数据如何存储,而失去了项目中原本最重要的业务逻辑的思考。
如果改成这样的思考方式,我们是不是显的更加有设计感,而不再是表的CURD了?
由于这个项目,我演示的比较简单,也因此导致一个问题,所有的DomainObject没有行为产生。
如果想研究更复杂的,大家可以去看我的一个 开源项目。
如果可以,请给这个开源项目一个star。毕竟开源不易,作者都是用爱再发电,谢谢大家。
什么是领域事件?简单来说就是某一类的动作会产生不同的事件。
在这里用户下单就是一个领域事件,比如当用户下单时,需要做:
- 减库存
- 创建订单
- 通知仓库人员准备发货
文件位置:shopping/application/productApp/app.go
通常,我们先减库存,因为我们要防止超卖,无法在异步环节处理。
在实际项目中,一般会提前生成OrderId,我在这里没做。
在最后两行,我们通过ioc/container容器,取出的实现。
然后调用事件发布:
先来看下这个的注册:
文件位置:shopping/application/module.go
是注册事件的意思,事件名称:,同时有两个事件订阅者:、
- :创建订单(生成一条订单记录)
- :通知仓库管理人员,需要准备打包货物(当然这里只是一个模拟)
这里的事件从技术角度来说,就是有一个发起方,有多个订阅者。(发布订阅模式)
随着业务的扩展,我们会不断的增加新的订阅者。保证新的需求过来时,不用修改原来的逻辑代码。
文件位置:shopping/application/job/createOrderEvent.go
文件位置:shopping/application/job/noticeWarehouseOrderEvent.go
到了这里,代码部份已经分析完了,我们再做一个闭环,给大家一个完整的数据流向:
- 在main.go中注册路由
- api请求时根据路由,会执行应用服务层的服务。
- 应用服务会请求领域层的仓储接口、或聚合根、领域事件。
通过分析也可以看出:
- 接口层、应用层、领域层没有依赖基础设施层
- 基础设施层是在main的启动模块(StartupModule.go)依赖的。
- 当领域层不依赖基础设施层,那么使用哪种OLTP、OLAP、缓存、mq都不会导致逻辑改变。
- 一个请求需要执行多个小逻辑时,由应用层控制(编排)
好了,本篇到这里就结束了。
感谢你能坚持看到这,如果对你有用,请给个不要钱的小爱心
如果遇到有不明白的地方,欢迎留言提问。
使用到的框架:farseer-go 如果可以的话,请大家给farseer-go 1个star
DEMO项目开源代码:github
框架官方文档:文档
github。
如果这个DEMO您看完后,能完全掌握了,不防看下
到此这篇领域驱动设计demo(领域驱动设计的定义)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/hd-api/54513.html