【DDD】业务建模实践 —— 删除帖子
2017-09-18 09:00
531 查看
本文是基于上一篇‘业务建模战术’的实践,主要讲解‘删除帖子’场景的业务建模,包括:业务建模、业务模型、示例代码;示例代码会使用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聚合中。
新增User.java,重新equals()和hashCode(),并提供isMyself(User)业务方法:
新增PostReader.java,提供deletePost(Post)方法:
思考
我们在这里将deletePost行为建模在PostReader实体上,现在看来合情合理,事实上,最开始建模的时候我们误入歧途,将该行为赋予了Post,那么是怎么发现“将deletePost行为赋予PostReader”(称之为方案2)要好于“将deletePost赋予Post"(称之为方案1)呢?是在开始写application层代码时发现的,我们发现方案1会将业务逻辑散落在应用服务层,而方案2则不会,他将整个”删除帖子”场景的业务逻辑内聚在了一个方法中。
这涉及到application层的一些东西,不过没关系,对理解这个决策影响不大。口说无凭,上代码。
方案1的application层代码:
方案2中application层代码:
从代码的简洁性、可读性、可维护性已经模型的合理性来讲,方案2完胜。
github地址:https://github.com/daoqidelv/community-ddd-demo/tree/deletePost
branch:deletePost
我觉得将删贴行为赋予帖子查看者还不如给帖子作者,这样作者拥有发帖,删贴操作理解起来也顺了很多。从场景分析,帖子列表中也只有是本人的帖子才会提供删贴操作,不会让帖子查看人逐个尝试删除帖子。如果把删除帖子的行为赋予作者,那么在删除时也只需要判断待删帖子的作者是否是本人。
按照上面的思路,我们调整下业务模型如下,之前的文章内容也就不修改了,这样读者也能看出业务模型迭代的过程,也服务DDD设计思想的实施建议。之前模型中的PostReader暂不删除,后面的“查询帖子详情”会使用到。
对应的代码已经修改,并上传到github上。
业务建模
这里的‘删除帖子’场景是指帖子作者主动删除帖子,至于管理员通过后台管理端下线帖子,我们认为该行为不同于‘删帖’,需要单独处理。我们来分析下“删除帖子”这一业务场景:删除帖子的人需要查询到帖子,然后才能删除帖子,否则没有删除帖子的入口,也就是说删除帖子的人也同时是帖子的查看者;帖子的查看者只能删除自己发表的帖子,否则整个社区就乱套了。我们尝试用一句话来描述‘删除帖子’场景:
帖子删除者(同时也是帖子查看者)可以删除本人发布的帖子。
可以看出这里有三个实体:帖子删除者、帖子查看者、帖子,一个业务行为:删除帖子,一个业务规则:只可以删除本人发布的帖子。现在我们要讨论下“帖子删除者”和“帖子查看者”有何区别,实际上在“删除帖子”这个业务场景下,这两个实体只是对同一个人的不同表述,也就是说这个人在“删除帖子”这个场景下拥有了两个角色,在“删除帖子”这个业务场景下,他们拥有的业务行为没有区别,都是“删除帖子”;但是“帖子删除者”相对于”帖子查看者”要狭隘一点,它只能适用于“删除帖子”这一个场景,而“帖子查看者”还可以适用于“查看帖子详情”、“查看帖子列表”等业务场景。因此,我们建模在“帖子查看者”这一个实体上。即:“帖子查看者”(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】领域驱动设计实践 —— 业务建模小招数
- 【DDD】领域驱动设计实践 —— 业务建模战术
- 我的建模可以复制 -7 (汇总和提炼业务领域的最佳实践)
- 油田采油生产业务建模之业务用例实践(EA使用入门)
- 领域驱动设计(DDD)在美团点评业务系统的实践
- 我的建模可以复制(010)- 业务领域的最佳实践
- 从帖子中心开始,聊“1对多”类业务数据库水平切分架构实践
- DDD实践问题之 - 关于论坛的帖子回复统计信息的更新的思考
- 大家一直在谈的领域驱动设计(DDD),我们在互联网业务系统是这么实践的
- RUP大讲堂(第四讲)-业务建模技术实践 推荐
- 我的建模可以复制(007)- 汇总和提炼业务领域的最佳实践
- 我的建模可以复制 -10 (业务领域的最佳实践)
- EA业务建模实践之业务用例图
- 我的建模可以复制 -10( 业务领域的最佳实践 )
- 油田采油生产业务建模之活动图实践(EA使用入门)
- Asp.Net大型项目实践(3)-业务领域对象建模
- 油田采油生产业务建模之数据流图实践(EA使用入门)
- 通过业务系统的重构实践DDD - 倒骑的驴 - 博客园
- DDD实践问题之 - 关于论坛的帖子回复统计信息的更新的思考
- 从帖子中心开始,聊“1对多”类业务数据库水平切分架构实践