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模式将域断言的概念引入到我们的代码中。通过将前提条件检查暴露给单独的函数,调用者可以始终在执行操作之前验证输入并做出相应的反应。同时,如果不先检查前提条件,就不可能再执行操作。
这种方法的额外好处是将决策过程从服务转移到领域代码。这不仅使我们的领域逻辑保持在一起,而且促进了它的单元测试。同时,简化的服务可能不再需要那么多关注。