本文是基于上一篇‘业务建模战术’的实践,主要讲解‘删除帖子’场景的业务建模,包括:业务建模、业务模型、示例代码;示例代码会使用java编写,文末附有github地址。相比于《领域驱动设计》原书中的航运系统例子,社交服务系统的业务场景对于大家更加熟悉,相信更好理解。本文是【DDD】系列文章的其中一篇,其他可参考:使用领域驱动设计思想实现业务系统

业务建模

  这里的‘删除帖子’场景是指帖子作者主动删除帖子,至于管理员通过后台管理端下线帖子,我们认为该行为不同于‘删帖’,需要单独处理。

  我们来分析下“删除帖子”这一业务场景:删除帖子的人需要查询到帖子,然后才能删除帖子,否则没有删除帖子的入口,也就是说删除帖子的人也同时是帖子的查看者;帖子的查看者只能删除自己发表的帖子,否则整个社区就乱套了。我们尝试用一句话来描述‘删除帖子’场景:

  • 帖子删除者(同时也是帖子查看者)可以删除本人发布的帖子。

  可以看出这里有三个实体:帖子删除者、帖子查看者、帖子,一个业务行为:删除帖子,一个业务规则:只可以删除本人发布的帖子。现在我们要讨论下“帖子删除者”和“帖子查看者”有何区别,实际上在“删除帖子”这个业务场景下,这两个实体只是对同一个人的不同表述,也就是说这个人在“删除帖子”这个场景下拥有了两个角色,在“删除帖子”这个业务场景下,他们拥有的业务行为没有区别,都是“删除帖子”;但是“帖子删除者”相对于”帖子查看者”要狭隘一点,它只能适用于“删除帖子”这一个场景,而“帖子查看者”还可以适用于“查看帖子详情”、“查看帖子列表”等业务场景。因此,我们建模在“帖子查看者”这一个实体上。即:“帖子查看者”(PostReader)实体拥有“删除帖子”(deletePost)这样一个业务行为。

  结合前一篇blog(业务建模实践 —— 发布帖子)中已有的业务模型来看下,发现之前已经有一个“人”相关的业务实体——帖子作者(PostAuthor),既然他们都是人,就有必要做一些重构。观察PostAuthor和PostReader,他们共有大部分的属性和方法:id、name、nickname、headphoto等等,因此我们可以在两者之上抽离出一个实体——“用户”(User),User持有用户通用的属性和方法。

  再回过头来看下“删除帖子”的业务规则:只能删除本人发布的帖子,也就是说PostReader需要判定是否和Post的PostAuthor是同一个人,而这个业务判定并不只是在这个场景下独用,所以我们可以让User来持有这个业务行为,称之为isMyself(User otherUser)。

  毫无悬念,PostReader应当持有一个业务行为:“删除帖子”(deletePost)。显然deletePost方法的参数应当是一个Post实体,而在deletePost方法中需要调用User.isMyself(User otherUser)方法判定读者和作者是否是同一个人,因此,Post对象必须提供一个PostAuthor出来,有两种方式:一种是使用post.authorId创建一个PostAuthor,另外一种方式是重构模型,直接让Post持有一个PostAuthor。方案一会让PostReader“依赖”PostAuthor,方案二会让Post和PostAuthor之间耦合太紧,本身PostAuthor已经“依赖”了Post,现在Post反过来又“关联”PostAuthor,出现了双向关系,不是好现象。但是,综合考虑,方案一不符合实际业务关系,PostReader没有必要依赖于PostAuthor,因此我们选择了方案二。

业务模型

  综合上面的建模讨论,可以得到如下的业务模型:

  :PostAuthor和Post之间的关系变成了无向的“关联”关系,是因为Post“依赖”于PostAuthor,PostAuthor“关联”Post,变成了双向关系,因此使用无向的“关联”关系表示比较合适。

  汇总《业务建模实践 —— 发布帖子》中的业务模型,我们得到最终版本的业务模型,如下:

  可以看到在帖子模块,业务模型涉及三个聚合:User、Post、Topic;涉及contentFilter相关领域服务;其中值对象TopicPost即在Post聚合中也在Topic聚合中。

示例代码

Post.java增加postAuthor属性,并在构造函数中初始化,同时新增delete()业务方法。

 public class Post {
......
/**
* 帖子作者
*/
private PostAuthor postAuthor; public Post(long authorId, String title, String sourceContent) {
this();
this.setAuthorId(authorId);
this.setTitle(title);
this.setSourceContent(sourceContent);
this.setPostAuthor(new PostAuthor(authorId)); //在构造函数初始化PostAuthor
} /**
* 删除帖子
*/
public void delete() {
this.setStatus(PostStatus.HAS_DELETED);
}
...... }

