DDD实践:Service层应该返回Result吗?函数式编程与DDD的完美结合

问题的提出

在DDD(领域驱动设计)实践中,一个常见的问题是: Service层是否应该返回Result结果?Result结果应该只在Controller里返回吗?

这是一个关于分层架构和领域纯粹性的重要问题。让我们通过实际项目重构来探讨这个话题。

初步分析

在我们的项目中,存在两种Result类型:

  1. OperationResult - 领域层的操作结果封装
  2. Result - API响应结果封装(HTTP层)

原始代码问题

// Service接口混合使用了两种Result
public interface JobActionService {
    void LaunchCurrentJobAsync();
    OperationResult<String, String> LaunchJob(long 
    id);
    Result PauseCurrentJob();           //  
    Service层返回HTTP Result
    Result ResumeCurrentJob();          //  
    Service层返回HTTP Result
    OperationResult<String, String> 
    TerminateCurrentJob();
}

// Service实现直接返回HTTP Result
@Override
public Result PauseCurrentJob() {
    if (RunningParameter.jobStatus == null) {
        return Result.fail("任务未开启");      //  
        直接返回HTTP Result
    }
    RunningParameter.jobStatus = 2;
    return Result.ok();
}

这种设计违反了DDD的分层原则,Service层被HTTP层的概念污染了。

DDD与函数式编程的关系

领域层的Result不污染领域层

很多人认为Service层不应该返回Result,但实际上需要区分:

  • HTTP层的Result - 包含code/message/data等HTTP相关概念,不应该在Service层使用
  • 领域层的OperationResult - 纯粹表达"一个操作可能成功或失败"的领域概念 OperationResult<T, E> 是一个纯粹的领域抽象:
public class OperationResult<T, E> {
    private T value;      // 成功时的领域值
    private E error;      // 失败时的领域错误
    private boolean success;
}

这与以下概念是等价的:

  • Java的Optional - 表示值可能不存在
  • Scala的Try/Either - 表示操作可能失败
  • Kotlin的Result - 表示操作结果
  • Rust的Result<T, E> - 表示操作结果 这些都是 函数式编程在领域建模中的应用 ,而不是HTTP层的概念。

函数式编程与DDD不是对立的

实际上,函数式编程与DDD非常契合:

  1. 值对象与不可变性
// DDD值对象
public class Money {
    private final BigDecimal amount;
    private final Currency currency;
}

// 函数式风格:操作返回新对象而不是修改状态
public Money add(Money other) {
    if (!this.currency.equals(other.currency)) {
        throw new CurrencyMismatchException();
    }
    return new Money(this.amount.add(other.
    amount), this.currency);
}
  1. 领域服务与纯函数
// 领域服务:纯函数,无副作用
public class PricingService {
    public OperationResult<Price, PricingError> 
    calculatePrice(
        Product product, 
        Customer customer
    ) {
        if (product.isOutOfStock()) {
            return OperationResult.failure
            (PricingError.OUT_OF_STOCK);
        }
        
        BigDecimal discount = customer.
        getDiscountRate();
        Price price = product.getBasePrice().
        applyDiscount(discount);
        
        return OperationResult.success(price);
    }
}

重构方案

明确分层职责

Controller层 (HTTP)
    ↓ 调用
Application层 (用例编排)
    ↓ 调用
Domain层 (业务逻辑)
    ↓ 使用
OperationResult<T, E> (领域结果类型)

重构步骤 步骤1:创建领域错误枚举

package cn.south.fmos.client.domain.enums;

import lombok.Getter;

@Getter
public enum JobActionError {
    JOB_NOT_STARTED("任务未开启"),
    JOB_ALREADY_STOPPED("任务已停止,无需暂停"),
    JOB_ALREADY_RUNNING("任务正在运行"),
    JOB_CANNOT_RESUME("任务已停止,无法重启");

    private final String message;

    JobActionError(String message) {
        this.message = message;
    }
}

步骤2:修改Service接口

public interface JobActionService {
    void LaunchCurrentJobAsync();
    OperationResult<StringStringLaunchJob(long 
    id);
    OperationResult<String, JobActionError> 
    PauseCurrentJob();      //  使用
    OperationResult
    OperationResult<String, JobActionError> 
    ResumeCurrentJob();     //  使用
    OperationResult
    OperationResult<StringString> 
    TerminateCurrentJob();
}

步骤3:修改Service实现

