Skip to content

领域驱动设计的核心概念

一. 什么是领域驱动设计

领域驱动设计(Domain-Driven Design,简称DDD)是一种软件开发方法论,它将软件系统的重点放在核心领域逻辑上,强调业务领域专家和开发人员之间的紧密协作,通过统一语言来构建对领域的共同理解。

二. 我们如何来学习DDD呢?

我们先不管领域驱动设计的相关概念,我们就先假设我们现实生活中没有这样的系统,那我们会如何处理业务呢?

假设,我们自己要设计生产一台车,我们会如何处理呢?

我们肯定是要心中有一个车的概念,车长什么样子,车有哪些功能。那我们要设计一个车的外观,需要车子的底盘,需要四个轮子,需要一个发动机,需要一个方向盘,需要车座。然后需要给车子配置上功能,比如:前进,后退,加速,转弯,甚至需要一些附加功能:听歌,导航等。这就是一个建模的过程,在DDD中,这个就叫领域建模。为什么加上领域呢?因为你是在设计造车,而不是设计造船或者设计飞机!这是在不同的领域。因此,我们建模的时候,不能参杂别的领域的概念进来。

三. 领域驱动设计的相关概念

现在我们有了领域建模的一个初步概念,那接下来我们来看一下车子的相关的组件。 车子有哪些组件呢?

  • 底盘
  • 轮毂
  • 轮胎
  • 发动机
  • 方向盘
  • 车座

这些组件在DDD中存在三个概念,一个是实体,一个是值对象, 最后一个是聚合根

1.1 实体(Entity)

实体是具有唯一标识的对象,即使其属性发生变化,其身份也保持不变。例如:

汽车的发动机:一般保养的时候,我们是不会换发动机的,我们只会去维护它,但是它的属性是会发生变化的,维护好后,它的性能可能增强,但是本身这个发动机他是没有发生变化的,还是原来的发动的,这个时候,我们认为他是有唯一标识的,需要通过这个标识来维护保养它。

1.2 值对象(Value Object)

值对象是没有唯一标识的对象,即使其属性发生变化,其身份也不会发生变化。例如:

汽车的轮胎:轮胎使用几年后,我们一般是不维修的,直接更换。更换的时候,是不需要原来的轮胎的标识的,只要性能相同,规格相同的轮胎,我们就可以直接更换。

1.3 聚合根(Aggregate Root)

聚合根是一个有界的上下文,它是一个包含多个实体和值对象的对象,它的边界是由业务规则来定义的。例如: 汽车:汽车是一个聚合根,它包含了多个实体和值对象,比如:发动机,轮胎,方向盘,车座等组件。

聚合根是一种特殊的实体,它是有唯一标识的。

给车建了模型,我们还要给它配置一些功能,车子本身有很多功能,比如:前进,后退,加速,转弯,这种就是实体自己本身的一些行为方法。

这样我们的实体或者聚合根,就在你脑子中是一个活灵活现的车了。

有没有发现,上面的这些概念,似乎只是对于以前我们开发时的概念的一个偷换。换了一些高大上的名词,但是本质上是没有改变的。 我们平时会说的可能是,这是一个车的类(聚合根),加上一些属性(实体, 值对象),加上一些业务方法——在MVC中我们可能会叫Service层中写的方法(实体的行为)。

理解了上面的概念,然后给你一堆这样的组件和方法,我们是不是就可以生产一台车子了?!

有了这些概念,我们就来试着写一个简单的例子:

java
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层的接口,比如我有这样一个生产汽车的接口

java
@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层的实现类,比如我有这样一个生产汽车的实现类

java
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层的实现类,比如我有这样一个生产汽车的实现类

java
public class CarRepository {
   public void save(Car car) {
      // 保存汽车
   }
}

仓库层的具体实现我们现在不用管,我们只要知道我们将会让这个车放入仓库。到这里,我们可以把上面的Service改造一下。

