【DDD】业务建模实践 —— 删除帖子

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

 1 public class Post {
 2      ......
 3     /**
 4      * 帖子作者
 5      */
 6     private PostAuthor postAuthor;
 7
 8     public Post(long authorId, String title, String sourceContent) {
 9         this();
10         this.setAuthorId(authorId);
11         this.setTitle(title);
12         this.setSourceContent(sourceContent);
13         this.setPostAuthor(new PostAuthor(authorId)); //在构造函数初始化PostAuthor
14     }
15
16     /**
17      * 删除帖子
18      */
19     public void delete() {
20         this.setStatus(PostStatus.HAS_DELETED);
21     }
22      ......
23
24 }

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

 1 public class User {
 2     /**
 3      * 用户id
 4      */
 5     private long id;
 6
 7     public User() {
 8         super();
 9     }
10
11     public User(long id) {
12         this.setId(id);
13     }
14
15     /**
16      * 判定另外一个用户是否是本人
17      * @param otherUser
18      * @return
19      *     true —— 本人
20      *  false —— 非本人
21      */
22     public boolean isMyself(User otherUser) {
23         if(this.equals(otherUser)) {
24             return true;
25         } else {
26             return false;
27         }
28     }
29
30     @Override
31     public boolean equals(Object anObject) {
32         if(anObject == null) {
33             return false;
34         }
35         if(this == anObject) {
36             return true;
37         }
38         if(anObject instanceof User) {
39             if(this.id == ((User)anObject).getId()) {
40                 return true;
41             }
42         }
43         return false;
44     }
45
46     @Override
47     public int hashCode() {
48         return Long.hashCode(this.id);
49     }59     ......
60 }

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

 1 public class PostReader extends User {
 2
 3     public PostReader(long id) {
 4         super(id);
 5     }
 6
 7     /**
 8      * 删帖
 9      * @param post 拟被删除的帖子实体
10      * @return post 删帖后的帖子实体
11      * @throws BusinessException
12      */
13     public Post deletePost(Post post) throws BusinessException {
14         if (post == null) {
15             throw new BusinessException(ExceptionCode.POST_IS_NOT_EXIT);
16         }
17         if (!this.isMyself(post.getPostAuthor())) {18             throw new BusinessException(ExceptionCode.CAN_NOT_DELETE_OTHER_USERS_POST);
19         }
20         post.delete();
21         return post;
22     }
23 }

思考

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

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

方案1的application层代码:

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

方案2中application层代码:

 1 public class PostsServiceImpl implements IPostsService {
 2     ......
 3     public void deletePost(BaseInNewBean<DeletePostInBean> inBean, HttpServletRequest request) throws Exception {
 4         ......
 5         Post post = postRepository.queryPostDetail(postId);
 6         postReader.deletePost(post);       //domain层在application层只有一个api暴露
 7         postRepository.deletePost(post);
 8         ......
 9     }
10     ......
11 }

从代码的简洁性、可读性、可维护性已经模型的合理性来讲,方案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

时间: 2024-08-05 13:55:47

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

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

本文是基于上一篇‘业务建模小招数’的实践,主要讲解‘发表帖子’场景的业务建模,包括:业务建模.业务模型.示例代码:示例代码会使用java编写,文末附有github地址.相比于<领域驱动设计>原书中的航运系统例子,社交服务系统的业务场景对于大家更加熟悉,相信更好理解.本文是[DDD]系列文章的第一篇,可参考:通过业务系统的重构实践DDD Round-I 业务建模 在大家的常识中,每个人都有自己的观点,并可以发表自己的观点,在社区中便表现为:发布帖子.那么谁发布帖子呢? 很明显是帖子作者,于是我们

EA业务建模实践之业务用例图

本文重点是业务建模实践,以及建模工具EA初级使用过程日志. 先前写了些文档,从不同角度描述了业务建模,但是条理性和规范性仍无法让人一目了然.春节期间当我再次读了<软件方法>前几章,产生了共鸣:误解随处都在,通过UML规范沟通环境,是辛勤汗水的教训. 按书中观点及回答问题如下: 业务建模:描述组织内部各系统(人肉系统.机械系统.电脑系统......)如何协作,使得组织可以为其他组织提供有价值的服务.新系统只不过是组织为了对外提供更好的服务,对自己的内部重新设计而购买的一个零件.组织引进一个软件系

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

