写在前面
本文一起看下项目架构DDD,MVC相关的内容。
1:MVC
不管我们做什么项目,自己想想其实只是做了三件事,如下:
其实,这三件事完全在一个类中做完也可以可以正常把项目完成的,就像下面这样:
@RequestMapping("/xxx")
public void XxxHttp {
private String userId;
private String username;
public Response queryUerById(String userId) {
queryFromDb(userId);
Response res = assembleRes();
return res;
}
// 查询数据
public void queryFromDb(String userId) {
// 查询数据库的操作,获取username等信息
}
// 组装响应
public Response assembleRes() {}
}
试想下,如果所有的东西全部都塞到一个类里去,那么这个后期项目的可维护性和可扩展性几乎为0。针对这个问题,系统架构就应运而生了,而MVC就是系统架构的一种,当然DDD也是,本文后边部分也会学习到。这里,先来只看MVC相关的内容,MVC通过分层的思想来解耦程序,增加可维护性和可扩展性,把不同的属性分配到不同类型的对象中,把方法进行更细粒度的划分,大概如下图:
对每部分功能进行细分后,MVC同样会将其划分到不同的层,即文件夹中,从而使得结构更加的清晰,基本上如下图:
为了对MVC有一个更加清晰的认识,以springboot的方式来看一个实际的项目例子:
为了方便学习,我录制了一个视频放在这里:
项目架构MVC
视频中MVC系统架构用到的关键层所对应的文件夹都已经创建完毕了。详细的参考下源码 。
再来看下最后的效果:
- 通过单元测试测试
- 访问接口
这样,一个典型的MVC项目我们就完成了!
接着继续来看DDD项目又是咋回事。
2:DDD
源码 。
DDD全称,domain-driven design,即领域驱动设计,是一种系统架构设计的思想,根据这种思想我们可以通过不同的模块来组织我们的项目结构,其中一种可能的组织方式如下:
注意:默认使用spring boot。
app:定义项目全局内容,如启动类,AOP配置,dockerfile,启动脚本等。
types:定义公用的常量类,枚举类,响应结果类,带分页的类等。
api:定义需要对外暴漏的类和接口,如dubbbo的服务描述接口,以及请求和响应中需要用到的类。
domain:最关键的模块,定义出不同业务模块的仓储接口,服务类(给出具体的实现),model(聚合类,entity,valobjs)。
infrastructure:对domain的仓储接口给出具体的实现,以及定义映射数据库表的PO类。
trigger:触发模块,作为触发程序执行的模块,如接收http接口的controller,接收rpc请求的provider,消费消息的消费者,以及自动定时执行的job等。
本文也会按照这种组织方式来共同进行学习。
2.1:app
定义app之前先来看下app模块中写啥东西:
全局的配置可以写在这里,比如:
1:项目级的AOP配置
2:项目级的config配置(比如配置redis的序列化方式等)
3:打包镜像,启动脚本(start.sh等)
2.2:types
定义types之前来看下types模块中写啥东西:
来定义一些通用的类型,比如常量类Constants,接口响应用到的公共对象,
比如定义了响应状态码和data数据ReponseEntity(当然不一定非得是这个名字)对象。
当然其他的只要是各个业务模块都可用用到的公共的类都可以定义在这里,但注意一定要是公用的哟!!!
- 一个可能的常量类
// 你们都甭继承我!!!
public final class Constants {
public static final String ERROR_MSG = "你干哈,都给我搞出错了!!!";
}
- 一个可能的封装相应信息的公共类
@Builder
public class ResEntity<T> {
private T data;
private int code;
}
其他的可根据具体情况定义在这里。
2.3:api
定义api之前来看下api模块中写啥东西:
如外部需要用到的rpc描述文件(需要打成jar包,提供出来),以及描述文件中需要用到的相关类,
如入参以及返回的对象等。
需要对外暴漏的类的和接口(如dubbo服务描述接口)
因此,一般该模块是提供出去给外部使用的,当然自己也会使用到。
- 定义可能的dubbo接口
/**
* dubbo的某个对外服务接口
*/
public interface DubboXxxInterface {
DubboReqObj querySth(DubboReqObj dubboReqObj);
}
- 入参和出参可能的用到的类
/**
* dubbo的某入参对象(使用者需要知道)
*/
public class DubboReqObj {
}
/**
* dubbo的某出参对象(使用者需要知道)
*/
public class DubboResObj {
}
2.4:domain
定义domain之前来看下domain模块中写啥东西:
首先按照业务模块,一个业务模块一个文件夹。在每个文件夹下分别定义的信息如下:
model:同MVC系统架构的domain层定义的内容
aggregates:组合valobjs,因为某个业务可能需要多种VO组合在一起才能满足需求,当然也可以聚合entity,所以该类对象就是用来聚合entity和valobjs,从而满足业务对数据需求的的一类对象
entity:一般和数据库实体对象是1v1的关系,但是相比与数据库实体PO,只包含其中业务相关的信息,比如id啊,创建时间啊,删除状态啊这些是不需要的,po的一些有限状态的字段在entity中使用枚举来表示,po的逗号分隔的信息在entity使用list形式来表示,总是呢,其实就是将po转换为更加符合业务需要的方式,这种更加符合业务需要的方式就是entity了,比如有PO,信息是private int id, private int status(1代表xxx 2代表xxx), private varchar hobbies(逗号分割),转换为entity就是 private StatusEnum status(1代表xxx 2代表xxx), private List hobbiesList,大概这种!哦,对,entity还有另一个作用对po做防腐,意思就是不要你随便糟蹋,嚯嚯PO同学。
valobjs:应该就是VO,给UI使用的
repository: 定义数据库操作的接口,infrastructure会通过依赖倒置的方式来提供具体的实现
service:具体服务代码的实现,同MVC系统架构的service,不过只定义本业务模块的相关服务类,相比于MVC的service范围更小,即是其子集,需要注意名字不一定非得是xxxService,也可以是xxxEgine,xxxFilter,xxxProcessor,xxxHandler等这种,当然为了能够更加清晰也可以再创建对应的engine,filter等子文件夹
- 可能的model信息(聚合对象,entity,valobjs)
- 可能的仓储接口
/**
* 用户仓库类
*/
public interface IUserRepository {
UserAggregates queryUserByIds(List<String> userIdList);
}
- 具体服务类
public interface IUserService {
}
public class UserServiceImpl implements IUserService {
// 通过spring容器注入在infrastructure模块中提供的仓库具体实现
@Resource
private IUserRepository userRepository;
}
以上private IUserRepository userRepository
我们会在infrastructure基础设施模块提供具体实现,并交给spring管理。
2.6:infrastructure
定义infrastructure之前来看下infrastructure模块中写啥东西:
1:基于domain层提供数据库操作的具体实现(在domain层中我们定义了数据库操作的接口了不是),与domain是一种依赖倒置的关系
2:和数据库表对应的PO也在这里写
3:对domain定义的仓库接口提供具体实现,因此需要依赖于domain模块,但注意domain模块不能依赖于infrastructure模块,但domain又需要具体的数据库接口实现的dao,咋办呢?通过spring 容器注入即可。
- user表PO
/**
* 数据库表user的映射(注意,需要做防腐处理,避免最重要的数据查询出现问题!!!)
*/
public class UserPO {
/** 用户ID */
private Long id;
/** 用户名称 */
private String userId;
/** 用户昵称 */
private String userNickname;
/** 用户头像 */
private String userHead;
/** 账号密码 */
private String userPassword;
/** 创建时间 */
private Date createTime;
/** 修改时间 */
private Date updateTime;
}
- dao
@Mapper
public interface IUserDao {
List<UserPO> queryUserList();
}
- 仓储实现
@Component
public class UserRepository implements IUserRepository {
@Resource
private IUserDao userDao;
@Override
public UserAggregates queryUserByIds(List<String> userIdList) {
List<UserPO> userPOS = userDao.queryUserList();
UserAggregates userAggregates = new UserAggregates();
// TODO userPOS -> userAggregates 略!
for (UserPO userPO : userPOS) {
System.out.println(userPO);
}
return userAggregates;
}
}
2.7:trigger
定义trigger之前来看下trigger模块中写啥东西:
写触发程序执行的相关内容,如
1:接收http请求的controller
2:接收mq消息的消费者
3:rpc调用的server(如dubbo server)
4:定时任务(如自己定义的timer,或者是等待xxx-job等分布式任务调度框架执行的代码)
5;其他等
- http
@RestController
public class Controller {
// 注入domain的service实现类就可以编写具体的处理逻辑了,这和常规的mvc就一样了!!!
@Resource
private IUserService userService;
@RequestMapping("/user")
public ResEntity<String> queryUser() {
// TODO 使用 userService写具体的逻辑,略!!!
return ResEntity.<String>builder()
.data("cccccc")
.build();
}
}
- rpc
/**
* 某dubbo的provider
*/
public class XxxDubboProvider {
}
- timer
/**
* 某定时执行的任务
*/
public class SomeXxxJob {
}
为了更好的帮助你理解DDD的内容,我录制了一个视频,放在这里:
项目架构DDD,手把手从零搭建一个落地的DDD项目,没有枯燥的理论,就是写代码!!!
写在后面
参考文章列表
架构的本质之MVC架构 —— Java简明教程,一套简单、清晰、明了的Java学习路线资料!!! 。