java
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结构没啥区别啊,甚至连代码似乎都一样。

那好,我们来看下面一个案例,假设现在我要发动一个指令。说启动车辆,我们以前会怎么写?

java
@RestController
@RequestMapping("/api/v1/car")
public class CarController {

   @Autowired
   private CarService carService;

   @PutMapping
   public Car startCar(Long id){
      carService.startCar(Long id);
   }

}

实现他的Service

java
public class CarServiceImpl implements CarService {

   @Autowired
   private CarRepository carRepository;

   @Override
   public Car startCar(Long id) {
      carRepository.changeStatus(id, "启动");
      return car;
   }
}
java
public class CarRepository {
    public void changeStatus(Long id, String status) {
      // 改变汽车
      carMapper.updateCar(10001, "启动"); // St
    }
}

还有可能直接在Controller层,调用这个仓库层或者实际操作的mapper层

java
@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, "启动");
   }
}

这种写法,表面上看一看就能看懂,但是从领域模型上看,就有点似是而非了,换个人来看的时候,完全看不出这是在操作什么,就知道改变了个状态。看不出在代码中是在操作一个活灵活现的车,相反,给人的感觉只是在改变汽车的状态。

如果是领域实践,我们会怎么做呢?

java
@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)

怎么理解领域服务呢?

领域服务就是在单个聚合根或者单个实体,已经无法直接用来完成业务逻辑的时候,我们可以通过领域服务来处理,下面我来举几个例子:

  1. 我需要计算多辆车的价格,这个时候,我就需要一个领域服务来处理。 因为我单个聚合根或者实体(代表一辆车),这时我是无法在单个聚合根或者实体中,直接计算多辆车的价格的。 那这个时候,我可以这么写:
java

// 领域服务
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;
   }
}

你看,这个时候,我要计算多辆车的时候,我通过对多辆车的集合,来计算出多辆车的价格。这个时候就需要通过领域服务来处理了。

  1. 我们再来看第二个例子,比如汽车启动的过程,正常我们会这样想,我们对汽车聚合根操作启动一下,像这样car.start(),汽车不就启动了吗?事实上汽车启动的过程非常复杂。我们可以把汽车启动的过程,分解成多个步骤,比如:
  2. 汽车启动前的各项组件的健康情况检查
  3. 汽车油料的检查
  4. 发动机的检查
  5. 发动机点火
  6. 汽车启动

当然了, 我这边写的肯定是不完整不完善的,重点是我们发现聊了汽车,我们可以还会涉及到更多的领域,比如检查系统这就是一个子领域,但是你要说没有这个系统,我们相车子如果各项硬件组件完好的情况下,也能完成启动。但是为了保证安全,我们还是非常有必要在车子上上这样一个子系统的。所以它(检查系统)也可以算是一个聚合根。 那我们怎么来处理这个启动的过程呢? 这时,我们就可以通过一个领域服务来处理了。

java
public class CarDomainServiceImpl implements CarDomainService {
   @Override
   public void start(Car car, CarCheckSystem checkSystem) {
      checkSystem.check(car); // 检查系统
      car.start(); // 启动车辆
   }
}

这里Car是一个聚合根,CarCheckSystem是一个聚合根,我们可以通过一个领域服务来处理汽车启动的过程。这两个聚合根都是我们整个汽车的一部分,这个检查系统,我们是不能用来别的领域中的。但是这两个可以算作是一个领域中。 这样我们就可以通过一个领域服务来处理汽车启动的过程了。

  1. 我们再来看第三个例子, 比如两辆车未来会交互(其实现在汽车已经可以了),比如两辆车之间的交互,比如两辆车之间的消息传递。这个时候,我们就可以通过一个领域服务来处理了。
java
public class CarDomainServiceImpl implements CarDomainService {
   @Override
   public void sendMessage(Car car1, Car car2, String message) {
      car1.sendMessage(car2, message); // 发送消息
   }
}