@Override
public OperationResult<String, JobActionError> 
PauseCurrentJob() {
    if (RunningParameter.jobStatus == null) {
        return OperationResult.failure
        (JobActionError.JOB_NOT_STARTED);
    }
    if (RunningParameter.jobStatus == 0) {
        return OperationResult.failure
        (JobActionError.JOB_ALREADY_STOPPED);
    }
    RunningParameter.jobStatus = 2;
    return OperationResult.success("任务已暂停
    ");  //  返回成功消息
}

@Override
public OperationResult<String, JobActionError> 
ResumeCurrentJob() {
    if (RunningParameter.jobStatus == null) {
        return OperationResult.failure
        (JobActionError.JOB_NOT_STARTED);
    }
    if (RunningParameter.jobStatus == 1) {
        return OperationResult.failure
        (JobActionError.JOB_ALREADY_RUNNING);
    }
    if (RunningParameter.jobStatus == 0) {
        return OperationResult.failure
        (JobActionError.JOB_CANNOT_RESUME);
    }
    LaunchCurrentJobAsync();
    return OperationResult.success("任务已恢复");
}

步骤4:修改Controller层

@PostMapping("/pause")
public Result pause() throws CustomException {
    OperationResult<String, JobActionError> result 
    = jobActionService.PauseCurrentJob();
    return result.fold(
        success -> Result.ok
        (success),              //  使用Service返回
        的消息
        error -> Result.fail(error.getMessage())
    );
}

@PostMapping("/resume")
public Result resume() throws CustomException {
    OperationResult<String, JobActionError> result 
    = jobActionService.ResumeCurrentJob();
    return result.fold(
        success -> Result.ok
        (success),              //  使用Service返回
        的消息
        error -> Result.fail(error.getMessage())
    );
}

遇到的问题与解决方案

问题1:OperationResult<Void, E> 的陷阱

最初我们尝试使用 OperationResult<Void, JobActionError> ,但遇到了问题:

//  这样会抛出NullPointerException
return OperationResult.success(null);

原因: OperationResult.success(T value) 方法使用了 Objects.requireNonNull(value) 。

解决方案:使用String代替Void

//  使用String类型,返回成功消息
OperationResult<String, JobActionError> 
PauseCurrentJob();

// Service实现返回具体消息
return OperationResult.success("任务已暂停");

// Controller直接使用Service返回的消息
return result.fold(
    success -> Result.ok(success),
    error -> Result.fail(error.getMessage())
);

问题2:fold方法的工作原理

public <R> R fold(Function<T, R> successMapper, 
Function<E, R> failureMapper) {
    return success ? successMapper.apply(value) : 
    failureMapper.apply(error);
}

fold 方法已经帮我们 解包 了 OperationResult :

  • 成功时 : success 参数就是 String 类型的值本身
  • 失败时 : error 参数是 JobActionError 枚举类型 所以:
return result.fold(
    success -> Result.ok(success),              // 
     success已经是String,不需要getValue()
    error -> Result.fail(error.getMessage())    // 
     error是JobActionError,需要getMessage()
);

最佳实践总结

分层架构

层级 使用类型 职责 Controller层 Result HTTP响应封装,负责将领域结果转换为API响应 Service层 OperationResult<T, E> 领域操作结果,表达业务成功/失败,使用领域错误类型 Domain层 JobActionError 领域错误枚举,定义业务错误类型

核心原则

  1. Service层只返回 OperationResult (领域层Result)
  2. Controller层负责将 OperationResult 转换为 Result (HTTP层Result)
  3. Service层不应该返回 Result (HTTP层概念)
  4. 使用领域错误枚举而不是字符串错误消息
  5. 避免使用 OperationResult<Void, E> ,改用 OperationResult<String, E>

优点

  • 保持领域纯粹性 - Service层不依赖HTTP层概念
  • 类型安全 - 使用枚举而不是字符串,编译时检查
  • 函数式风格 - 支持链式调用、函数式组合
  • 关注点分离 - 每层职责明确
  • 易于测试 - 领域逻辑独立于HTTP层

结论

在DDD实践中,Service层 可以 返回Result,但要注意:

  1. 返回的是 领域层的OperationResult ,而不是HTTP层的Result
  2. 函数式编程与DDD是 兼容 的,可以很好地结合
  3. 领域层的Result不污染领域层,它是纯粹的领域抽象
  4. Controller层负责将领域结果转换为HTTP响应 这种设计既保持了领域的纯粹性,又充分利用了函数式编程的优势,是DDD实践中的一个良好模式。
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com