团队开发框架实战—DDD之我见

Evans DDD

  • 2004年Eric Evans 发表《Domain-Driven Design –Tackling Complexity in the Heart of Software》 (领域驱动设计 )简称Evans DDD
  • 领域建模是一种艺术的技术,它是用来解决复杂软件快速应付变化的解决之道

领域驱动设计之父

领域驱动设计之父.png

领域模型相关领导人物

领域模型相关领导人物.png

分析设计发展的三个阶段

  • 第一阶段:围绕数据库的驱动设计,新项目总是从设计数据库及其字段开始。
  • 第二层次:面向对象的分析设计方法诞生后,有了专门的分析和设计阶段之分,分析阶段和设计阶段是断裂的
  • 第三阶段:融合了分析阶段和设计阶段的领域驱动设计(Evans: DDD)。

第一阶段:传统的数据库方式

过去软件系统分析设计总是从数据库开始,这种围绕数据库分析设计的缺点非常明显:

  • 分析方面:不能迅速有效全面分析需求。
  • 设计方面:导致过程化设计编程,丧失了面向对象设计的优点。
  • 运行方面:导致软件运行时负载集中在数据库端,系统性能难于扩展,闲置了中间件J2EE服务器处理性能。
    对象和关系数据库存在阻抗,本身是矛盾竞争的

第二阶段:分析和设计分裂

第二阶段比第一阶段进步很多,开始采取面向对象的方法来分析设计需求。
分析人员的职责:是负责从需求领域中收集基本概念。面向需求。
设计人员的职责:必须指明一组能北项目中适应编程工具构造的组件,这些组件必须能够在目标环境中有效执行,并能够正确解决应用程序出现的问题
两个阶段目标不一致,导致分裂,项目失败

新阶段:分析设计统一语言

统一领域模型,它同时满足分析原型和软件设计,如果一个模型实现时不实用,重新寻找新模型。
一个无处不在(ubiquitous)的语言,项目中所有人统一交流的语言。
减少沟通疑惑,减少传达走样。使得软件更加适合需求。

概念、价值、重点、范围、好处

概念:一种模型驱动设计(MDD) ,强调分析和设计不分离,一个领域模型体现分析与设计的结果
过程:业务需求->领域建模,领域模型->编码实现
价值:模型实现业务需求,反应业务核心价值
重点:领域建模,深入分析业务需求是关键
范围:长期维护、有价值、业务复杂的系统
好处

  • 有效防止最终代码实现的走样
  • 模型封装状态与行为,无外部依赖,单元测试保证正确性
  • 业务逻辑实现代码:集中、无重复、健壮、维护性好
  • 快速应对需求变化
  • 模型可以被重用,模型积累
  • 模型直接表达业务需求,基于模型沟通更方便

领域建模前要理解的点

  • 侧重点:对行为结果的建模,将对象理解为某个事实的结果,从事实观的角度去理解对象;不同于行为驱动开发(BDD),BDD基于:Data、Context、Interaction三个要素,从对象扮演不同角色参与交互活动的角度进行分析和建模
  • 模型的目的:是为了实现用户需求,而不是实现用户与系统的交互过程,所以模型中不包含系统使用者,即用户;需要区分:行为驱动者、行为参与者、用户的关系
  • 思考问题出发点:不是思考用户有哪些职责行为,也不是思考用户如何与系统交互,而是思考系统需要实现哪些需求,有哪些业务场景,每个业务场景会涉及哪些对象,这些对象如何协作完成业务场景

领域建模分析思路

  • 先努力尽量全面深入理解需求
  • 找出业务场景,画出用例图
  • 从结构以及行为两个角度进行分析,找出所有你能想到的对象,列出这些对象
  • 分析哪些对象需要建模,哪些不需要
  • 分析有哪些聚合,确定聚合边界
  • 分析聚合内对象的关系、聚合之间的关系
  • 分析历史模型
  • 分析聚合之间如何协作,发现领域服务
  • 分析模型可重用性