新增User.java,重新equals()和hashCode(),并提供isMyself(User)业务方法:

 public class User {
/**
* 用户id
*/
private long id; public User() {
super();
} public User(long id) {
this.setId(id);
} /**
* 判定另外一个用户是否是本人
* @param otherUser
* @return
* true —— 本人
* false —— 非本人
*/
public boolean isMyself(User otherUser) {
if(this.equals(otherUser)) {
return true;
} else {
return false;
}
} @Override
public boolean equals(Object anObject) {
if(anObject == null) {
return false;
}
if(this == anObject) {
return true;
}
if(anObject instanceof User) {
if(this.id == ((User)anObject).getId()) {
return true;
}
}
return false;
} @Override
public int hashCode() {
return Long.hashCode(this.id);
} ......
}

新增PostReader.java,提供deletePost(Post)方法:

 public class PostReader extends User {

     public PostReader(long id) {
super(id);
} /**
* 删帖
* @param post 拟被删除的帖子实体
* @return post 删帖后的帖子实体
* @throws BusinessException
*/
public Post deletePost(Post post) throws BusinessException {
if (post == null) {
throw new BusinessException(ExceptionCode.POST_IS_NOT_EXIT);
}
        if (!this.isMyself(post.getPostAuthor())) { throw new BusinessException(ExceptionCode.CAN_NOT_DELETE_OTHER_USERS_POST);
}
post.delete();
return post;
}
}

思考

  我们在这里将deletePost行为建模在PostReader实体上,现在看来合情合理,事实上,最开始建模的时候我们误入歧途,将该行为赋予了Post,那么是怎么发现“将deletePost行为赋予PostReader”(称之为方案2)要好于“将deletePost赋予Post"(称之为方案1)呢?是在开始写application层代码时发现的,我们发现方案1会将业务逻辑散落在应用服务层,而方案2则不会,他将整个”删除帖子”场景的业务逻辑内聚在了一个方法中。

  这涉及到application层的一些东西,不过没关系,对理解这个决策影响不大。口说无凭,上代码。

方案1的application层代码:

 public class PostsServiceImpl implements IPostsService {
......
public void deletePost(BaseInNewBean<DeletePostInBean> inBean, HttpServletRequest request) throws Exception {
......
Post post = postRepository.queryPostDetail(postId);
if (post == null) { //Post实体对象不能判定自己是否为空,只能放到这里
throw new BusinessException(ExceptionCode.POST_IS_NOT_EXIT);
}
if (postReader.isMyself(post.getPostAuthor())) { //Post实体并不持有PostReader对象,因此无法完成此判定
throw new BusinessException(ExceptionCode.CAN_NOT_DELETE_OTHER_USERS_POST);
}
post.deletePost(post);
postRepository.deletePost(post);
......
}
......
}

方案2中application层代码:

 public class PostsServiceImpl implements IPostsService {
......
public void deletePost(BaseInNewBean<DeletePostInBean> inBean, HttpServletRequest request) throws Exception {
......
Post post = postRepository.queryPostDetail(postId);
postReader.deletePost(post); //domain层在application层只有一个api暴露
postRepository.deletePost(post);
......
}
......
}

从代码的简洁性、可读性、可维护性已经模型的合理性来讲,方案2完胜。

建模经验

使用“继承”方式实现不同角色的同类实体

  对于同一类实体的不同角色,考虑使用“继承”方式来实现,将实体中共有的属性和业务行为建模在父实体上,将角色独有的属性和业务行为建模在子实体上。比如:PostAuthor和PostReader都是“用户”,因此我们抽象出一个父实体——User,它持有所有用户共有的属性和行为,PostAuthor则持有“帖子作者"独有的”发布帖子”的业务行为,PostReader则持有“帖子读者”独有的“删除帖子”的业务行为。

持续集成尽早发现模型中的不足

  每一个版本迭代完成后,尽早在application层完成集成,这样能尽早发现模型的不足;如果没有条件及早的开展application层的集成,那么必须写单元测试,以便以客户端的视角来审视模型的合理性。比如:对于deletePost建模在Post上还是PostReader上的例子中,便是尽早完成application层集成之后发现的问题。

源码

  此业务建模的demo已上传至github,欢迎下载和讨论,但拒绝被用于任何商业用途。

  github地址:https://github.com/daoqidelv/community-ddd-demo/tree/deletePost

  branch:deletePost

迭代

按"混沌攻城狮"的建议,应当将“deletePost”业务行为赋予PostAuthor而不是PostReader,这样更符合业务场景,想来甚是合理。摘录他的评论如下:

我觉得将删贴行为赋予帖子查看者还不如给帖子作者,这样作者拥有发帖,删贴操作理解起来也顺了很多。从场景分析,帖子列表中也只有是本人的帖子才会提供删贴操作,不会让帖子查看人逐个尝试删除帖子。如果把删除帖子的行为赋予作者,那么在删除时也只需要判断待删帖子的作者是否是本人。

按照上面的思路,我们调整下业务模型如下,之前的文章内容也就不修改了,这样读者也能看出业务模型迭代的过程,也服务DDD设计思想的实施建议。之前模型中的PostReader暂不删除,后面的“查询帖子详情”会使用到。

对应的代码已经修改,并上传到github上。

