领域驱动设计实现业务流程:开始开发

根据业务,我们开始编写用例,接下来我们开始编程。

编写用例

网站登录,考虑以下用例

  • 用户正常登录
  • 用户无账号,注册账号
  • 用户忘记密码,重置密码
  • 用户注销登录

示例代码(JAVA):

@RestController
public class UserController {
    /**
     * 登录
     */
    public void login(){
    
    }
    
    /**
     * 注册
     */
    public void register(){
    
    }
    
    /**
    * 重置密码
    */
    public void reset(){
    
    } 
    
    /**
     * 退出登录
     */
    public void logout(){

    }
}

这样,先将业务的方法写好,占住位置,待以后实现这些功能。
这么做的优点是,提醒自己需要完成的功能,避免因为实现某一功能太过深入,而忘记其他需要实现的功能,也避免了频繁切换查看需求文档。
本来这应该是测试驱动开发中的做法,但我们现在是开头,还没开始编写Service服务层,所以把这些业务都罗列到Controller中。

在这一阶段,我们需要考虑我们是否完全穷尽了该场景下的需求?
比如,是否支持第三方登录,限制尝试密码的次数等等需求。当然,如果用例是由别人给出的,那这就是别人应该考虑的了。

陈列所有需求之后,控制器层的编码远没有完成。

确定接收的参数

Controller层是处理与UI或通信协议有关的整个软件系统的门面,它的任务包括:

  • 匹配路由
  • 接收参数
  • 调用服务
  • 返回消息

那么接下来,我们优先进行哪个任务呢?
按道理,我们实现某个功能方法,需要考虑输入和输出,而输入的参数又依赖匹配的路由,所以应该先设计访问路由。然而,路由的设计需要通盘考虑所有功能,甚至可能需要restful风格的API。
所以,建议优先思考接收的参数。即,先与客户方确定好请求数据的格式

对于用户登录这一场景,首先想到接收账号密码,作为登录。
示例代码如下:

@RestController
public class UserController {
    @Autowired
    UserApplicationService service;
    /**
     * 登录
     */
    @PostMapping("/user/login")
    public String login(@RequestBody LoginCommand command){
        service.login(command);
    	return "登录成功!";
    }
}

由于登录功能实际上是交给服务层完成的,需要创建Service应用服务层文件。
应用服务层的代码暂时可以像Controller这样,依次编写方法占位。
示例代码:

@Service
public class UserApplicationService{
    @Transactional
    public void login(LoginCommand command){
    }
}

而返回的参数,当然不能仅仅返回个字符串,我们最后肯定要返回JSON的。
但是目前为止,我们还不清楚最后应该返回什么内容,所以先用“登录成功!”标记下来。

而路由的话,目前想不到什么比较好的路由设计,也先用“/user/login”标记下来,之后全盘考虑时再做修改。
这样,第一次迭代结束,可以先进行测试,确保访问正常。

其中,对于接收的参数,因为是通过JSON提交的,所以需要为它设计一个Command对象,用于接收消息体。

public class LoginCommand{
    private String username;
    private String password;
    //省略Getter和Setter、构造器等方法...
    //省略的方法可由Lombok或IDEAJ快速生成
}

同时,对于应用服务的接收参数的建议如下:

//应用服务推荐接收id或命令对象,命令操作是外部向领域模型发起的一次写操作,它封装原始数据类型
//用命令对象的好处是可以概览软件向外部提供的功能,领域模型与外部隔离
//技巧:若命令对象只有1-2个字段,甚至可以把命令对象解开,只传递原始数据
public OrderId createOrder(CreateOrderCommand command) ;
public void changeProductCount(String id, ChangeProductCountCommand command) ;
public void pay(String id, PayOrderCommand command) ;
public void changeAddressDetail(String id, String detail) ;

应用服务

应用服务,是领域模型的门面。在DDD中业务被提到第一优先级,所以我们希望对业务的处理能提取出来。

在DDD中,实现业务功能应该采用自顶向下的实现方式。

ApplicationService采用了门面模式,作为领域模型向外提供业务功能的总出入口,就像酒店的前台处理客户的不同需求一样。