领域建模步骤参考

  • 先从需求中考虑一些业务场景,和领域专家交谈场景的过程,从中识别出一些明显的领域概念,以及它们的关联,关联可以暂时没有方向但需要有(1:1,1:N,M:N)这些关系;可以用文字精确的没有歧义的描述出每个领域概念的涵义以及包含的主要信息;
  • 根据上面分析得到的领域概念建立一个初步的领域模型
  • 分析主要的软件应用程序功能,识别出主要的应用层的类;这样有助于及早发现哪些是应用层的职责,哪些是领域层的职责;
  • 进一步分析领域模型,识别出哪些是实体,哪些是值对象,哪些是领域服务
  • 分析关联,通过对业务的更深入分析以及各种软件设计原则及性能方面的权衡,明确关联的方向或者去掉一些不需要的关联;
    找出聚合边界及聚合根,这是一件很有难度的事情;因为你在分析的过程中往往会碰到很多模棱两可的难以清晰判断的选择问题,所以,需要我们平时一些分析经验的积累才能找出正确的聚合根;
  • 聚合根分配仓储,为每个聚合分配一个仓储,此时只要设计好仓储的接口即可;
  • 走查场景,确定我们设计的领域模型能够有效地解决业务需求;
  • 考虑如何创建领域实体或值对象,是通过工厂还是直接通过构造函数;
  • 停下来重构模型。寻找模型中觉得有些疑问或者是蹩脚的地方,比如思考一些对象应该通过关联导航得到还是应该从仓储获取?聚合设计的是否正确?考虑模型的性能怎样,等等;

分层架构

领域驱动设计标准分层架构.png
职责
User Interface 负责向用户展现信息以及解释用户命令。
Application 很薄的一层,定义软件要完成的所有任务。对外为展现层提供各种应用功能(包括查询或命令),对内调用领域层(领域对象或领域服务)完成各种业务逻辑,应用层不包含业务逻辑,但包含流程控制逻辑。
Domain 负责表达业务概念,业务状态信息以及业务规则,领域模型处于这一层,是业务软件的核心。
Infrastructure 为其他层提供通用的技术能力,通过架构和框架来支持其他层的各种技术需求。如提供持久化领域对象的支持。

