当前位置: 代码迷 >> 驱动开发 >> DDD 领域驱动设计-看小弟我怎么应对业务需求变化,愚蠢的应对
  详细解决方案

DDD 领域驱动设计-看小弟我怎么应对业务需求变化,愚蠢的应对

热度:413   发布时间:2016-04-28 10:04:03.0
DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?

写在前面

阅读目录:

领域驱动设计的核心-Domain Model(领域模型),这个大家都知道,可是,上次关于领域模型的设计分享,要追溯到两个月之前了,这中间搞了一些有的没有的东西,比如纠结于仓储等,说这些东西不重要,其实也蛮重要的,因为它是一个完整应用程序所必须要考虑的东西(Demo 除外),但是相对于领域模型,在领域驱动设计中它才是最重要的。

这篇博文我分享的思路是:一个具体的业务场景,一个现实项目的业务需求变化,应用领域驱动设计,看我是如何应对的???

注意:上面我用的是问号,所以,必不可少的会有一些“坑”,大家在读的过程中,要“小心”哦。

具体业务场景

具体业务场景?没错,就是我们熟悉的博客园站内短消息,详见:[网站公告]8月17日14:00-15:00(周日下午)发布新版站内短消息

上面那次版本发布,已经过去一个多月的时间了,说是“新版”,其实就是重写之前短消息的代码,然后用领域驱动设计的思想去实现,界面换了个“位置”,功能和原来的没有太大变化。发布之后,出现了很多的问题,比如前端界面、数据库优化、代码不规范等等。有些技术问题可以很快的解决,比如数据库的索引优化等,但是,有些问题,比如 SELECT FileName,因为程序代码是基于领域驱动设计的思想去实现的,那你就不能直接去写select filename1,filename2,filename2... from tablename这样的 SQL 代码,所以实现起来需要思考很多,这个是比较头疼的。

我为什么会说这些问题?因为这些问题,只有在实际应用项目中才会出现,你搞一个领域驱动设计的简单 Demo,会出现数据库性能问题吗?肯定不会,那也就不会去思考仓储的一些问题,更谈不上一些改变了,所以领域驱动设计的推进,只有你去实际用它,而不只是做一些演示的东西,在实际应用中,去发现问题并解决问题,我觉得这样才会更有价值。

关于短消息这个业务场景,其实我之前写的一些领域驱动设计博文,都是围绕着它展开的,很多园友认为这个业务场景是我虚构的,就像之前 netfocus 兄问我:“你说的这个短消息是不是类似于博客园的短消息?”,我回答:“是的!”,呵呵。后来我发现虚构的业务场景,有时候很难说明问题,比如之前 Jesse Liu 在一篇博文中,提到一个用户注册问题,关于这个问题,其实讨论了很久,但最后结果呢?我认为是没有结果,因为业务场景是虚构的,所以就会造成“公说公有理,婆说婆有理”的情况,以至于大家很难达成一些共识的点。

博客园短消息的业务场景,真实的不能再真实了,毕竟大家都在实际用,我就不多说了,就是简单的一个用户和另一个用户发消息,然后进行回复什么的,在之前的一些博文中也有说明,大家可以参考下:我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践

业务需求变化

现在的博客园短消息,为了方便用户看到之前回复的一些内容,我们在底部增加了“=== 下面是回复信息 === ”,示意图:

这种方式其实就是把之前回复内容放到新消息内容里面,然后作为一个新消息进行发送,除去消息内容“冗余”不说,还有个弊端就是,如果两个人回复的次数很多,你会发现,消息内容会变成“一坨XX”,不忍直视。

后来,我们也看不下去了,所以决定做一些改变,仿照 iMessage 或 QQ 那种消息模式,示意图:

这种方式和上面那“一坨XX”形成了鲜明对比,对话模式的消息显示,使用户体验度更好,就像两个人面对面说话一样,很轻松也很简洁,我想我们这种方式的改变,会让你“爱上”我们短消息的。

对,没错,这就是业务需求变化,我们在应用程序开发的过程中,需求是一直不断变化的,我们要做的就是不断完善和适应这种需求变化,当然每个人应对的方式不同,下面看一下我“愚蠢”的应对。

“愚蠢”的应对

我个人觉得这一节点内容非常重要,在领域驱动设计的过程中,也是很多人常掉进的“坑”,因为我们长期受“脚本模式”的影响,在业务需求变化后,应用程序需要做出调整,但是你会不自觉的“跑偏”,这就偏离了领域驱动设计的思想,最后使你的应用程序变得“不伦不类”。

当时为了很快的在应用程序中实现这种功能,我说的是技术上实现,完全没有用领域驱动的思想去考虑,我是怎么思考的呢?先从 UI 上考虑,主要是两个界面:

  • 消息列表:收件箱、发件箱和未读消息列表。
  • 消息详情:消息详情页。

消息列表实现

之前短消息不管发送,回复,还是转发,都是作为一个新短消息进行发送的,“消息的上下文”作为一个消息的附属体,放在新短息内容中,也就是说,你把之前发送的消息删掉,在新回复的短消息内容中,是仍然看到之前发送内容的,这个在列表的显示就是单独进行显示,但新的需求变化就不能这样进行操作了,这个就有点像两个人聊一个话题,里面都是我们针对这个话题进行讨论的内容,在列表显示的时候,首先,标题显示就是这个话题的标题,就像邮件回复一样,我们可以加上“消息标题(3)”,这个“3”,就表示两个人回复了3次。

