在建模时,有时会遇到一些业务逻辑的概念,它放在实体或值对象中都不太合适。这就是可能须要建立领域服务的一个信号。缓存
从概念上说,领域服务表明领域概念,它们是存在于问题域中的行为,它们产生于与领域专家的对话中,而且是领域模型的一部分。
模型中的领域服务表示一个无状态的操做,他用于实现特定于某个领域的任务。
当领域中某个操做过程或转化过程不是实体或值对象的职责时,咱们便应该将该操做放在一个单独的元素中,即领域服务。同时务必保持该领域服务与通用语言是一致的,而且保证它是无状态的。app
领域服务有几个重要的特征:框架
若是某操做不适合放在聚合和值对象上时,最好的方式即是将其建模成领域服务。
通常状况下,咱们使用领域服务来组织实体、值对象并封装业务概念。领域服务适用场景以下:dom
当你认同并不是全部的领域行为都须要封装在实体或值对象中,并明确领域服务是有用的建模手段后,就须要小心了。不要将过多的行为放到领域服务中,这样将致使贫血领域模型。
若是将过多的逻辑推入领域服务中,将致使不许确、难理解、贫血而且低概念的领域模型。显然,这样会抵消 DDD 的不少好处。ide
领域服务是排在值对象、实体模式以后的一个选项。有时,不得已为之是个比较好的方案。函数
应用服务,并不会处理业务逻辑,它是领域模型直接客户,进而是领域服务的客户方。
领域服务表明了存在于问题域内部的概念,他们的接口存在于领域模型中。相反,应用服务不表示领域概念,不包含业务规则,一般,他们不存在于领域模型中。测试
应用服务存在于服务层,处理像事务、订阅、存储等基础设施问题,以执行完整的业务用例。优化
应用服务从用户用例出发,是领域的直接用户,与领域关系密切,会有专门章节进行详解。
基础设施服务,从技术角度出发,为解决通用问题而进行的抽象。
比较典型的如,邮件发送服务、短信发送服务、定时服务等。ui
领域服务的执行通常会涉及实体或值对象,在其基础之上将行为封装成业务概念。
比较常见的就是银行转帐,首先银行转帐具备明显的领域概念,其次,因为同时涉及两个帐号,该行为放在帐号聚合中不太合适。所以,能够将其建模成领域服务。this
public class Account extends JpaAggregate { private Long totalAmount; public void checkBalance(Long amount) { if (amount > this.totalAmount){ throw new IllegalArgumentException("余额不足"); } } public void reduce(Long amount) { this.totalAmount = this.totalAmount - amount; } public void increase(Long amount) { this.totalAmount = this.totalAmount + amount; } }
Account 提供余额检测、扣除和添加等基本功能。
public class TransferService implements DomainService { public void transfer(Account from, Account to, Long amount){ from.checkBalance(amount); from.reduce(amount); to.increase(amount); } }
TransferService 按照业务规则,指定转帐流程。
TransferService 明肯定义了一个存在于通用语言的一个领域概念。领域服务存在于领域模型中,包含重要的业务规则。
业务计算,主要以实体或值对象做为输入,经过计算,返回一个实体或值对象。
常见场景如计算一个订单应用特定优惠策略后的应付金额。
public class OrderItem { private Long price; private Integer count; public Long getTotalPrice(){ return price * count; } }
OrderItem 中包括产品单价和产品数量,getTotalPrice 经过计算获取总价。
public class Order { private List<OrderItem> items = Lists.newArrayList(); public Long getTotalPrice(){ return this.items.stream() .mapToLong(orderItem -> orderItem.getTotalPrice()) .sum(); } }
Order 由多个 OrderItem 组成,getTotalPrice 遍历全部的 OrderItem,计算订单总价。
public class OrderAmountCalculator { public Long calculate(Order order, PreferentialStrategy preferentialStrategy){ return preferentialStrategy.calculate(order.getTotalPrice()); } }
OrderAmountCalculator 以实体 Order 和领域服务 PreferentialStrategy 为输入,在订单总价基础上计算折扣价格,返回打折以后的价格。
根据业务流程,动态对规则进行切换。
仍是以订单的优化策略为例。
public interface PreferentialStrategy { Long calculate(Long amount); }
PreferentialStrategy 为策略接口。
public class FullReductionPreferentialStrategy implements PreferentialStrategy{ private final Long fullAmount; private final Long reduceAmount; public FullReductionPreferentialStrategy(Long fullAmount, Long reduceAmount) { this.fullAmount = fullAmount; this.reduceAmount = reduceAmount; } @Override public Long calculate(Long amount) { if (amount > fullAmount){ return amount - reduceAmount; } return amount; } }
FullReductionPreferentialStrategy 为满减策略,当订单总金额超过特定值时,直接进行减免。
public class FixedDiscountPreferentialStrategy implements PreferentialStrategy{ private final Double descount; public FixedDiscountPreferentialStrategy(Double descount) { this.descount = descount; } @Override public Long calculate(Long amount) { return Math.round(amount * descount); } }
FixedDiscountPreferentialStrategy 为固定折扣策略,在订单总金额基础上进行固定折扣。
领域概念自己属于领域模型,但具体实现依赖于基础设施。
此时,咱们须要将领域概念建模成领域服务,并将其置于模型层。将依赖于基础设施的具体实现类,放置于基础设施层。
比较典型的例子即是密码加密,加密服务应该位于领域中,但具体的实现依赖基础设施,应该放在基础设施层。
public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); }
PasswordEncoder 提供密码加密和密码验证功能。
public class BCryptPasswordEncoder implements PasswordEncoder { private Pattern BCRYPT_PATTERN = Pattern .compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}"); private final Log logger = LogFactory.getLog(getClass()); private final int strength; private final SecureRandom random; public BCryptPasswordEncoder() { this(-1); } public BCryptPasswordEncoder(int strength) { this(strength, null); } public BCryptPasswordEncoder(int strength, SecureRandom random) { if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) { throw new IllegalArgumentException("Bad strength"); } this.strength = strength; this.random = random; } public String encode(CharSequence rawPassword) { String salt; if (strength > 0) { if (random != null) { salt = BCrypt.gensalt(strength, random); } else { salt = BCrypt.gensalt(strength); } } else { salt = BCrypt.gensalt(); } return BCrypt.hashpw(rawPassword.toString(), salt); } public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.length() == 0) { logger.warn("Empty encoded password"); return false; } if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) { logger.warn("Encoded password does not look like BCrypt"); return false; } return BCrypt.checkpw(rawPassword.toString(), encodedPassword); } }
BCryptPasswordEncoder 提供基于 BCrypt 的实现。
public class SCryptPasswordEncoder implements PasswordEncoder { private final Log logger = LogFactory.getLog(getClass()); private final int cpuCost; private final int memoryCost; private final int parallelization; private final int keyLength; private final BytesKeyGenerator saltGenerator; public SCryptPasswordEncoder() { this(16384, 8, 1, 32, 64); } public SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength) { if (cpuCost <= 1) { throw new IllegalArgumentException("Cpu cost parameter must be > 1."); } if (memoryCost == 1 && cpuCost > 65536) { throw new IllegalArgumentException("Cpu cost parameter must be > 1 and < 65536."); } if (memoryCost < 1) { throw new IllegalArgumentException("Memory cost must be >= 1."); } int maxParallel = Integer.MAX_VALUE / (128 * memoryCost * 8); if (parallelization < 1 || parallelization > maxParallel) { throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel + " (based on block size r of " + memoryCost + ")"); } if (keyLength < 1 || keyLength > Integer.MAX_VALUE) { throw new IllegalArgumentException("Key length must be >= 1 and <= " + Integer.MAX_VALUE); } if (saltLength < 1 || saltLength > Integer.MAX_VALUE) { throw new IllegalArgumentException("Salt length must be >= 1 and <= " + Integer.MAX_VALUE); } this.cpuCost = cpuCost; this.memoryCost = memoryCost; this.parallelization = parallelization; this.keyLength = keyLength; this.saltGenerator = KeyGenerators.secureRandom(saltLength); } public String encode(CharSequence rawPassword) { return digest(rawPassword, saltGenerator.generateKey()); } public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.length() < keyLength) { logger.warn("Empty encoded password"); return false; } return decodeAndCheckMatches(rawPassword, encodedPassword); } private boolean decodeAndCheckMatches(CharSequence rawPassword, String encodedPassword) { String[] parts = encodedPassword.split("\\$"); if (parts.length != 4) { return false; } long params = Long.parseLong(parts[1], 16); byte[] salt = decodePart(parts[2]); byte[] derived = decodePart(parts[3]); int cpuCost = (int) Math.pow(2, params >> 16 & 0xffff); int memoryCost = (int) params >> 8 & 0xff; int parallelization = (int) params & 0xff; byte[] generated = SCrypt.generate(Utf8.encode(rawPassword), salt, cpuCost, memoryCost, parallelization, keyLength); if (derived.length != generated.length) { return false; } int result = 0; for (int i = 0; i < derived.length; i++) { result |= derived[i] ^ generated[i]; } return result == 0; } private String digest(CharSequence rawPassword, byte[] salt) { byte[] derived = SCrypt.generate(Utf8.encode(rawPassword), salt, cpuCost, memoryCost, parallelization, keyLength); String params = Long .toString(((int) (Math.log(cpuCost) / Math.log(2)) << 16L) | memoryCost << 8 | parallelization, 16); StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2); sb.append("$").append(params).append('$'); sb.append(encodePart(salt)).append('$'); sb.append(encodePart(derived)); return sb.toString(); } private byte[] decodePart(String part) { return Base64.getDecoder().decode(Utf8.encode(part)); } private String encodePart(byte[] part) { return Utf8.decode(Base64.getEncoder().encode(part)); } }
SCryptPasswordEncoder 提供基于 SCrypt 的实现。
在限界上下文集成时,常常须要对上游限界上下文中的概念进行转换,以免概念的混淆。
例如,在用户成功激活后,自动为其建立名片。
在用户激活后,会从 User 限界上下文中发出 UserActivatedEvent 事件,Card 上下文监听事件,并将用户上下文内的概念转为为名片上下文中的概念。
@Value public class UserActivatedEvent extends AbstractDomainEvent { private final String name; private final Long userId; public UserActivatedEvent(String name, Long userId) { this.name = name; this.userId = userId; } }
UserActivatedEvent 是用户上下文,在用户激活后向外发布的领域事件。
@Service public class UserEventHandlers { @EventListener public void handle(UserActivatedEvent event){ Card card = new Card(); card.setUserId(event.getUserId()); card.setName(event.getName()); } }
UserEventHandlers 在收到 UserActivatedEvent 事件后,未来自用户上下文中的概念转化为本身上下文中的概念 Card。
领域服务能够在应用服务中使用,已完成特定的业务规则。
最经常使用的场景为,应用服务从存储库中获取相关实体并将它们传递到领域服务中。
public class OrderApplication { @Autowired private OrderRepository orderRepository; @Autowired private OrderAmountCalculator orderAmountCalculator; @Autowired private Map<String, PreferentialStrategy> strategyMap; public Long calculateOrderTotalPrice(Long orderId, String strategyName){ Order order = this.orderRepository.getById(orderId).orElseThrow(()->new AggregateNotFountException(String.valueOf(orderId))); PreferentialStrategy strategy = this.strategyMap.get(strategyName); Preconditions.checkArgument(strategy != null); return this.orderAmountCalculator.calculate(order, strategy); } }
OrderApplication 首先经过 OrderRepository 获取 Order 信息,而后获取对应的 PreferentialStrategy,最后调用 OrderAmountCalculator 完成金额计算。
在服务层使用,领域服务和其余领域对象能够根据需求很容易的拼接在一块儿。
固然,咱们也能够将领域服务做为业务方法的参数进行传递。
public class UserApplication extends AbstractApplication { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserRepository userRepository; public void updatePassword(Long userId, String password){ updaterFor(this.userRepository) .id(userId) .update(user -> user.updatePassword(password, this.passwordEncoder)) .call(); } public boolean checkPassword(Long userId, String password){ return this.userRepository.getById(userId) .orElseThrow(()-> new AggregateNotFountException(String.valueOf(userId))) .checkPassword(password, this.passwordEncoder); } }
UserApplication 中的 updatePassword 和 checkPassword 在流程中都须要使用领域服务 PasswordEncoder,咱们能够经过参数将 UserApplication 所保存的 PasswordEncoder 传入到业务方法中。
因为实体和领域服务拥有不一样的生命周期,在实体依赖领域服务时,会变的很是棘手。
有时,一个实体须要领域服务来执行操做,以免在应用服务中的拼接。此时,咱们须要解决的核心问题是,在实体中如何获取服务的引用。一般状况下,有如下几种方式。
若是一个实体依赖领域服务,同时咱们本身在管理对象的构建,那么最简单的方式即是将相关服务经过构造函数传递进去。
仍是以 PasswordEncoder 为例。
@Data public class User extends JpaAggregate { private final PasswordEncoder passwordEncoder; private String password; public User(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } public void updatePassword(String pwd){ setPassword(passwordEncoder.encode(pwd)); } public boolean checkPassword(String pwd){ return passwordEncoder.matches(pwd, getPassword()); } }
若是,咱们彻底手工维护 User 的建立,能够在构造函数中传入领域服务。
固然,若是实体是经过 ORM 框架获取的,经过构造函数传递将变得比较棘手,咱们能够为其添加一个 init 方法,来完成服务的注入。
@Data public class User extends JpaAggregate { private PasswordEncoder passwordEncoder; private String password; public void init(PasswordEncoder passwordEncoder){ this.setPasswordEncoder(passwordEncoder); } public User(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } public void updatePassword(String pwd){ setPassword(passwordEncoder.encode(pwd)); } public boolean checkPassword(String pwd){ return passwordEncoder.matches(pwd, getPassword()); } }
经过 ORM 框架获取 User 后,调用 init 方法设置 PasswordEncoder。
若是在使用 Spring 等 IOC 框架,咱们能够在从 ORM 框架中获取实体后,使用依赖注入完成领域服务的注入。
@Data public class User extends JpaAggregate { @Autowired private PasswordEncoder passwordEncoder; private String password; public void updatePassword(String pwd){ setPassword(passwordEncoder.encode(pwd)); } public boolean checkPassword(String pwd){ return passwordEncoder.matches(pwd, getPassword()); } }
User 直接使用 @Autowired 注入领域服务。
public class UserApplication extends AbstractApplication { @Autowired private AutowireCapableBeanFactory beanFactory; @Autowired private UserRepository userRepository; public void updatePassword(Long userId, String password){ User user = this.userRepository.getById(userId).orElseThrow(() -> new AggregateNotFountException(String.valueOf(userId))); this.beanFactory.autowireBean(user); user.updatePassword(password); this.userRepository.save(user); } public boolean checkPassword(Long userId, String password){ User user = this.userRepository.getById(userId).orElseThrow(() -> new AggregateNotFountException(String.valueOf(userId))); this.beanFactory.autowireBean(user); return user.checkPassword(password); } }
UserApplication 在获取 User 对象后,首先调用 autowireBean 完成 User 对象的依赖绑定,而后在进行业务处理。
有时在实体中添加字段以维持领域服务引用,会使的实体变得臃肿。此时,咱们能够经过服务定位器进行领域服务的查找。
通常状况下,服务定位器会提供一组静态方法,以方便的获取其余服务。
@Component public class ServiceLocator implements ApplicationContextAware { private static ApplicationContext APPLICATION; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { APPLICATION = applicationContext; } public static <T> T getService(Class<T> service){ return APPLICATION.getBean(service); } }
ServiceLocator 实现 ApplicationContextAware 接口,经过 Spring 回调将 ApplicationContext 绑定到静态字段 APPLICATION 上。getService 方法直接使用 ApplicationContext 获取领域服务。
@Data public class User extends JpaAggregate { private String password; public void updatePassword(String pwd){ setPassword(ServiceLocator.getService(PasswordEncoder.class).encode(pwd)); } public boolean checkPassword(String pwd){ return ServiceLocator.getService(PasswordEncoder.class).matches(pwd, getPassword()); } }
User 对象直接使用静态方法获取领域服务。
以上模式重点解决若是将领域服务注入到实体中,而 领域事件 模式从相反方向努力,解决如何阻止注入的发生。
一种彻底避免将领域服务注入到实体中的模式是领域事件。
当重要的操做发生时,实体能够发布一个领域事件,注册了该事件的订阅器将处理该事件。此时,领域服务驻留在消息的订阅方内,而不是驻留在实体中。
比较常见的实例是用户通知,例如,在用户激活后,为用户发送一个短信通知。
@Data public class User extends JpaAggregate { private UserStatus status; private String name; private String password; public void activate(){ setStatus(UserStatus.ACTIVATED); registerEvent(new UserActivatedEvent(getName(), getId())); } }
首先,User 在成功 activate 后,将自动注册 UserActivatedEvent 事件。
public class UserApplication extends AbstractApplication { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserRepository userRepository; private DomainEventBus domainEventBus = new DefaultDomainEventBus(); @PostConstruct public void init(){ this.domainEventBus.register(UserActivatedEvent.class, event -> { sendSMSNotice(event.getUserId(), event.getName()); }); } private void sendSMSNotice(Long userId, String name) { // 发送短信通知 } public void activate(Long userId){ updaterFor(this.userRepository) .publishBy(domainEventBus) .id(userId) .update(user -> user.activate()) .call(); } }
UserApplication 经过 Spring 的回调方法 init,订阅 UserActivatedEvent 事件,在事件触发后执行发短信逻辑。activate 方法在成功更新 User 后,将对缓存的事件进行发布。
不少状况下,独立接口时没有必要的。咱们只需建立一个实现类便可,其命名与领域服务相同(名称来自通用语言)。
但在下面状况下,独立接口时有必要的(独立接口对解耦是有好处的):
对于行为建模,不少人第一反应是使用静态方法。但,领域服务比静态方法存在更多的好处。
领域服务比静态方法要好的多:
从表现力角度出发,类的表现力大于方法,方法的表现力大于代码。
领域事件是最优雅的解耦方案,基本上没有之一。咱们将在领域事件中进行详解。
当领域服务存在多个实现时,自然造成了策略模式。
当领域服务存在多个实现时,能够根据上下文信息,动态选择具体的实现,以增长系统的灵活性。
详见 PreferentialStrategy 实例。