领域驱动设计的核心概念
一. 什么是领域驱动设计
领域驱动设计(Domain-Driven Design,简称DDD)是一种软件开发方法论,它将软件系统的重点放在核心领域逻辑上,强调业务领域专家和开发人员之间的紧密协作,通过统一语言来构建对领域的共同理解。
二. 我们如何来学习DDD呢?
我们先不管领域驱动设计的相关概念,我们就先假设我们现实生活中没有这样的系统,那我们会如何处理业务呢?
假设,我们自己要设计生产一台车,我们会如何处理呢?
我们肯定是要心中有一个车的概念,车长什么样子,车有哪些功能。那我们要设计一个车的外观,需要车子的底盘,需要四个轮子,需要一个发动机,需要一个方向盘,需要车座。然后需要给车子配置上功能,比如:前进,后退,加速,转弯,甚至需要一些附加功能:听歌,导航等。这就是一个建模的过程,在DDD中,这个就叫领域建模
。为什么加上领域呢?因为你是在设计造车,而不是设计造船或者设计飞机!这是在不同的领域。因此,我们建模的时候,不能参杂别的领域的概念进来。
三. 领域驱动设计的相关概念
现在我们有了领域建模
的一个初步概念,那接下来我们来看一下车子的相关的组件。 车子有哪些组件呢?
- 底盘
- 轮毂
- 轮胎
- 发动机
- 方向盘
- 车座
这些组件在DDD中存在三个概念,一个是实体
,一个是值对象
, 最后一个是聚合根
。
1.1 实体(Entity)
实体
是具有唯一标识的对象,即使其属性发生变化,其身份也保持不变。例如:
汽车的发动机:一般保养的时候,我们是不会换发动机的,我们只会去维护它,但是它的属性是会发生变化的,维护好后,它的性能可能增强,但是本身这个发动机他是没有发生变化的,还是原来的发动的,这个时候,我们认为他是有唯一标识的,需要通过这个标识来维护保养它。
1.2 值对象(Value Object)
值对象
是没有唯一标识的对象,即使其属性发生变化,其身份也不会发生变化。例如:
汽车的轮胎:轮胎使用几年后,我们一般是不维修的,直接更换。更换的时候,是不需要原来的轮胎的标识的,只要性能相同,规格相同的轮胎,我们就可以直接更换。
1.3 聚合根(Aggregate Root)
聚合根
是一个有界的上下文,它是一个包含多个实体和值对象的对象,它的边界是由业务规则来定义的。例如: 汽车:汽车是一个聚合根,它包含了多个实体和值对象,比如:发动机,轮胎,方向盘,车座等组件。
聚合根
是一种特殊的实体,它是有唯一标识的。
给车建了模型,我们还要给它配置一些功能,车子本身有很多功能,比如:前进,后退,加速,转弯,这种就是实体
自己本身的一些行为方法。
这样我们的实体
或者聚合根
,就在你脑子中是一个活灵活现的车了。
有没有发现,上面的这些概念,似乎只是对于以前我们开发时的概念的一个偷换。换了一些高大上的名词,但是本质上是没有改变的。 我们平时会说的可能是,这是一个车的类(聚合根),加上一些属性(实体, 值对象),加上一些业务方法——在MVC中我们可能会叫Service层中写的方法(实体
的行为)。
理解了上面的概念,然后给你一堆这样的组件和方法,我们是不是就可以生产一台车子了?!
有了这些概念,我们就来试着写一个简单的例子:
public class Car { // 聚合根
private String id;
private String brand;
private String model;
private Engine engine;
private Tire[] tires;
public void start() {
// 启动车辆
engine.start();
}
public void stop() {
// 停止车辆
engine.stop();
}
}
public class Engine { // 实体
private String id;
private String type;
private int power;
public void start() {
// 启动发动机
}
public void stop() {
// 停止发动机
}
}
public class Tire { // 值对象
private String id;
private int size;
private String brand;
private String type;
}
车子的领域建模
,我们就完成了。为什么需要这样的概念呢,因为建模的过程,更符合业务语言,程序员的话可能说,我要建一汽车类,建一引擎类,建一轮胎类,我们要调用业务方法启动,然后修改一下发动机的状态等。但是业务语言不是这么说的,业务语言就会说,我要设计这样一辆车,外观是什么样的,然后给它配置上功能,比如:前进,后退,加速,转弯,甚至需要一些附加功能:听歌,导航等。我们这里的业务概念大家应该都能听懂和理解,如果换个我们不熟悉的领域就会很容易蒙圈了。
所以,我们需要一个统一的语言来描述业务语言,这个语言就是统一语言
。 统一语言
是团队成员(包括领域专家和技术人员)共同使用的语言,它:
- 减少沟通成本
- 提高理解的准确性
- 直接反映在代码实现中 我们的
统一语言
就是我们的业务语言,我们的业务语言就是我们的统一语言
。
好了,我们的模型已经搭建完成,现在就是需要来操作这个模型了。DDD中,所谓的六边型结构,或者洋葱结构,就是这个函义,我们的领域建模就是搭建的核心,剩下的就是外围的各种辅助组件,以及访问出入口的一个操作。
假设我们现在要生产这样一辆车,那我们会怎么做呢?
首先需要给指令,指令从哪进去呢,这里我们可以理解为Controller
层的接口,比如我有这样一个生产汽车的接口
@RestController
@RequestMapping("/api/v1/car")
public class CarController {
@Autowired
private CarService carService;
@PostMapping
public Car createCar(String brand, String model, Engine engine, Tire[] tires){
carService.createCar(String brand, String model, Engine engine, Tire[] tires);
}
}
这接口我们告诉生产人员(这里可以指我们的系统),我需要生产什么样的一辆车,告诉他们口算,型号,发动机,轮胎等信息。
然后我们需要一个Service
层来处理这个指令,我们可以理解为Service
层的实现类,比如我有这样一个生产汽车的实现类
public class CarServiceImpl implements CarService {
@Override
public Car createCar(String brand, String model, Engine engine, Tire[] tires) {
Car car = new Car(String brand, String model, Engine engine, Tire[] tires);
return car;
}
}
到这里,我们就生产了一辆车了,大家可能会说,这跟我们平时开发的过程有什么不同呢? 其实这里还不是完全能体现出来区别,因为这里创建汽车的对象和原来没什么不同。
假如我们这个车不需要放入仓库,或者说是我们自己在家自己生产的定制车辆,那我们就不用放入仓库,直接返回我调用者,就可以使用这辆车了。对吧!
那换成工厂就不一样了,它需要把生产出来的车先放进车库,这个时候,我们就需要用到仓库。我们来写一个仓库来处理吧
我们需要一个Repository
层来处理这个指令,我们可以理解为Repository
层的实现类,比如我有这样一个生产汽车的实现类
public class CarRepository {
public void save(Car car) {
// 保存汽车
}
}
仓库层的具体实现我们现在不用管,我们只要知道我们将会让这个车放入仓库。到这里,我们可以把上面的Service
改造一下。
public class CarServiceImpl implements CarService {
@Autowired
private CarRepository carReopsitory;
@Override
public Car createCar(String brand, String model, Engine engine, Tire[] tires) {
Car car = new Car(String brand, String model, Engine engine, Tire[] tires);
carReopsitory.save(car);
return car;
}
}
这样我们是不是就把生产出来的车,存到车库里面去了,下次有需要,拿出来使用便是了。
那这跟我们原来的MVC结构没啥区别啊,甚至连代码似乎都一样。
那好,我们来看下面一个案例,假设现在我要发动一个指令。说启动车辆,我们以前会怎么写?
@RestController
@RequestMapping("/api/v1/car")
public class CarController {
@Autowired
private CarService carService;
@PutMapping
public Car startCar(Long id){
carService.startCar(Long id);
}
}
实现他的Service
public class CarServiceImpl implements CarService {
@Autowired
private CarRepository carRepository;
@Override
public Car startCar(Long id) {
carRepository.changeStatus(id, "启动");
return car;
}
}
public class CarRepository {
public void changeStatus(Long id, String status) {
// 改变汽车
carMapper.updateCar(10001, "启动"); // St
}
}
还有可能直接在Controller
层,调用这个仓库层或者实际操作的mapper层
@RestController
@RequestMapping("/api/v1/car")
public class CarController {
@Autowired
private CarRepository carRepository;
@Autowired
private CarMapper carMapper;
@PutMapping
public Car startCar(Long id){
carRepository.changeStatus(id, "启动");
}
//或者
@PutMapping
public Car startCar(Long id){
carMapper.updateCar(id, "启动");
}
}
这种写法,表面上看一看就能看懂,但是从领域模型上看,就有点似是而非了,换个人来看的时候,完全看不出这是在操作什么,就知道改变了个状态。看不出在代码中是在操作一个活灵活现的车,相反,给人的感觉只是在改变汽车的状态。
如果是领域实践,我们会怎么做呢?
@RestController
@RequestMapping("/api/v1/car")
public class CarController {
@Autowired
private CarService carService;
@PutMapping
public Car startCar(Long id){
carService.startCar(Long id);
}
}
public class CarServiceImpl implements CarService {
@Autowired
private CarRepository carRepository;
@Override
public Car startCar(Long id) {
Car car = carRepository.findById(id); //找到这辆车
car.start(); // 启动车辆
return car;
}
}
public class CarRepository {
public Car findById(Long id) {
// 改变汽车
return carMapper.selectById(10001); // St
}
}
看,这样是不是感觉车辆有血有肉了,我们让汽车真的像是一辆汽车,你能看懂,业务人员也同样能看懂。我们假设内存无限大,那是不是可以把任意大的聚合根或者实体放在内存中,然后操作的时候就很方便了。
1.4 领域服务(Domain Service)
怎么理解领域服务呢?
领域服务就是在单个聚合根或者单个实体,已经无法直接用来完成业务逻辑的时候,我们可以通过领域服务来处理,下面我来举几个例子:
- 我需要计算多辆车的价格,这个时候,我就需要一个领域服务来处理。 因为我单个聚合根或者实体(代表一辆车),这时我是无法在单个聚合根或者实体中,直接计算多辆车的价格的。 那这个时候,我可以这么写:
// 领域服务
public class CarDomainServiceImpl implements CarDomainService {
@Override
public BigDecimal calculateTotalPrice(List<Car> cars) {
BigDecimal totalPrice = BigDecimal.ZERO;
for (Car car : cars) {
totalPrice = totalPrice.add(car.getPrice());
}
return totalPrice;
}
}
你看,这个时候,我要计算多辆车的时候,我通过对多辆车的集合,来计算出多辆车的价格。这个时候就需要通过领域服务来处理了。
- 我们再来看第二个例子,比如汽车启动的过程,正常我们会这样想,我们对汽车聚合根操作启动一下,像这样
car.start()
,汽车不就启动了吗?事实上汽车启动的过程非常复杂。我们可以把汽车启动的过程,分解成多个步骤,比如: - 汽车启动前的各项组件的健康情况检查
- 汽车油料的检查
- 发动机的检查
- 发动机点火
- 汽车启动
当然了, 我这边写的肯定是不完整不完善的,重点是我们发现聊了汽车,我们可以还会涉及到更多的领域,比如检查系统
这就是一个子领域,但是你要说没有这个系统,我们相车子如果各项硬件组件完好的情况下,也能完成启动。但是为了保证安全,我们还是非常有必要在车子上上这样一个子系统的。所以它(检查系统
)也可以算是一个聚合根。 那我们怎么来处理这个启动的过程呢? 这时,我们就可以通过一个领域服务来处理了。
public class CarDomainServiceImpl implements CarDomainService {
@Override
public void start(Car car, CarCheckSystem checkSystem) {
checkSystem.check(car); // 检查系统
car.start(); // 启动车辆
}
}
这里Car
是一个聚合根,CarCheckSystem
是一个聚合根,我们可以通过一个领域服务来处理汽车启动的过程。这两个聚合根都是我们整个汽车的一部分,这个检查系统
,我们是不能用来别的领域中的。但是这两个可以算作是一个领域中。 这样我们就可以通过一个领域服务来处理汽车启动的过程了。
- 我们再来看第三个例子, 比如两辆车未来会交互(其实现在汽车已经可以了),比如两辆车之间的交互,比如两辆车之间的消息传递。这个时候,我们就可以通过一个领域服务来处理了。
public class CarDomainServiceImpl implements CarDomainService {
@Override
public void sendMessage(Car car1, Car car2, String message) {
car1.sendMessage(car2, message); // 发送消息
}
}