//示例代码OrderApplicationService:
//该方法目的为改变订单中产品的数量,必须是事务操作
@Transactional
public void changeProductCount(String id, ChangeProductCountCommand command) {
    //接收参数为实体ID和命令对象
    //获得Order对象
    Order order = orderRepository.byId(orderId(id));
    //order自身业务逻辑:获得产品ID,改变数量
    order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());
    //order仓库保存对象
    orderRepository.save(order);
}

通过Controller层调用ApplicationService。

一个业务用例对应应用服务上的一个业务方法,即Controller控制器的方法与ApplicationService应用服务的方法对应。

//获得订单ID和命令对象
@PostMapping("/{id}/products")
public void changeProductCount(@PathVariable(name = "id") String id, @RequestBody @Valid ChangeProductCountCommand command) {
    //通过Controller层调用ApplicationService
    orderApplicationService.changeProductCount(id, command);
}

对业务需求的处理流程体现了DDD处理业务需求的最常见最典型的形式
应用服务作为总体协调者

  1. 先通过资源库获取到聚合根
  2. 然后调用聚合根中的业务方法
  3. 最后再次调用资源库保存聚合根。

获取聚合根

软件中的写操作要么是修改既有数据,要么是新建数据。
对于前者,DDD要从资源库获取聚合根,而新建数据要创建聚合根

聚合根的创建过程可简单可复杂
简单时,直接调用构造函数即可
复杂时,比如需要调用其他系统获取数据等

其中,使用工厂方法的主要动机是创建复杂的对象和聚合。
工厂应该提供一个创建对象的接口,接口封装所有创建对象的复杂操作过程。

//聚合根中的工厂方法
//工厂方法create(),业务上的创建
public static Product create(String name, String description, BigDecimal price) {
    return new Product(name, description, price);
}
//构造函数,技术上的创建
private Product(String name, String description, BigDecimal price) {
    this.id = ProductId.newProductId();
    this.name = name;
    this.description = description;
    this.price = price;
    this.createdAt = Instant.now();
}

工厂方法的优势:
1.分离技术和业务上的职责,表达通用语言
2.减少输入参数,减轻创建对象的负担
3.封装对象的创建,确保对象的创建处于正确状态
劣势:
由于要从持久化存储中获得聚合根,对性能上有影响

资源库

聚合根创建之后需要持久化到硬盘,否则在内存中会被垃圾回收机制回收。
而资源库作为聚合根的家,提供聚合根的查询和保存,类似于HashMap的get和set方法。
只有聚合根才配拥有资源库Repository,若是查询其他的功能则在展现层提供。

比如,实现Order的资源库OrderRepository如下:

//只提供两个方法
public void save(Order order) {
    String sql = "INSERT INTO ORDERS (ID, JSON_CONTENT) VALUES (:id, :json) " +
            "ON DUPLICATE KEY UPDATE JSON_CONTENT=:json;";
    Map<String, String> paramMap = of("id", order.getId().toString(), "json", objectMapper.writeValueAsString(order));
    jdbcTemplate.update(sql, paramMap);
}

public Order byId(OrderId id) {
    try {
        String sql = "SELECT JSON_CONTENT FROM ORDERS WHERE ID=:id;";
        return jdbcTemplate.queryForObject(sql, of("id", id.toString()), mapper());
    } catch (EmptyResultDataAccessException e) {
        throw new OrderNotFoundException(id);
    }
}

通常仓库接口会放在领域目录里,供领域模型调用,然后实现Repository会放基础设施目录里。比如MybatisRepository和JpaRepostiory提供不同的实现。

总结

综合下来,开发的第一次迭代过程如下:
1.接到需求后,考虑业务流程,划分出主题域,找出聚合根
2.根据聚合根,创建Controller文件,编写方法覆盖聚合根的该业务场景的所有功能。
3.开始准备完成第一个功能
4.处理输入的参数,根据需求创建命令对象,标记路由等。
5.创建应用服务文件,编写方法供Contoller层调用
6.暂时用字符串作为返回消息。
7.测试路由访问是否正常
在之前的第一次迭代完成后,可以测试是否能正常响应。接下来该完成实际业务逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值