将业务逻辑集中在一起的简单模式


CanExecute/Execute模式背后的想法非常简单。让我们将决策制定(前提条件检查)与执行实际操作分开,但将它们都保留在域对象中。另外,如果不满足前提条件,我们将阻止执行该操作。

例子
假设一个社交媒体平台想要跟随其竞争对手并将其某些功能限制为“高级”客户。在这种情况下,只有付费用户才能更改他们的用户名。我们的第一次尝试非常简单:

class User {
    private UUID id;
    private String username;
    private UserType type;
        
    // ...

    UserType getType() {
        return type;
    }

    void setUsername(String username) {
        this.username = username;
    }
}


class UserService {

   
// ...

    void changeUsername(UUID userId, String updatedUsername) {
        User user = userRepository.getById();
        if (user.type != UserType.PAYING) {
           
// probably combined with an exception handler 
           
// to present a meaningful error message
            throw new PaidFeatureUnavailableException();
        }
    
        user.setUsername(updatedUsername);
        userRepository.save(user);
    }
   
}

我们服务类中的检查type(

user.type != UserType.PAYING
)不会阻止从其他地方更改用户名。
因此,仍然可以更改(设置)非付费客户的用户名,从而违反我们的域规则。
即使从测试代码来看,从域的角度来看执行非法操作也不应该是可能的。

其次,用户相关的逻辑已经泄露到对应的服务类中。

第一次重构


class User {
        
    // ...
    
   
// no more setUsername(...) method!

    void changeUsername(String updatedUsername) {
        if (type != UserType.PAYING) {
            throw new PaidFeatureUnavailableException();
        }
            
        this.username = updatedUsername;
    }
}

class UserService {

   
// ...

    void changeUsername(UUID userId, String updatedUsername) {
        User user = userRepository.getById();
       
// what about the error handling???
        user.changeUsername(updatedUsername);
        userRepository.save(user);
    }
    
}

我们所有的域逻辑现在都包含在User类中。通过删除username的setter方法,现在没有人能为免费用户更改其值了。

但是,UserService失去了对错误处理的控制。如果前提条件失败,域User类将抛出异常,中断执行流程。我们的服务可能会忽略它或捕获它并做其他事情。它们都不是一种“干净”的方法。

第二次重构

class User {
        
    // ...
    
    boolean changeUsername(String updatedUsername) {
        if (type != UserType.PAYING) {
            return false;
        }
            
        this.username = updatedUsername;
        return true;
    }
}

class UserService {

   
// ...

    void changeUsername(UUID userId, String updatedUsername) {
        User user = userRepository.getById();
        if (!user.changeUsername(updatedUsername)) {
           
// error handling logic here
        }
        userRepository.save(user);
    }
    
}


即使是现在,也没有什么能阻止调用者忽略方法的响应。在这种情况下,错过的用户名更新只会被忽视。这似乎是一个很难发现的潜在问题源。

CanExecute/Execute模式
CanExecute/Execute模式背后的想法非常简单。让我们将决策制定(前提条件检查)与执行实际操作分开,但将它们都保留在域对象中。另外,如果不满足前提条件,我们将阻止执行该操作。

class User {
        
    // ...
    
   
// can be called from the outside without side effects
    boolean canChangeUsername() {
        return type == UserType.PAYING;
    }

    void changeUsername(String updatedUsername) {
       
// fails if not met! (domain assertion)
        require(canChangeUsername(),
"Illegal username change"); 
            
        this.username = updatedUsername;
    }
}

class UserService {

   
// ...

    void changeUsername(UUID userId, String updatedUsername) {
        User user = userRepository.getById();
        if (!user.canChangeUsername()) {
           
// error handling logic here
        }
        user.changeUsername(updatedUsername);
        userRepository.save(user);
    }
    
}

当然,我们现在要运行两次先决条件检查。幸运的是,这样的逻辑很少需要大量计算。它也不应该涉及任何 I/O 操作,因为所有输入都可以作为参数传递。因此,我们为额外的域安全级别付出了相对较小的代价。然而,那些痴迷于性能的人可能需要寻找其他东西(可能包括一种完全不同的代码组织方式)。

概括
CanExecute /Execute模式将域断言的概念引入到我们的代码中。通过将前提条件检查暴露给单独的函数,调用者可以始终在执行操作之前验证输入并做出相应的反应。同时,如果不先检查前提条件,就不可能再执行操作。

这种方法的额外好处是将决策过程从服务转移到领域代码。这不仅使我们的领域逻辑保持在一起,而且促进了它的单元测试。同时,简化的服务可能不再需要那么多关注。