DDD与数据事务脚本
DDD与数据事务脚本
扯淡
相信点进来看这篇文章的同学,大部分是因为标题里面的“DDD”所吸引!DDD并不是一个新技术,如果你百度一下它的历史就会知道,实际上它诞生于2004年, 到现在已经18年,完全是个“古董”,软件开发技术日新月异,DDD确显得很独特,一直不温不火,也未淘汰。有些人为了使用DDD“苦思冥想”、有些人对它保持敬畏,觉得是一种高端的技术,当然也有人觉得这玩意垃圾根本没用。废话不多说. 下面我尝试使用一个最基本的业务场景来讨论下ddd和事务脚本。
假如我们的现在需要做这么一个系统,名字叫做“消息发送系统”。 系统里面存在很多用户,而我们需要做的,就是给指定的用户发送消息
- 用户能在收件箱中看到收到的消息,消息有已读,未读状态,消息内容不能为空。
- 通过发送用户id、消息内容,就可以发送到用户。注意这里并没有发件人的概念。
数据事务脚本
需求很简单,现在我们开始设计这个功能。相信你只需要学习过一些基本的编程技术,你第一直觉一定会整理出下面这个数据结构
message{
id , //消息id
userId ,//接受用户id
content ,//消息内容
state ,//消息状态已读,未读
...
}
当设计出这个数据结构的时候,我们心里面其实认为这个系统的“核心”就完成了, 这TM是真简单啊! 为什么会是这样?我们都知道,计算机最核心的组件是cpu,而cpu就是用来计算处理用户输入的数据,并将结果反馈给用户。而软件运行在计算机上,所能提供的功能实际上也是这样处理数据。当然了,计算机还有磁盘,磁盘可以存储用户输入cpu的数据,也能存储cpu处理完后的数据!按这个逻辑,一个软件(系统)实际上就是在将特定业务场景下的数据输入计算机,计算机处理完后存储这些数据。软件只需要准确的记录下计算过程中产生的所有数据,并正确的存储算据即可实现对应的功能。
软件的不同功能,只是以合适的方式创建、修改、展示数据。上面的message结构,准确的找出了我们“消息发送系统”里面产生的数据。 发送消息,只不过就是新增了这样一个消息数据, 而用户看消息,只是找到所有message中userid为自己的id的消息,显示出来就可以了。这个“消息发送系统”非常简单,其实大的系统也是一样,大系统由很多这样小的系统组成,本质确是一样的。
我们已经找出这个系统会产生的数据结构,当然还需要保存起来,假设将它存入数据库里面(如果你愿意,你也可以存到文件里,或者干脆只放到内存里),那么我们在数据库里面就新建一张message的表,字段就是id,userId,content,state。假设现在有个页面,您可以在这个页面中输入id,userid,content,state,点击提交按钮,软件取到这些值,就写入数据库。伪代码大概是这样:
var id = input('id');
var userId = input('userId');
var content = input('content');
if(userId == ""){
err "userid不能为空"
}
if(content == ''){
err "内容不能为空";
}
//发送消息
state = input('state');
database.exeSql("insert into message values(id,userid,content,state)");
//查看消息
messages = database.exeSql("select * from message where userid=userid");
这个时候“大神”告诉你,这个太low了,逻辑和操作数据库全部写一起了, 一点都不面向对象,一个类都没有,你应该封装一个message类。我们现在有orm框架,能直接将对象存到数据库,这个时候你经过不断的学习,你写出了下面的代码:
id = input('id');
userId = input('userId');
content = input('content');
if(userId == ""){
err "userid不能为空"
}
if(content == ''){
err "内容不能为空";
}
state = input('state');
//发送消息
message = new message(id,userid,content,state);
orm.save(message);
//查看消息
messages= orm.getMessageByUserId(userid);
简单到爆,一切都这么自然,很完美这样你的消息系统就完成了。 当然了, “大神”还会告诉你,或者你自己意识到下面这些“硬核知识点”
- 将上面message这种跟数据库表对应的对象,称之为PO(Persistent Object)
- 将存储message到数据库,或者从数据库中通过相应的条件查找、更新、删除、message对象的功能,封装到一起,叫做dao对象
- 前端页面里不同的功能点,最好别直接调用DAO操作存储数据,因为还有一些业务逻辑代码需要编写,为了封装性,需要将不同的功能点封装到不同的类里面,我们叫做sevice类(可能很多同学直接一个数据表对应一个po,对应一个dao,对应一个service),比方说messageServcie
class messageServcie{
message[] getMessageBuUserId();//通过用户id,获取它收到的消息
readMessageById(messageId);//将message设置为已读
}
写了这么多,相信大家也看出来了,这就是所谓《数据事务脚本》开发方式的来源。 这种开发方式很直接也很简单。如果你问我有什么问题? 我觉得没什么问题,挺好的。我们以数据为中心,软件只是在处理数据,存储数据而已。
接下来我们说DDD
由于我们所使用的编程语言java、C#等,都是面向对象编程的,也很认可面向对象编程的好处!而数据事务脚本开发方式,核心是数据,虽然里面也有po对象,但是po对象却没有任何行为,这让很多人觉得有点尴尬! 而ddd确说我们的领域对象是有相应的行为的,这也是很多人喜欢ddd的理由。
我们知道对象包括属性和它的行为。而软件,从面向对象的角度,就是在业务范围内设计不同的对象,然后通过对象与对象之间的行为调用,改变对象的状态,从而实现不同的功能,想想我们的现实世界,确实是由一个一个的物体组成的。 汽车是不同的零部件组成,人由不同的器官组成,这也是为什么说面向对象更加容易理解,更加流行的原因。 下面还是以消息发送系统为例。
通过阅读上面“消息发送系统的”需求, 应该很容易发现消息, 用户这些名词。 暂且不管对不对, 先将这些写成类
class message{
id , //消息id
userId ,//接受用户id
content ,//消息内容
state//消息状态已读,未读
message(id,userId,content){
if(userId == ""){
err "这消息不行"
}
if(content == ""){
err "这消息不行"
}
state = "未读";
this.id=id;
....
}
///修改消息状态
setState(state){
if(this.state == "已读" && state=="未读"){
err "已读的消息,不能设置成未读"
}
this.state = state;
}
}
class user{
id,
userid,
messages[],//收件箱中的消息
addMessage(message){
message.userid= this.userid;
this.messages.add(message);
},
//将消息设置为已读
setMessageisReaded(message){
message.setState("已读");
}
}
class userService{
//发送消息
sendMessage(id,userid,content,state){
var message = new message(id,userid,content,state);
var messageBox = findUserMessageBox(userid);//找到收件箱对象
messageBox.add(message);
}
//获取用户收件箱中的消息
getMessageByUserId(userId){
var user = findUserByUserId(userid);
return user.messages;
}
//将消息设置为已读
setMessageisReaded(userId,message){
var user = findUserByUserId(userid);
user.setMessageisReaded(message);
}
}
大家看了上面的代码觉得怎么样? 是不是感觉也很自然, 对象有自己的属性和对象。 这个时候相信有些看到这里的同学, 已经有疑问了“你这是搞着玩吧,数据都不存到数据库的?”。上面说过从面向对象编程的角度,软件就是对象与对象的交互,交互完后对象状态会改变, 比如下说上面的发送消息代码,我们可以理解为:新创建了一个message对象, 通过userid找到用户,然后将message放到用户收到的消息列表中。假如我们的内存是无限的,而且不会丢失,我们还需要存数据吗? 现实当然内存不无限,也会丢失,那么是不是把这个message对象和user对象通过某种方式保存到磁盘,需要的时候取出来就可以了? 存储方式很多比如你直接json序列化写到文件,写到MongoDB,或者存到关系型数据库。 但是这里与数据事务脚本概念已经不一样,数据事务脚本存的是软件运行产生的结构数据, 而这里存的是对象,这一点一定要理解。 所谓的repository就是用来存取对象,dao却是用来存结构化数据,概念有很大的不同!!!
如果你看过一些ddd的文章,你一定知道ddd里面有很多名词:统一语言、事件风暴、限界上下文、领域、子域、支撑域、聚合、聚合根、实体、值对象。 初学者一看,尼玛吓死人这么多的东西! 然后就是一脸懵逼。 实际上面这些词是可以分类的,有些是告诉你怎么和客户沟通,了解需求,有些是告诉你怎么划分不同子系统,有些是告诉你怎么找到设计对象,有些实际上只是给有特征的对象起个名, 有些是给对象与对象的关系起个名。
对于一般开发着来说,找到对象、设计对象才是我们最关心的,因为对象是开发者写代码的基础!至于跟客户沟通、了解需求、划分不同的子系统、对象关系命名这种东西暂且放放! ddd之所以不普及,不是因为需求没沟通好,子系统没划分好,而是因为不知道怎么设计对象,就不知道应该怎么写代码。 对象是一个抽象的东西。有些系统里的对象,是有实物对应的,比如说购物车,我们就很清楚知道有什么属性和行为。而有些没实物对应,就不知道怎么找到对象,也不知道对象应该具有什么行为。一个简单的功能,每个开发者都有不同的理解与抽象,出现不同的设计方案,而且大家都认为自己的合理的。 园子里有很多高手写的ddd文章,如果只是解释介绍上面的名词,一般讨论不起来,只有点赞、叫好的份。因为上面这些名词解释可以说是死的, 大家能看懂就行, 但是如果出现一些ddd设计案例, 就会出现一些不同的设计方案来讨论。而最终当然也是不了了之! 上面我写的消息发送系统代码,初学者一看觉得写的挺好,但是ddd高手一定有不认同的地方!
对象的行为如何划分
对于ddd 对象设计来说,最纠结的是行为应该放到哪个对象? 举个烂大街的例子“图书管理系统”中,读者借书的逻辑,应该设计到哪个对象。一种常见的设计是这样:
///读者
class reader{
id,
///名字,地址,余额,借书记录
其他属性...
///借书
borrow(book){
//判断能不能借这本书的一系列逻辑...
book.setState('已借');
}
}
//书(这里没有定义书数量,假设书都是一本一本)
class Book{
id,
title,
state,//已借出,未借出
setState(state){
if(this.state == "已借" && state == "已借"){
err "不能再借了";
}
}
}
class readerService{
borrow(readerId,bookId){
book = getBookById(bookId);
reader = getReaderById(readerId);
reader.borrow(book);
}
}
从代码上能看出借书逻辑写到了reader(读者)对象上,“读者借书”所以借书应该就写到读者上,很符合现实和直觉。但是这里我们需要再仔细思考下。 现实世界中对于读者去图书馆借书,实际上是这样的,读者向图书馆申请借书,图书馆查看图书是否可以借给这个读者(可能这本书已经借出,也有可能这个用户不能借书...),如果能,就将书借给这个读者,然后记录一下,借书业务就完成了。如果你觉得这个描述是对的,那么借书的逻辑还应该放到reader这个对象上吗?显然不是,借书的主要逻辑应该图书馆,或者是图书管理员(取决于业务复杂程度),读者只是一个驱动者!上代码。
///读者
class reader{
id
///名字,地址,余额,借书记录
其他属性...
}
//书
class Book{
id,
title,
state,//已借出,未借出
setState(state){
if(this.state == "已借" && state == "已借"){
err "不能再借了";
}
}
}
class BookLibrary{
borrow(readerId,bookId){
reader = getReaderById(readerId);//找到这个读者
book = getBookById(bookId);//找到这本数
//检查这个读者能不能借这本书...
book.setState("已借");//将书设置为已借
}
}
看上去差不都,实际上也差不多!呵呵... 但是借书这个逻辑不再放到了读者对象上,而是放到了BookLibrary上面。
再举些例子:
- “银行账号转账”, 转账逻辑显然也不是账号(account)上面的,账号的行为的主要是这2个:充值(钱变多)和消费(钱变少)。 至于转账应该是“银行”的功能!!!
- “论坛用户发帖”, 发帖逻辑也不应该是用户对象上,而应该是论坛本身!
- “用户购物”,购物逻辑也不应该是用户对象上的,这是购物软件本身通过操作账号钱包对象,物品,等等一系列对象达到购物这个目的。
...
这种例子很多,其实这里最主要是。 不能将业务逻辑简单的放到驱动者身上,而是要深入分析一个功能点具体是怎么实现的,是有哪些对象一起交互才完成的功能。 如果全部放到驱动者身上,最终会导致好像所有的功能都应该写到"用户"这个对象上,因为是用户在使用驱动软件。用户通过操作软件界面上的功能点,驱动软件运行。 其实往往出现在软件界面上面的功能点,都应该写到这个软件本身对象(BookLibrary)这个对象上。软件本身再操作不同的领域对象,实现功能!!! 这也就是ddd中所谓application services的来源,将不同类型的功能通过services概念在封装到一起,形成不同的***Service,取名叫做领域服务对象。那有人可能又会说,这样下去是不是所有的业务都到了service里面,其实不是,其他对象有自己的业务逻辑,service需要操作不同的领域对象,实现不同的业务, 比方说:银行转账中,账户充值消费就是账号这个行为的,这里面就应该对账号是不是已经被冻结,余额是否足够消费,进行业务处理。
其实还有一种简单方法来设计对象,面向对象编程一开始就告诉了我们:对象=属性+行为,通过行为修改属性来到达改变对象状态实现不同的功能。也就是说行为跟对象属性是有关联性的,这也是面向对象中所谓的"内聚"。
- 如果一个行为跟这个对象上的属性没有任何关联,这个行为放到这个对象上就是不合适的,
- 如果一个行为需要好几个对象,而把功能放到这好几个对象中其中一个对象上也是不合理的。可以通过service的方式实现,还有一种方式就是将这个行为独立抽象出一个对象(这取决与业务需要),而这个对象拥有其他需要的对象,比分说银行转账
class transfer{
//执行转账
execute(fromAccout,toAccount,amount){
if(this.check(fromAccout,toAccount)){
fromAccout.sub(amount);
fromAccout.add(amount);
}
err '不能转';
}
//检查是否能转
check(fromAccout,toAccount){
//一系列的检查
}
}
对于借书这个场景来说,一个借书流程需要,读者对象,书对象,以及一些借书规则,这些信息,这里面任何一个对象都不具备所有信息,比如说,读者对象并不知道读书对象现在是什么状态,也不知道借书有哪些规则。 所以将借书逻辑放到读者对象上是不合适的。
相信对于找出对象的属性一般没问题,所以可以通过先找出对象的属性,然后再通过属性找出它的行为!!!
restfull资源与对象的不匹配
ddd之所以这段时间变得火,主要是因为微服务。 大家都说使用ddd能给微服务带来多少好处(可能最大的好处是能分出不同的子系统(微服务)), 我这里说一个它的坏处。
按照ddd的思路,最终出来的是一个一个的对象。 而rest出来的api是一个一个的资源。ddd每个对象是有不同行为的,而rest中却规定,对于资源的操作应该是统一的(post,delete,get,put)。 对象中不同的行为,应该怎么在rest中表达? 从这个角度来说,ddd与restfull天然需要做一次适配,而ddd与远程对象调用更加合拍(ejb,wcf等)
而如果是传统的数据事务脚本,数据很容易对应到rest中的资源,对于数据的操作无非就是(增,删,查,改),也容易对应到(post,delele,get,put)。
总结
写了这么多,从数据事务脚本的由来,到ddd,再写到怎么设计对象。总的来说就下面几点!
- 数据库事务脚本:service-->dao这种方式也挺好,并不是不行!!甚至跟rest配合起来更加“直接”。这也是为什么现在大部分还是数据事务脚本方式的原因(代码中有repository,有domian可不一定是ddd哦)
- 在ddd对象设计中不能将业务逻辑简单放到驱动者身上,应该仔细思考,到底发生了什么,不能为了充血而充血! 可以通过先找对象属性,然后再找关联的业务行为。 但是前面也说了,每个人可能抽象出不同的对象,如果对象符合行为和属性内聚这个特性,建议一定要有自信别乱想(这也是很多人纠结的地方)...
- ddd与restfull并不是完美匹配的。
当然这只是抛砖引玉,实际上ddd是一套完整的软件开发流程,并不只是设计对象而已, 在实际开发中也还有很多的问题需要思考,比如存储对象,应该放到service中吗? 发送邮件,发送短信这些功能点,写到领域对象,还是service里面,类似的问题很多!! 这里不讨论,这需要大家的开发经验实践,或者去查看一下大神的最佳实践。 就这样了,希望对大家有帮助,谢谢阅读!
DDD与数据事务脚本的更多相关文章
- .NET应用架构设计—表模块模式与事务脚本模式的代码编写
阅读目录: 1.背景介绍 2.简单介绍表模块模式.事务脚本模式 3.正确的编写表模块模式.事务脚本模式的代码 4.总结 1.背景介绍 要想正确的设计系统架构就必须能正确的搞懂每个架构模式的用意,而不是 ...
- 你在用什么思想编码:事务脚本 OR 面向对象?
最近在公司内部做技术交流的时候,说起技能提升的问题,调研大家想要培训什么,结果大出我意料,很多人想要培训:面向对象编码.于是我抛出一个问题:你觉得我们现在的代码是面向对象的吗?有人回答:是,有人回答否 ...
- 框架计划随笔 三.EntityFramework在传统事务脚本模式下的使用
某个朋友问为什么不推首页或者允许评论,我说一直没怎么写博客,也习惯了先随便乱画再开始写文档,担心公开后一些不经意的"呓语“中得出的错误的结论会给别人错误的观点,所以这个系列只是当做熟悉写博客 ...
- 如何解决分布式系统数据事务一致性问题(HBase加Solr)
如何解决分布式系统数据事务一致性问题 (HBase加Solr) 摘要:对于所有的分布式系统,我想事务一致性问题是极其非常重要的问题,因为它直接影响到系统的可用性.本文以下所述所要解决的问题是:对于入H ...
- SQL Server 2008如何导出带数据的脚本文件
第一步,选中需要导出脚本的数据库,右键选中 第二步,选取弹出菜单中的任务-生成脚本选项(会弹出一SQL生成脚本的向导) 第三步,在向导中点击下一步,弹出选择数据库界面(默认是自己之前选中的数据库),把 ...
- 处理文本,提取数据的脚本-主要就是用sed
处理文本,提取数据的脚本 #! /bin/sh | sed 's/)<\/small><\/td><td>/\n/g' # 用换行符替换 # 删除带有分号的行 # ...
- Jmeter(七)Jmeter脚本优化(数据与脚本分离)
午休时间再来记一记,嗯..回顾着使用Jmeter的历程,想着日常都会用到的一些功能.一些组件:敲定了本篇的主题----------是的.脚本优化. 说起脚本优化,为什么要优化?又怎么优化?是个永恒的话 ...
- SQL查找数据库中所有没有主键的数据表脚本
--SQL查找数据库中所有没有主键的数据表脚本 --运行脚本后在消息中可能会显示下面现象中的一种:--(1)数据库中所有数据表都有主键(则证明所有数据表都有主键)--(2)当前数据表[数据表名]没有主 ...
- 【性能测试】:LR插入mysql数据库数据,脚本参数化问题
一,今天准备脚本做mysql数据库的铺地数据,脚本内容不赘述,在批量执行insert语句时候,出现一个问题: // sprintf(chQuery, "insert into table ( ...
随机推荐
- LCT小记
不用说了,直接上怎么 die( 千万不要和 Treap 一样写左旋 zig 和右旋 zag,莫名死亡.Splay 只支持一个 rotate 上旋一个节点即可. splay() 之前记得弄一个栈存储 u ...
- 《剑指offer》面试题39. 数组中出现次数超过一半的数字
问题描述 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字. 你可以假设数组是非空的,并且给定的数组总是存在多数元素. 示例 1: 输入: [1, 2, 3, 2, 2, 2, 5, 4, ...
- 多线程的libcurl的使用
摘要:libcurl在多线程中,采用https访问,经常运行一段时间,会出现crash. libcurl的在多线程中的使用特别注意的有两点: 1. curl的句柄不能多线程共享. 2. ssl访问时, ...
- Web安全攻防(一)XSS注入和CSRF
跨站脚本攻击(XSS) XSS(Cross Site Scripting),为不和层叠样式表CSS混淆,故将跨站脚本攻击缩写为XSS. 攻击原理: 恶意攻击者往Web页面里插入恶意Script代码,当 ...
- 神坑!为什么prometheus的pushgateway不能对上报的counter进行累加?
部署了一个prometheus的pushgateway,然后两次对其发送counter类型的数据: #第一次发送 curl -X POST -d '# TYPE my_first_metric_ahf ...
- 事务与一致性:刚性or柔性
转发自 https://cloud.tencent.com/developer/article/1038871 在高并发场景下,分布式储存和处理已经是常用手段.但分布式的结构势必会带来"不一 ...
- 小程序或者vue,解决菜单导航做做成轮播的样子
案例: 其中最重要的思路就是如何让第二次或第三次以及后面的轮播有数据: 做法大致跟轮播图做法一样,只不过我们需要进行书写样式,代码如下: <!-- 做一个轮播图navbar demo --> ...
- Spring Boot Starter 和 ABP Module
Spring Boot 和 ABP 都是模块化的系统,分别是Java 和.NET 可以对比的框架.模块系统是就像乐高玩具一样,一块一块零散积木堆积起一个精彩的世界.每种积木的形状各不相同,功能各不相同 ...
- vector自实现(一)
vector.h: #ifndef __Vector__H__ #define __Vector__H__ typedef int Rank; #define DEFAULT_CAPACITY 3 t ...
- gin中的多模板和模板继承的用法
1. 简单用法 package main import ( "github.com/gin-contrib/multitemplate" "github.com/gin- ...