本文结合团队在ECO(社区服务系统)业务建模过程中的实践经验,总结得到一些DDD业务建模的小招数,不一定是完美的,但是对我们团队来说很有效用,希望能帮到其他人.后面会陆续将项目中业务建模的一些经典例子放上来,分享给大家. ECO系统是线上旧系统,它的建模过程有别于新系统的业务建模.由于背着历史包袱,ECO的建模过程不是那么纯粹,很容易受到旧代码的影响,陷入代码的细节中,初期举步维艰,靠着小步快跑的方式得到了一些雏形和方法论,后面越来越顺,效果还是不错的. 本文为[DDD]系列文章中的其中一篇,其

软件项目需求开发过程实践之业务建模用例图

本次软件工程项目是重建办公业务流程管理平台,需要在继承原370个流程基础上,还需要提供快速流程开发能力,并要求体现出流程管理的规范性,以及流程的执行力.效率.效益,最终为企业管理创新提供流程再造的能力. 在项目前期及需求分析阶段,开发人员致力于"降低成本",以最小的代价完成项目,其可预见性的软件产品是经过系统平台升级的,并经过改良的第二个办公业务流程管理平台.按客户验收要求,"只能打60分,是不能给予验收". 在软件开发中,需求工作致力于解决"产品好卖&q

UML建模实践——选“对”企业架构建模视角很关键

1.关于企业架构 根据开放群组的业务领导层IT架构指引:"有效的企业架构(Enterprise Architecture,EA)对企业的生存和成功具有决定性的作用,是企业通过IT获得竞争优势的不可缺少的手段." 企业架构如同战略规划,可以辅助企业完成业务及IT战略规划.在业务战略方面,可使用TOGAF及其架构开发方法(Architecture Development Method,ADM)来定义企业的愿景/使命.目标/目的/驱动力.组织架构.职能和角色.在IT战略方面,TOGAF及AD

业务建模 之 闲话&#39;闭包&#39;与&#39;原型继承&#39;

[引言]在业务建模中,我们经常遇到这样一种情况:“原型”对象负责实现业务的基本诉求(包括:有哪些属性,有哪些函数以及它们之间的关系),以“原型”对象为基础创建的“子对象”则实现一些个性化的业务特性,从而方便的实现业务扩展.最常见的搞法是: 1. 定义一个‘构造函数’,在其中实现属性的初始化,例如:var Person = function( ){};    //函数体中可以进行一些变量的初始化. 2. 再设置该函数的prototype成员,例如:Person.prototype = { goto

对业务建模

这不是什么新鲜话题,但是最近在维护一个之前同事遗留下来的项目,有了一些感想.其实我很久都不做具体的业务了.很长一段的时间主要在负责基础设施以及业务工具方面的开发,所以我做的事情,往往并不需要和具体的业务打交道,其实这样挺困难的,关键在于我并非设计者,设计者是其他的专职设计师,他学历高,工作时间也长,但是可惜的是,他在web系统的架构以及设计上并没有多少的经验,所以其实事情也挺难做的. 而现在接手的这个项目是一个很纯粹的业务系统,没有太多的自主设计,用的是标准javaEE的那一套,从技术上面来说是

业务建模

ON/OFF机制 1.配置标准端对端业务 一般分为4个步骤: (1)定义应用 应用( Application)具体描述应用的动作,比如说 http 应用,规定了每次取得页面的大小和时间间隔:对于 ftp 应用,规定上传和下载的流量,文件的大小和产生的事件间隔. 打开应用配置器物件的属性对话框,可以看到OPNET 已经为我们定义了 9 种应用 以Database应用为例配置业务参数 Transaction Mix 代表查询数据量占整个输入交易量的比例,一般是查询一半,其他交易占一半: Transa

直播开始:&#39;云榨汁机&#39;诞生记--聊聊JavaScript中的&#39;业务建模&#39;

闭包是JavaScript中的一个重要特性,在之前的博文中,我们说闭包是一个'看似简单,其实很有内涵'的特性.当我们用JavaScript来实现相对复杂的业务建模时,我们可以如何利用'闭包'这个特性呢?JavaScript中的'原型继承',又可以解决业务建模中的哪些问题呢?今天我们就通过一家'榨汁机工厂'生产设计'榨汁机'的故事,来聊一聊'闭包'和'原型继承'在业务建模中的作用.现在直播开始: 1> 工厂默认选用A型刀头方案制造榨汁机 例子当中我们主要涉及到2个函数:1.榨汁机的生产工厂(Jui