领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点。
该文章为战术模式的第四篇,重心讲解领域服务模式。
在建模时,有时会遇到一些业务逻辑的概念,它放在实体或值对象中都不太合适。这就是可能需要创建领域服务的一个信号。从概念上说,领域服务代表领域概念,它们是存在于问题域中的行为,它们产生于与领域专家的对话中,并且是领域模型的一部分。
通过本 Chat,您可以:
- 理解领域服务
- 实现领域服务
- 领域服务建模模式
- 小结
在建模时,有时会遇到一些业务逻辑的概念,它放在实体或值对象中都不太合适。这就是可能需要创建领域服务的一个信号。
1 理解领域服务从概念上说,领域服务代表领域概念,它们是存在于问题域中的行为,它们产生于与领域专家的对话中,并且是领域模型的一部分。
模型中的领域服务表示一个无状态的操作,他用于实现特定于某个领域的任务。当领域中某个操作过程或转化过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的元素中,即领域服务。同时务必保持该领域服务与通用语言是一致的,并且保证它是无状态的。
领域服务有几个重要的特征:
- 它代表领域概念。
- 它与通用语言保存一致,其中包括命名和内部逻辑。
- 它无状态。
- 领域服务与聚合在同一包中。
如果某操作不适合放在聚合和值对象上时,最好的方式便是将其建模成领域服务。
一般情况下,我们使用领域服务来组织实体、值对象并封装业务概念。领域服务适用场景如下:
- 执行一个显著的业务操作过程。
- 对领域对象进行转换。
- 以多个领域对象作为输入,进行计算,产生一个值对象。
当你认同并非所有的领域行为都需要封装在实体或值对象中,并明确领域服务是有用的建模手段后,就需要当心了。不要将过多的行为放到领域服务中,这样将导致贫血领域模型。
如果将过多的逻辑推入领域服务中,将导致不准确、难理解、贫血并且低概念的领域模型。显然,这样会抵消 DDD 的很多好处。
领域服务是排在值对象、实体模式之后的一个选项。有时,不得已为之是个比较好的方案。
1.3 与应用服务的对比应用服务,并不会处理业务逻辑,它是领域模型直接客户,进而是领域服务的客户方。
领域服务代表了存在于问题域内部的概念,他们的接口存在于领域模型中。相反,应用服务不表示领域概念,不包含业务规则,通常,他们不存在于领域模型中。
应用服务存在于服务层,处理像事务、订阅、存储等基础设施问题,以执行完整的业务用例。
应用服务从用户用例出发,是领域的直接用户,与领域关系密切,会有专门章节进行详解。
1.4 与基础设施服务的对比基础设施服务,从技术角度出发,为解决通用问题而进行的抽象。
比较典型的如,邮件发送服务、短信发送服务、定时服务等。
2. 实现领域服务 2.1 封装业务概念领域服务的执行一般会涉及实体或值对象,在其基础之上将行为封装成业务概念。
比较常见的就是银行转账,首先银行转账具有明显的领域概念,其次,由于同时涉及两个账号,该行为放在账号聚合中不太合适。因此,可以将其建模成领域服务。
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 明确定义了一个存在于通用语言的一个领域概念。领域服务存在于领域模型中,包含重要的业务规则。
2.2 业务计算业务计算,主要以实体或值对象作为输入,通过计算,返回一个实体或值对象。
常见场景如计算一个订单应用特定优惠策略后的应付金额。
public class OrderItem { private Long price; private Integer count; public Long getTotalPrice(){ return price * count; }}
OrderItem 中包括产品单价和产品数量,getTotalPrice 通过计算获取总价。
public class Order { private List 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 为输入,在订单总价基础上计算折扣价格,返回打折之后的价格。
2.3 规则切换根据业务流程,动态对规则进行切换。
还是以订单的优化策略为例。
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 为固定折扣策略,在订单总金额基础上进行固定折扣。
2.4 基础设施(第三方接口)隔离领域概念本身属于领域模型,但具体实现依赖于基础设施。
此时,我们需要将领域概念建模成领域服务,并将其置于模型层。将依赖于基础设施的具体实现类,放置于基础设施层。
比较典型的例子便是密码加密,加密服务应该位于领域中,但具体的实现依赖基础设施,应该放在基础设施层。
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 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 = 1 and = 1 and
关注
打赏