【DDD】业务建模实践 —— 删除帖子的更多相关文章

  1. 【DDD】业务建模实践 —— 发布帖子

    本文是基于上一篇‘业务建模战术’的实践,主要讲解‘发表帖子’场景的业务建模,包括:业务建模.业务模型.示例代码:示例代码会使用java编写,文末附有github地址.相比于<领域驱动设计> ...

  2. 【DDD】业务建模实践 —— 人关注人

    社区业务领域中,存在‘人关注人’的场景,在这个场景中,关系较为复杂,且均表现在‘人’同一个业务实体上,因此,这个case的建模过程值得思考.本文将就‘人关注人’这个业务case的领域建模进行探讨,欢迎 ...

  3. 【DDD】领域驱动设计实践 —— 业务建模小招数

    本文结合团队在ECO(社区服务系统)业务建模过程中的实践经验,总结得到一些DDD业务建模的小招数,不一定是完美的,但是对我们团队来说很有效用,希望能帮到其他人.后面会陆续将项目中业务建模的一些经典例子 ...

  4. 【DDD】领域驱动设计实践 —— 业务建模战术

    本文结合团队在COMMUNITY(社区服务系统)业务建模过程中的实践经验,总结得到一些DDD业务建模的小招数,不一定是完美的,但是对我们团队来说很有效用,希望能帮到其他人.后面会陆续将项目中业务建模的 ...

  5. 【DDD】领域驱动设计实践 —— 业务建模实例(‘发布帖子’)

    本文是基于上一篇‘业务建模小招数’的实践,后面的多篇博文类似.本文主要讲解‘发表帖子’场景的业务建模,包括:业务建模.业务模型.示例代码:示例代码会使用java编写,文末附有github地址.相比于& ...

  6. 领域驱动设计(DDD)的实践经验分享之持久化透明

    原文:领域驱动设计(DDD)的实践经验分享之持久化透明 前一篇文章中,我谈到了领域驱动设计中,关于ORM工具该如何使用的问题.谈了很多我心里的想法,大家也对我的观点做了一些回复,或多或少让我深深感觉到 ...

  7. 使用 UML 进行业务建模:理解业务用例与系统用例的相似和不同之处

    使用 UML 进行业务建模:理解业务用例与系统用例的相似和不同之处   作者:Arthur V. English 出处:IBM   本文内容包括: 背景 业务用例模型与系统用例模型有什么相似之处? 业 ...

  8. 领域驱动设计(DDD)的实践经验分享之ORM的思考

    原文:领域驱动设计(DDD)的实践经验分享之ORM的思考 最近一直对DDD(Domain Driven Design)很感兴趣,于是去网上找了一些文章来看看,发现它确实是个好东西.于是我去买了两本关于 ...

  9. DCI架构是如何解决DDD战术建模缺点的?

    摘要:将DCI架构总结成一句话就是:领域对象(Object)在不同的场景(Context)中扮演(Cast)不同的角色(Role),角色之间通过交互(Interactive)来完成具体的业务逻辑. 本 ...

随机推荐

  1. Go语言Map的使用

    Go 语言Map(集合) Map 是一种无序的键值对的集合.Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值. Map 是一种集合,所以我们可以像迭代数组和切片那样 ...

  2. Python 接口:从协议到抽象基类

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 15.0px Helvetica } 抽象基类的常见用途:实现接口时作为超类使用.然后,说明抽象基类如何检查 ...

  3. C语言指针(二)指向指针的指针

    注意:指向指针的指针适合于做链表 1.声明方式:在变量名前放置两个*符号 int**var; 2.实例: #include<stdio.h>int main (){intvar;int*p ...

  4. Myeclipse详细使用教程

    Myeclipse详细使用教程.. /*+Shift+Enter(生成多行注释) /**+Shift+Enter(生成文档注释)-----------------问题:在编辑jsp的时候,如果光标移动 ...

  5. myeclipse db browser 新建数据源

    Myeclipse 新建数据源 一.打开myeclipse(打开了当我没说) 二.在window选项中找到show view ,点击other,输入db,选择DB Browser 三.在DB Brow ...

  6. 递归调用里的性能问题(js)

    说明 这是在codewars.com上刷的一道js练习题,在此做个记录 问题描述 The Fibonacci sequence is traditionally used to explain tre ...

  7. Web开发中常用的状态码

    在HtttpServletResponse类中有关于状态码的描述. static int SC_ACCEPTED Status code (202) indicating that a request ...

  8. https加密实现

    author:JevonWei 版权声明:原创作品 在httpd安装完成的基础上实现https加密 安装mod_ssl软件包 yum -y install mod_ssl http -M 显示mod_ ...

  9. protobuf和thrift对比

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt383 数据类型 protobuf thrift protobuf thrif ...

  10. this的用法 – JavaScript深入浅出(二)

    写在前面 上一篇中,我们对于JavaScript中原始值.复杂值以及内存空间进行了一个深入浅出的总结,这次我们来聊一聊JavaScript中this关键字的深入浅出的用法. 在 JavaScript ...