其实用话题这个逻辑是有些不准确的,毕竟我们是短消息项目,我们可以这样想,我给 netfocus 发了一个标题为:“打个招呼”,内容为:“hello netfocus”的消息,然后他给我进行了回复:“hello xishuai”,可能后面还有一些消息回复内容,但都是针对我发的第一条消息回复,也就是说下面都是回复内容,那这个在消息列表显示的时候,标题就显示为“打个招呼(3)”,后面时间为最新回复时间,示意图:

上面是 netfocus 的收件箱示意图,收件箱列表显示的逻辑就是以发件人标题为一个标识,比如 Jesse Liu 也给 netfocus 发了一个“打个招呼”的消息,虽然标题一样,但发件人不一样,所以列表显示两条消息。

那代码怎么实现这个功能呢?贴出代码看看:

        public async Task<IEnumerable<MessageListDTO>> GetInbox(Contact reader, PageQuery pageQuery)        {            var query = efContext.Context.Set<Message>()                .Where(new InboxSpecification(reader).GetExpression()).GroupBy(m => new { m.Sender.ID, m.Title }).Select(m => m.OrderByDescending(order => order.ID).FirstOrDefault());            int skip = (pageQuery.PageIndex - 1) * pageQuery.PageSize;            int take = pageQuery.PageSize;            return await query.SortByDescending(sp => sp.ID).Skip(skip).Take(take)                .Project().To<MessageListDTO>().ToListAsync();//MessageListDTO 为上一版本遗留问题(Select FileName),暂时没动。        }

GetInbox 是 MessageRepository 中的操作,其实原本收件箱的代码不是这样处理的,你会看到,现在的代码其实就是 Linq 的代码拼接,我当时这样处理就是为了可以方便查询,现在看确实像“一坨XX”,代码我就不多说了,上面列表显示功能是可以实现的,除去回复数显示,其实你会看到,这个就是对发件人和标题进行筛选,选取发送时间最新的那一条消息。

虽然这段 Linq 代码看起来很“简单”,但是如果你跟踪一下生成的 SQL 代码,会发现它是非常的臃肿,没办法,为了实现功能,然后就不得不去优化数据库,主要是对索引的优化,这个当时优化了好久,也没有找到合适的优化方案,最后不得不重新思考这样做是不是不合理?这完全是技术驱动啊,后来,我发现,在领域驱动设计的道路上,我已经完全“跑偏”了。

消息详情页实现

业务需求的变化,其实主要是消息详情页的变化,从上面那张消息详情页示意图就可以看出,刚才上面说了,收件箱列表显示是对标题和发件人的筛选,其实详情页就是通过标题和发件人找出回复消息,然后通过发送时间降序排列。具体操作是,在收件箱中点击一条消息,然后通过这条消息和发件人去仓储中找这条消息的回复消息,示例代码:

        public async Task<IEnumerable<Message>> GetMessages(Message message, Contact reader)        {            if (message.Recipient.ID == reader.ID)            {                return await GetAll(Specification<Message>.Eval(m => m.Title == message.Title                    && ((m.Sender.ID == message.Sender.ID && m.Recipient.ID == message.Recipient.ID && (m.DisplayType == MessageDisplayType.OutboxAndInbox || m.DisplayType == MessageDisplayType.Inbox))                    || (m.Recipient.ID == message.Sender.ID && m.Sender.ID == message.Recipient.ID && (m.DisplayType == MessageDisplayType.OutboxAndInbox || m.DisplayType == MessageDisplayType.Outbox)))),                    sp => sp.ID, SortOrder.Ascending).ToListAsync();            }            else            {                return await GetAll(Specification<Message>.Eval(m => m.Title == message.Title                        && ((m.Sender.ID == message.Sender.ID && m.Recipient.ID == message.Recipient.ID && (m.DisplayType == MessageDisplayType.OutboxAndInbox || m.DisplayType == MessageDisplayType.Outbox))                        || (m.Recipient.ID == message.Sender.ID && m.Sender.ID == message.Recipient.ID && (m.DisplayType == MessageDisplayType.OutboxAndInbox || m.DisplayType == MessageDisplayType.Inbox)))),                        sp => sp.ID, SortOrder.Ascending).ToListAsync();            }        }

不知道你是否能看懂,反正我现在看这段代码是需要思考一下的,呵呵。消息详情页基本上就是这样实现的,还有一些是在应用层获取“点击消息”,UI 中消息显示判断等一些操作。

消息发送、回复、销毁等实现

其实除了上面列表和详情页的变化,消息发送、回复和销毁实现也需要做出调整,因为消息领域模型没有任何变动,发送消息还是按照之前的发送逻辑,所以发送消息是没有变化的,回复消息也没有大的变化,只不过回复的时候需要获取一下消息标题,因为除了第一条发送消息需要填写标题,之后的消息回复是不需要填写标题的,需要添加的只不过是消息内容。消息销毁的改动相对来说大一点,因为之前都是独立的消息发送,所以可以对每个独立的消息进行销毁操作,但是从上面消息详情页示意图中可以看到,独立的消息是不能销毁的,只能销毁这个完整的消息,也就是详情页最下面的删除按钮,示例代码:

        public async Task<OperationResponse> DeleteMessage(int messageId, string readerLoginName)        {            IContactRepository contactRepository = new ContactRepository();            IMessageRepository messageRepository = new MessageRepository();            Message message = await messageRepository.GetByKey(messageId);            if (message == null)            {                return OperationResponse.Error("抱歉!获取失败!错误:消息不存在");            }            Contact reader = await contactRepository.GetContactByLoginName(readerLoginName);            if (reader == null)            {                return OperationResponse.Error("抱歉!删除失败!错误:操作人不存在");            }            if (!message.CanRead(reader))            {                throw new Exception("抱歉!获取失败!错误:没有权限删除");            }            message.DisposeMessage(reader);            var messages = await messageRepository.GetMessages(message, reader);            foreach (Message item in messages)            {                item.DisposeMessage(reader);                messageRepository.Update(item);            }            await messageRepository.Context.Commit();            return OperationResponse.Success("删除成功");        }

这个是应用层中消息销毁操作,可以看到应用层的这个操作代码很凌乱,这就是为了实现而实现的代价,除了消息销毁,还有一个操作就是消息状态设置,也就是消息“未读”和“已读”设置,这个代码实现在应用层 ReadMessage 操作中,代码更加凌乱,我就不贴出来了,和消息销毁操作比较类似,消息状态设置只不过设置一些状态而已。

回到原点的一些思考

为什么我会详细描述我当时实现的思路?其实就是想让你和我产生一些共鸣,上面的一些实现操作,完全是为了实现而实现,不同的应用场景下的业务需求变化是不同的,但思考的方式一般都是想通的,也就是说如果你能正确应对这个业务需求变化,那换一个应用场景,你照样可以应对,如果你不能正确应对,那领域驱动设计就是“空头白话”,为什么?因为领域驱动设计就是更好的应对业务需求变化的。

其实上面的需求变化,我们已经变相的实现了,只不过没有发布出来,就像一个多月之前的发布公告中所说,“Does your code look like this?”,如果按照这种方式实现了,那以后的短消息代码,就是那一坨面条,惨不忍睹。

回到原点的一些思考,其实就是回到领域模型去看待这次的业务需求变化,关于这部分内容,我还没有准确的做法,这边我说一下自己的理解:

业务需求变化,领域模型变化了吗?

首先,在之前的实现中,消息列表显示这部分内容,应该是应用层中体现的,所以在领域模型中可以暂时不考虑,这个在仓储中应该着重思考下。那领域模型变化了什么?先说发送消息,这个变化了吗?我觉得没有,还是点对点的发送一个消息,这个之前是用 SendSiteMessageService 领域服务实现的,逻辑也没有太大的变化,那回复消息呢?其实我觉得这是最大的一个变化,如果你看之前的回复代码,我是没有在领域模型中实现回复消息操作的,为什么?因为我当时认为,回复消息其实也是发送消息,所以在应用层中回复消息操作,其实就是调用的 SendSiteMessageService 领域服务,这个现在看来,是不应该这样实现的。

我们先梳理一下回复消息这个操作的处理流程,这个其实上面有过分析,除了第一条消息是发送以外,之后的消息都是回复操作,这就要有一个标识,用来说明这条消息是回复的那一条发送消息,那这个怎么来设计呢?回复消息设计成实体好?还是值对象好?我个人觉得,应该设计成实体,原因大家想想就知道了,虽然它依附于发送消息存在,但是它也是唯一的,比如一个人给另外两个人回复同样内容的消息,那这两个回复消息应该都是独立存在的,那这个依附关系怎么处理呢?我们可以在消息实体中添加一个标识,用来表示它回复的是那条消息。

上面这个确定之后,那我们如何实现回复消息操作呢?我们可以用一个领域服务实现,比如 ReplySiteMessageService,用来处理回复消息的一些操作,这个和 SendSiteMessageService 领域服务可能会有些不同,比如一个人 1 天只能发送 200 条消息,但是这个逻辑我们就不能放在回复消息领域服务中,回复只是针对一个人的回复,所以这个可以不做限制,发送是针对任何人的,为了避免广告推广,这个我们必须要做一个发送限制,当然具体实现,就要看需求的要求了。

除了回复消息这个变化,说多一点,消息状态(未读和已读)和消息销毁,这个可能也会有细微的变化,比如消息状态,在消息列表中打开一个消息,其实就是把这条消息的回复内容都设置成已读了,我们之前的设计是针对独立的消息状态,也就是说每个消息都有一个消息状态,按照这种方式,其实我们可以把这个状态放在发送消息实体中,如果有人回复了,那这个消息状态就是设置为未读,回复消息没有任何状态,如果这样设计的话,有点像值对象的感觉,可以从消息实体中独立出来一个回复消息值对象,当然这只是我的一种思路。消息销毁和这个消息状态比较类似,这边就不多说了,除了这两个变化,其实还有一些细节需要考虑,这个只能在实现中进行暴露出来了。

对象读取的额外思考

这个其实是我看了仓储那惨不忍睹的实现代码,所引起的一些思考,你可以读一下,这样的一篇博文:你正在以错误的方式使用ORM

仓储在领域驱动设计的作用,可以看作是实体的存储仓库,我们获取实体对象就要经过仓储,仓储的实现可以是任何方式,但传输对象必须是聚合根对象,这个在理论中没有什么问题,但是在实际项目中,我们从仓储中获取对象,一般有两种用途:

  1. 用于领域模型中的一些验证操作。
  2. 用于应用层中的 DTO 对象转化。

第一种没有什么问题,但是第二种,这个就不可避免的造成性能问题,也就是上面文中 Jimmy(AutoMapper 作者)所说的 Select N 问题,这个我之前也遇到过,最后的解决方式,我是按照他在 AutoMapper 映射的一些扩展,也就是上面代码中的 Project().To(),但这样就不可避免的违背了领域驱动设计中仓储的一些思想。

关于这个内容,我不想说太多,重点是上面领域模型的思考,仓储的问题,我是一定要做一些改变的,因为它现在的实现,让强迫症的我感觉到非常不爽,不管是 CQRS、ES、还是六边形架构,总归先尝试实现再说,有问题不可怕,可怕的是不懂得改正。

写在最后

在领域驱动设计的道路上,有很多你意想不到的情况发生,稍微不注意,你就会偏离的大方向,很遗憾,我没有针对这次的业务需求变化,做出一些具体的实现,但我觉得意识到问题很重要,这篇博文分享希望能与你产生一些共鸣。

24楼『大雪无痕』
@全体成员,,谁能用 100个字,帮我解释一下 什么是 “领域驱动”????,,—— 好吧,我承认 我不喜欢看 没有 重点标黑 的文字。
Re: xuefly
@『大雪无痕』,[email protected],,谁能用 100个字,帮我解释一下 什么是 “领域驱动”????,,—— 好吧,我承认 我不喜欢看 没有 重点标黑 的文字。,领域驱动设计良好,领域驱动开发zuo si。领域是边界,‘域’就是边界,领域指问题域,主要指我们帮助客户解决的客户的那个业务领域而不指程序员所熟悉的各种类啦的接口啦的数据库啦的这些东西所在的那个问题域。,领域驱动设计是好的,领域驱动开发是作死。领域驱动基于这样的假设:客户领域既有的业务架构,客户领域的现状是良好的(这当然良好了,它的良好不是一日建成的更不是程序员建成的),领域驱动就是要重用已有的良好的知识。我们所做的只不过是把这些良好的知识使用机器可以理解的语言表述一遍(当然由于人们有了计算机这个良好的生产工具后,当然客户领域的现行架构是要因为有了更牛逼的计算机这个生产工具后做些调整的,而这个调整也不是我们要干的事,人家绝不比咱们差,人家早已做过调整,人家的知识通常是可以重用的在那个领域中比我们要良好的多)。
Re: 缪军
@『大雪无痕』,谁能用 100个字,帮我解释一下 什么是 “领域驱动”????,,大多数情况下,社区里讨论DDD,其实特指Eric Evans的DDD,另一方面,面向对象理论的核心也是领域驱动,domain是所有问题的核心,,然而,你要明白,虽然字面相似,但是这完全是两个世界观,对分析与设计的理解有着巨大的差别,,我个人对eric的理论持强烈反对的立场.,,我的建议是:与其在这里看各种不着调的YY,不如找几本代表两种思想的书籍仔细研读几遍:,1.Eric Evans的DDD的代表作当然是lt;Domain-Driven Design –Tackling Complexity in the Heart of Softwaregt;,2.OOAD的代表作强烈推荐邵维忠教授和杨芙清院士的新书lt;面向对象的分析与设计gt;,3.国外的重量级OOAD的著作推荐Grady Booch的lt;Object Oriented Analysis and Design With Applications Third Editiongt;
23楼徐少侠
@netfocus,同意这个想法,,手机短信的来回两个人的消息,弄个标题进去的确没啥意思,,但是如果两个人之间讨论两件事,一个是讨论代码,一个是讨论晚上吃饭,,至少以博客园如今的用户体验来说,是希望分开有两个独立的,此时如果没title,不好处理呀
22楼netfocus
当然,博主的消息的是否删除的设计,和你的是不同的,呵呵,他目前是通过不同的状态了标记是否对发件人可见或者对收件人可见的。扩展性方面,没有你的设计好。不过应付目前的需求也可以了。如果要改为你的设计,改造还是需要一定代价,还需要做数据迁移什么。所以暂时应该不会动。
21楼翱翔
现有系统如何应对需求变化?,1. 原需求与新需求本质不同是什么?,  是消息之间的关系 -- 原需求中每个消息是独立的,新需求中消息被按某种业务规则(Session)分组,2. 如何实现?,  a. 应用层实现Session -- 每次加载数据时,加入业务规则的实现,应该是类似文章中的实现方式。优点是修改量少,快速;缺点是代码丑陋,维护成本高。,  b. 模型层/数据层实现Session -- 在底层加入session模型,对已有模型自动升级。优点是清晰,便于后期维护和应对需求变更;缺点是修改量大,成本高。
Re: 徐少侠
@翱翔,@xuefly,根-gt;子实体 根--gt;依赖,,参与者-gt;会话状态--gt;会话,会话-gt;消息状态--gt;消息,消息--gt;参与者,会话,
20楼斯克迪亚
我也觉得应该参考手机短信、微信、微博私信的做法,不要区分本体和回复,统一视为短消息就可以了,后面要做的只是把短消息按对话目标用户分组以显示出来即可,详细页面就显示两方的对话,这样再好不过了。,,现在这样把短消息整成一个小论坛形式确实有些复杂化了,仔细想想看我觉得你可能是受电子邮件的形式影响较多,但是邮件这种东西是比较正式的、有主题性的,短消息侧重的是便捷地沟通,标题其实都没必要存在的,很多时候我都觉得写上个标题都是多此一举的麻烦事,好多短消息内容只有一句或半句话,那都要加个望眼欲穿却还半遮面的坑爹标题,还有些短消息直接把内容写在标题上,你说我点它,打开一看内容和主题一模一样,大呼坑爹,我要不点它,鬼知道里面还有没有别的内容啊,而且两个人之间同时开两个以上的私密话题进行讨论的可能性微乎其微,没什么意义。,,我觉得咱要不就直接动个大刀子,把领域模型彻底改造得了。,,
Re: 田园里的蟋蟀
@斯克迪亚,翱翔的问题netfocus已经回答了,我来回答一下兄台的疑问。,,现在的短消息系统确实和手机短信、微信什么的不太一样,是和邮件比较类似,关于是否需求主题或标题的概念,我觉得是需要的,其实像微信什么的都属于即时消息,也就直接对收件人发送消息,并没有说要写个标题什么的,而现在的短消息系统,应该属于延时消息,这个我不一定会立即查看的,就像邮箱一样,可能会过一段时间进行处理,而且其中会有一些系统消息什么的,这个是需要一个标题进行说明的。,,其实现在的需求改变是,比如A给B发一个消息,是需要填写收件人、标题和内容的,而B回复A只需要写内容就可以了,这个其实最能体现模型就是我和netfocus讨论的第一种实现方式,设计为两个领域模型,也就是消息和回复模型,除了第一次是发送消息以外,其他都是针对这条消息的回复,也就是只有内容。我们最后探讨的是使用第二种方式,也就是消息和回复都是消息,使用parent什么的进行关联,这种方式比较好处理些,也容易进行实现,上面还有翱翔说的第三种方式Session,其实都是有一定道理的,没有谁对谁错,我觉得探讨这里面所蕴含的东西是非常有价值的。
19楼翱翔
貌似少了很多回复?我错过了什么?,回复里的讨论很棒!,,本人不了解DDD, 仅仅根据个人的经验提出设计模型。,,分析,1, Message - 如果删除了新消息,回复消息怎么办?不允许删除?为什么不允许,如果客户就是要删除呢?难道要做补丁式的实现?, 所以消息是独立的实体,无论是新消息还是回复消息,回复消息不依赖新消息。,2, Session - 会话是一个很有用的实体模型,是从现实业务中抽象出来的。它是消息和用户的关系管理。, 一个特别的地方, 我认为Session是有且仅有一个用户关联,这样可以灵活管理session和消息的状态。,,设计模型,Message: ID, Title, Body, Attach, Creator, DateTime,Session: ID, Owner, Title, State, MessageListlt;Message, State[new, read, delete...]gt;,,可能的业务需求变化:,1. 用户可以删除新消息或者回复消息,或者session。之后仍然可以收到这个session的消息。,2. 用户可以发送一个消息给多个人,回复时可以回复所有人(类似邮件/群聊),3. 可以邀请别人加入session。(查看之前所有消息或只可以查看加入后消息, 类似群聊)
Re: netfocus
@翱翔,看了下你的设计,你设计的模型如下:,Message: ID, Title, Body, Attach, Creator, DateTime,Session: ID, Owner, Title, State, MessageListlt;Message, State[new, read, delete...]gt;,,我有一些疑问想和你讨论下:,1. 通过session来管理所有的消息吗,因为我看到你的session对象中有一个消息集合。为何要让session来管理所有的消息,而不让消息依赖一个sessionId?出于什么方面的考虑呢?,2. 然后,我看到消息对象中有一个title,session中也有一个title,这是为什么呢?既然title放在了session中,为何在message中也要有?,3. attach是做什么用的呢?,4. message中有一个creator,是表示这个消息的发送人吗?那收件人在哪里表示?还有session中的owner和message的creator是一个意思吗?如果是的话,那为何要重复设计呢?,5. MessageList集合,MessageListlt;Message, State[new, read, delete...]gt;这样设计是什么含义呢,是一个dict还是一个list呢?state是表示某个消息的状态吧?那这个消息是对发送人和接收人是同一份数据还是不同的?如果是同一份数据,那用同一个状态来表示的话,如何做到对发送人和接收人的显示时,状态是隔离的?因为实际上我发件人删除了消息,收件人是能继续看到这个消息的。包括消息的已读未读,也是同一个问题。
18楼netfocus
搞清楚什么是领域驱动意义不大,因为你永远无法搞清楚什么是领域驱动。我觉得我们更应该关心的是针对当前领域内的业务需求,大家各自分析出模型,并说出为什么要这样建模的原因。每个人得出的模型必定是站在一定的角度来理解领域,理解需求的。只要能自圆其说,能满足业务需求以及未来一定的业务变化,我觉得都是可行的建模方案。建模者可以从众多的建模结果中权衡选择一种,我相信没有最好的方案,因为每一种方案都有优点和缺点,否则就不需要权衡了。就像楼主目前选择的这种,也是综合考虑了目前已经实现的基础上,希望做最小改动为出发点,且能相对自然的应付目前的需求而采取的方案;如果有另外一种建模结果,但是和目前的建模方案完全不同,那调整会很大,成本过大。所以,都是综合权衡的结果。
17楼沈赟
mark
16楼netfocus
代楼主谢谢这么多人提供的宝贵意见哈,看起来楼主今天很忙,没时间回复大家!
Re: 深蓝医生
@徐少侠,[email protected],@田园里的蟋蟀,,继续补充,在会话领域模型中,困扰论坛和邮件领域的父子关系永远不存在了,,因为会话自然的是以时间顺序组织的,只要按照会话被保存到服务器的时间排序,就是实际的会话顺序,,因此任何parentID,都不是消息领域内应该出现的,赞同,所以DDD,找对领域,找到问题,分析领域更加重要,如果分析错了,那么设计再好也是错.
14楼xuefly
我感觉楼主想多了。看看手机消息是什么样的这个问题就比较明了了。消息中哪有什么主题这种凭空捏造出的概念呢,两个用户的消息的主题是什么只有他们俩知道,系统怎么可能知道呢?如果有一个技术主题的话那么这个技术主题恐怕会是(user1ID,user2ID)这个由两个用户的标识组成的没有方向的二元组。我们的系统中是不需要关注消息间的关系的,只需要按照时序把他俩的消息展示出来,比如uer1的展示在左面,user2的展示在右面,而消息的内容主题是什么自会有他俩的人脑和读者知道。
13楼gihelo
其实今年已经好几位在对这套方式提出疑问了,包括国外大牛也在对这套模式提出了疑问.我自己个人也是反对这中方式滴,出现这些毛病的最大的问题是,没多少人真正在阅读那本书,包括这里的很多“大牛”只是看了那本书的结果。然后呢就这样推广了,我们真正想想看那本书核心本质在讲什么?在讲DDD的过程,在讲domainmodel,已经如何在domainmodel层面防范变化,他在一层就已经把问题解决了,所以他才往后搞了一个仓储方案,问题就出在这里了.人家在前面防范过了,然后顺带实现滴仓储。而我们呢?你真正做了domainmodel么,你真正做了这个层面的防范没有,完全没有,你EF还code first了,EF(包括以后的Orm)自己很大的特性是延后查询,你可以延后滴啊,既然可以延后,为什么你要弄个仓储去提前设计方法??反过来既然你提前设计方法了,那人家的前提条件domainmodel分析你们又丢那里去了??难道所谓的code first方式就是你们的domainmodel??你这到底是设计domainmodel,还是在设计数据库??
Re: netfocus
@徐少侠,太忙,没时间仔细看你的回复。有空时再看!
11楼小菜?大神?
学习了,MARK
10楼田园里的蟋蟀
像上面有朋友说,现在的短消息调整不是领域驱动,而是UI驱动,这个表面上确实是这样,那如果我不用一个界面表示业务需求变化,而是用一堆文档表示,那是不是应该说是“文档驱动”了呢?其实不管什么来表示,或者你画一些活动图什么的,这都是业务需求变化的一种反应,使用界面,我觉得表述的更加直接。,,其实关于这次的业务需求变化,博文中表述的已经很明确了,我的想法是针对这次的变化,看领域模型是如何调整的,也就是之前我和netfocus兄探讨的两种实现方式,重点我觉得应该放在这个上面,其他一些相关的讨论我觉得可以另写一篇博文,一个博文探讨有一个主题即可。
Re: xuefly
@田园里的蟋蟀,引用像上面有朋友说,现在的短消息调整不是领域驱动,而是UI驱动,这个表面上确实是这样,那如果我不用一个界面表示业务需求变化,而是用一堆文档表示,那是不是应该说是“文档驱动”了呢?其实不管什么来表示,或者你画一些活动图什么的,这都是业务需求变化的一种反应,使用界面,我觉得表述的更加直接。,,其实关于这次的业务需求变化,博文中表述的已经很明确了,我的想法是针对这次的变化,看领域模型是如何调整的,也就是之前我和netfocus兄探讨的两种实现方式,重点我觉得应该放在这个上面,其他一些相关的讨论我觉得可以另写一篇博文,一个博文探讨有一个主题即可。,楼上经典
9楼netfocus
@徐少侠,,,你的观点我也认同,但我觉得你的建模结果也只是对领域的其中一种建模结果,不能说是唯一正确的,因为这只是对消息系统领域的一种理解方式;,你的设计是:,发送消息时,会有一个消息,同时生成一个会话,该消息自动关联到该会话,也就是消息上会有一个sessionId;,然后消息的标题不属于消息本身,而是属于会话;,当回复时,你也把回复理解为一个消息,然后这个消息的sessionId也是当前的session的id;,,这个设计,其实和我之前和蟋蟀讨论的第二种方式很像的。只是有3点区别:,1.我们没有独立的session对象,sessionId就是第一条消息的id;(第一条消息本质上可以替代会话的作用,因为后续该消息的所有回复都是以该消息为中心的),2.同时消息标题属于第一条消息;这样,发送消息时,只需要创建一个消息对象即可;,3.回复时,也是创建一个消息对象,这个消息对象的parentId就是第一个消息的id,本质上就是和你说的sessionid是一回事;如果你觉得parentId不是领域里的概念,那也完全可以把parentId的命名改为sessionId,其实就是第一条消息的id。这点相信蟋蟀肯定心里很清楚的。,,这两种消息的最大差别就是是否把消息标题独立到一个独立的会话对象中。我认为是否独立在消息系统这个领域中都可以;你这种设计,和老外喜欢在设计论坛时,有一个thread的概念,thread只负责存储帖子标题以及帖子的所有统计信息,比如回复数,然后帖子内容放在post里,回复内容也在post里,thread起到了穿针引线的作用。但这只是论坛领域的一种理解方式,还有其他的理解方式和建模方式。很多中国的论坛就不是这样设计。,,然后,我和蟋蟀之前还讨论了另一种建模方案,就是明确区分开消息和回复,把回复也建模为独立的聚合;,消息还是和我上面说的两种方案一样,只是回复只有一个被回复的消息ID。这样回复其实没有了消息的意思,只是像一个链表一样,在头结点后面追加一个节点而已。回复不需要关心我当前要发送给谁,它只关心我当前是对那个消息进行回复。然后因为消息的所有回复只允许消息发送人和接受人才能看到,自然就等同实现了通过回复来实现消息通信的目的。,,我觉得这三种建模方案都可以,区别是我们如何理解领域中的概念的,如何抽象的。我觉得没有对错。就看我们如何权衡每一种建模结果的好坏,,并综合考虑其他的因素,比如现有的实现方式,改造难度,项目周期,人力成本,等等。
Re: xuefly
@netfocus,[email protected],,,你的观点我也认同,但我觉得你的建模结果也只是对领域的其中一种建模结果,不能说是唯一正确的,因为这只是对消息系统领域的一种理解方式;,你的设计是:,发送消息时,会有一个消息,同时生成一个会话,该消息自动关联到该会话,也就是消息上会有一个sessionId;,然后消息的标题不属于消息本身,而是属于会话;,当回复时,你也把回复理解为一个消息,然后这个消息的sessionId也是当前的session的id;,,这个设计,其实和我之前和蟋蟀讨论的第二种方式很像的。只是有3点区别:,1.我们没有独立的session对象,sessionId就是第一条消息的id;(第一条消息本质上可以替代会话的...,相对更加良好的设计通常不会需要这么多文字描述别人才能明白。
8楼netfocus
这是你认为标题没意义呀!别人不一定这样认为的。如果楼主也觉得标题无意义,就不会把消息详情页改版为帖子详情页那样的风格了。标题的作用不需要讨论的,我觉得自有其意义,不需要用到标题的时候,你觉得是多余的,但是有时必须要标题的。因为确实有很多消息是围绕同一个主题两个人之间来回回复的。只是这种情况对你比较少可能。而且,是否需要标题,也是个需求问题,没有对错,就是需要的问题。
7楼netfocus
比如我先给你发个消息讨论ddd,然后你回复我我在回复你,最后讨论结束。然后你给我发个消息,讨论cqrs,然后你和我继续回复彼此,讨论结束。一个月后,你打开你的消息列表,由于标题的存在,你才能清晰的看到我们有过这两个主题的讨论,然后点击主题进去能看到回复详情,否则,你就只能看到你和我的所有聊天记录,很笼统,没办法区分每个消息是针对哪个主题的。,,当然,呵呵。这个其实是需求问题,如果博客园的消息,两个人之间基本只会发一个消息,那确实标题意义不大。但是如果有些人,交流比较频繁,经常两个人讨论很多主题,那标题就很有意义。我个人有时会向dudu反馈一些问题,然后他告诉我解决进度。如果没有标题,我可能以后不好方便的知道我向他反馈过哪些问题。所以,对我来说,还是有一定意义的。
6楼cwwhy
既然是领域设计了,是不是应该反映真实的领域场景,,怎么我发现消息领域还能增删改查消息呢,消息不应该是发送者发送或删除,接收者接收或者回复吗?,没有人觉得很奇怪吗?
5楼翱翔
文中场景不需要session ,消息和用户下消息关联就可以,需要session时可以在扩展。
4楼田园里的蟋蟀
感谢各位提出的意见,下一步的计划是,按照和netfocus兄讨论的那样,基本确定会用第二种方式实现,因为改动较小,这个先实现再说,暴露出来问题,再进行解决,这样体会的才会更加深刻,还有就是关于仓储的调整,我再研究下CQRS或其他实现方式,把现在的仓储实现进行调整下。
Re: 徐少侠
@田园里的蟋蟀,@翱翔,,Session 从字面看就应该关联两个人,如同开会,不会只有一个人,当然,实际情况会出现主持者到了,参与者一个没来。呵呵,,不过仔细考虑后,我感觉你说的有理。又学到东西了,呵呵。,,其实你的设计基本能满足很多需求了,比如那些删除、已读, MessageListlt;Message, State[new, read, delete...]gt;,,还有那个未读消息数量,以前我感觉要在session里附加某个参与者的最后阅读时间,想通了你的session不直接关联参与者的道理后,以及看到你的messageList设计,这个阅读时间也就没用了
3楼Jesse Liu
神一样的代码,楼主也POST出来了,赞一个。。。。上面那两坨看得我肠子打结的代码仔细一看,其实就是个Linq to SQL。所以我感觉规约这个东西还真是能不用就不用,而且领域实体,聚合这些东西,在查询这里面感觉不是很适合,很不自由,稍微复杂一点的地方,代码就会很难看。领域模型更适合写的操作,最近在看CQRS,所以有一些新的想法 。
2楼netfocus
标题是消息的剪短描述,我觉得还是有一定意义的,因为他是整个对话的主题,没有了主题,那整个对话也就没意义了。这点和手机里的短信还真不一样,而是和邮件一样。手机里的短消息,是没有标题的,你和别人的对话,你打开手机查看消息时是用对方的手机号码来作为session标题的,也就是你区分不出你们有过几次真正的不同对话,手机短消息只是帮你们两人记录聊天记录,这和楼主的短消息系统的场景是不同的。有了标题,我们才能明确知道我和对方有哪几次对话,每一次对话的主题是什么,这点手机短信是做不到的,因为没有标题
Re: 斯克迪亚
@netfocus,引用标题是消息的剪短描述,我觉得还是有一定意义的,因为他是整个对话的主题,没有了主题,那整个对话也就没意义了。这点和手机里的短信还真不一样,而是和邮件一样。手机里的短消息,是没有标题的,你和别人的对话,你打开手机查看消息时是用对方的手机号码来作为session标题的,也就是你区分不出你们有过几次真正的不同对话,手机短消息只是帮你们两人记录聊天记录,这和楼主的短消息系统的场景是不同的。有了标题,我们才能明确知道我和对方有哪几次对话,每一次对话的主题是什么,这点手机短信是做不到的,因为没有标题,我觉得根据具体使用情况,尤其是我自己的短消息使用情况来讲,标题的存在意义甚微,让我感觉它就是强加上去的鸡肋,这里面充斥着毫无意义的、文不对题的、有误导性的、半遮半掩的标题,很多半句话、一句话的事也得写个标题。,,我给你列一些你看看:,,标题: 炉石传说 C# 开发笔记,炉石传说 C# 你说的那个辅助,能分享下吗.我想学习下.,,标题: 你好,你好。 ,本人编写的近乎全部《仙境传说》(RO)服务器工具下载 包含源代码 ,我想要你这篇日志里的工具。还有备份么? ,,标题: 你好,看到你写的文章了,想和你认识,交个朋友。我叫洪伟杰 QQxxxx,,标题: 怎么联系你,有点东西想咨询,你好哥们,转盘那个不错,怎么联系你,有点东西想咨询,我的qqxxxxxx,,标题: w,你好,我是德资企业RIB软件公司的HR李小姐,目前在招聘C#开发工程师的岗位,负责B/S项目的核心代码编程,学习最先进的HTML5技术。公司产品为建筑造价软件,已经有50年的历史,为全球唯一一个整合性的5D软件,欢迎.net或C#开发经历的人员来应聘。 ,目前公司还有招聘10名以上开发工程师,要求有C#,.net项目经验,对HTML5, web应用开发感兴趣;或者是直接做web前端开发的人员也合适。 ,xxxxxxxxxxxxx,,标题: 成子湖关注了您,园友成子湖在您的博客中关注了您。,(这类系统消息有很多),,标题: 老师你好,你好,我是一名C#编程初学者今年都已经25岁了,我很喜欢微软的技术现在刚学编程不久,请问我努力学习一年后能在大都市找到适合的工作吗? ,请问可以推荐一下学习路线吗?,,标题: 本人编写的近乎全部《仙境传说》(RO)服务器工具下载 包含源代码[失效了],亲,能在发布一次么,收藏了= =可是却没有下载地址,让人伤心啊,,标题: 您的博客中有违规内容,您好! ,关于翻墙方面的内容属于违反国家相关部门规定的内容。 ,麻烦您删除一下该内容。望理解。,,标题: 您好,您好,周末打扰您休息了. ,我看了您写过关于Entity Framework的文章. ,很想和您结实,咨询一些关于EF和搭建项目的知识. ,希望能加我QQxxxxxxxx,,标题: 你好,朋友,你写的这篇文章《较为周全的Asp.net提交验证方案》我很喜欢,能给我发一下,数据库SQL创建脚本和程序的原代码吗?我的QQxxxx,,标题: 您好,想求一下”幸运转盘“的源码,我是大三学生,在学习android,看了您的程序十分佩服,想了解一下转盘的原理是怎么实现的,想学习一下。网上搜到的关于转盘文章都说的很模糊,所以想从您的源码中学习。我可以保证只做学习用,绝无商业用途,请给我发送一份。我的邮箱xxxx
1楼gihelo
在ps一下,博主这个依旧不是领域模型,博主设计滴从上看到下,我更愿意认为这是UI service,是对外服务,而不是领域模型
Re: netfocus
@gihelo,恩,很希望能听你的意见,能否大概给出你的领域模型呢?
  相关解决方案