Important Tips

  • 在领域驱动设计中要先设计领域模型,接着写Domain逻辑,至于数据库,仅仅是用来存储数据的工具。使用database first那不叫领域驱动设计,很明显你先设计的表结构,所以应该叫数据库驱动设计更为准确。更不要引入数据库独有的技术,例如触发器,存储过程等。数据库除了存储数据外,其余一切逻辑都是Domain逻辑。
  • 在领域驱动设计中,当领域层的代码完成后,领域专家查看的时候,不会看领域层,而是直接看单元测试中的代码,因为领域专家不懂代码,并且他也不懂你是如何实现的,它关心的是我该如何使用它?我想要的业务操作,你有没有完全实现?单元测试就是最好的体现。
  • DDD 倾向于“测试先行,逐步改进”的设计思路。测试代码本身便是通用语言在程序中的表达,在开发人员的帮助下,领域专家可以阅读测试代码来检验领域对象是否满足业务需求。
  • 领域事件一般没有返回值的设计,它只是去通知事件订阅者执行,并不一定需要事件订阅者返回结果给它,那我们如果判断是否执行正确呢?就是通过异常判断,如果领域事件发生异常,后面的操作也将不会正常执行。
  • 在解决方案中,我们可以看到只有领域层、基础设施层和领域层单元测试的项目,并没有应用层和表现层的实现,但到目前为止,我们似乎把整个系统都完成了一样,这种感觉是很美妙的,领域模型在我手心中,任你是 Web 实现或者 WebApi 实现,又或者是其他技术框架,我都不怕,一切都是自然而然的工作,所以,关于后面的实现,你也可以交给其他人去完成,地基由我奠基,盖楼你来完成
  • Unit Of Work的职责只有一个,就是负责搜集所有的更改,并提供一次性提交更改的能力。工作单元接口(IUnitOfWork)只有一个方法:Commit;UnitOfWork的定义:维护对象状态,统一提交更改。第一句指的RegisterNew、RegisterDirty、RegisterClean、RegisterDeleted等,第二句指的是Commit,现在IUnitOfWork只有一个Commit。对应用层服务,只需要暴露Commit方法即可,Rollback其实可以去掉,因为Commit里内部完全可以自动回滚的。而那4个register方法,不是给应用层服务看到的,而是给仓储基类使用的UnitOfWork 的定义
  • 经典DDD中是在应用层服务中调用领域服务,CQRS架构则是在command handler中调用领域服务。
  • DDD+TDD,我个人非常推崇这种开发方式。
  • IRepository与IDAL的本质区别,应该是领域需要什么,定义什么,否则搞出个CURD的东西来,还不如直接ORM。
  • 如果用领域对象来解决查询问题,肯定很别扭的。CQRS才是让领域层摆脱查询负担的终极方法。效果很好,模型很纯净,查询很轻松自由高性能;查询我直接用dapper,灵活高性能,想怎么查就怎么查,想返回什么字段就返回什么字段;模型用DDD+Event Sourcing
  • 领域服务的存在,一定是由于光用聚合本身封装不了当前业务场景的完整业务逻辑的时候,才需要引入。比如一次修改多个聚合根,或者注册用户是要判断用户名唯一性,等;而像你这个,假如注册用户就是简单的new user,那我觉得没必要设计领域服务,只要在应用层userRepository.Add(new User())即可;而userRepository.Add(user), orderRepository.Add(order)这种应该在应用层出现,目的是为了持久化领域聚合根;
  • Repository 不进行持久化,应用层会调用Unit Of Work去做在领域层中用到的Repository应该都是读的方法,而所有写的方法比如说Add, Update 都应该被提到应用层去操作,对么? 这样做可以让领域层更纯洁,只处理领域实体和相关的业务,持久化就给应用层来做。
  • EF 中的Context 是Unit Of Work 的概念,而IDbSet 就是Repository的概念。
  • 架构分层思想也是一种分离关注点的思想,每一层只关注自己的职责,层与层之间通过接口耦合,我觉得没什么不妥;OO的本质是强调消息发送,但是消息不一定是一定要发送到消息队列,然后对方接收才叫消息发送和接收;a直接调用b的方法也是一种消息传递的表达,只不过此时消息发送者和接收者是强耦合的;
  • 因为要注册一个账户,所以要在Domain中添加一个UserService。这种做法有点让我费解,那岂不是增加一个产品类还要建一个CategoryService,增加一个产品要加一个ProductService这种注册其实是添加用户的行为,领域模型专注于业务模型,注册其实是新产生一个用户。我觉得UserService Register操作直接返回一个用户实体会更合理一些(不持久化)。
  • 在领域层那里不使用repository的更新类操作(即Insert/Update/Delete),只使用查询类操作即(GetById,或者是Get)。把所有的更新类操作都放到应用层,这样由应用层去决定什么时候把实体更新到repository,以及什么时候去提交到数据库中。
  • IRepository正如它的名字一样,它就像一个容器,允许我们把东西放进去或者取出来,它离真正的数据库还有一步之遥,并且通过Unit Of Work,把对事务以及持久化的控制都交到了外面。而不是像DAL那样直接就反映到数据库中去了。IRepository解除了领域层对基础设施层的依懒,这个也是大家经常提到了Repository的优点之一。但是未必这一点一定非得需要IRepository,把IDAL接口移个位置同样也可以实现,不信您看看洋葱架构。
  • 领域驱动不会使用任何ORM,因为数据库只是输出输出存储设备,和领域资产没有所谓的对应关系,领域资产凌驾于所有应用程序之上,他不会因为什么数据结构啊,表单啊,表格的不同而影响其通用性。
  • 真正的DDD其实应该连数据如何持久化和用什么ORM都不应该关心,他完全是对一个领域的需求抽象出通用模型,对通用业务逻辑的抽象实现。对持久化技术之类的,只提供通用接口。你对一个领域了解的越多,能抽象的模型和服务越多。所以关健在于领域知识,和DDD的基本知识。DDD设计出来的东西必须应用到具体业务中才能有真正的实现。

更多资料和资源

参考框架

  • Apworks
    Apworks is a flexible, scalable, configurable and efficient .NET based application development framework that helps software developers to easily build enterprise applications by applying either Classic Layering or Command-Query Responsibility Segregation (CQRS) architectural patterns.
  • enode
    ENode is a framework aims to help us developing ddd, cqrs, eda, and event sourcing style applications.
  • 基于asp.net mvc + DDD 构架的开源.net cms系统.
    一个基于DDD的开源项目,各种技术!

参考书籍

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容