您当前的位置: 首页 >  蔚1

领域驱动设计战术模式:领域服务

蔚1 发布时间:2019-08-15 23:31:00 ,浏览量:1

领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点。

该文章为战术模式的第四篇,重心讲解领域服务模式。

在建模时,有时会遇到一些业务逻辑的概念,它放在实体或值对象中都不太合适。这就是可能需要创建领域服务的一个信号。从概念上说,领域服务代表领域概念,它们是存在于问题域中的行为,它们产生于与领域专家的对话中,并且是领域模型的一部分。

通过本 Chat,您可以:

  1. 理解领域服务
  2. 实现领域服务
  3. 领域服务建模模式
  4. 小结

在建模时,有时会遇到一些业务逻辑的概念,它放在实体或值对象中都不太合适。这就是可能需要创建领域服务的一个信号。

1 理解领域服务

从概念上说,领域服务代表领域概念,它们是存在于问题域中的行为,它们产生于与领域专家的对话中,并且是领域模型的一部分。

模型中的领域服务表示一个无状态的操作,他用于实现特定于某个领域的任务。当领域中某个操作过程或转化过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的元素中,即领域服务。同时务必保持该领域服务与通用语言是一致的,并且保证它是无状态的。

领域服务有几个重要的特征:

  • 它代表领域概念。
  • 它与通用语言保存一致,其中包括命名和内部逻辑。
  • 它无状态。
  • 领域服务与聚合在同一包中。
1.1 何时使用领域服务

如果某操作不适合放在聚合和值对象上时,最好的方式便是将其建模成领域服务。

一般情况下,我们使用领域服务来组织实体、值对象并封装业务概念。领域服务适用场景如下:

  • 执行一个显著的业务操作过程。
  • 对领域对象进行转换。
  • 以多个领域对象作为输入,进行计算,产生一个值对象。
1.2 避免贫血领域模型

当你认同并非所有的领域行为都需要封装在实体或值对象中,并明确领域服务是有用的建模手段后,就需要当心了。不要将过多的行为放到领域服务中,这样将导致贫血领域模型。

如果将过多的逻辑推入领域服务中,将导致不准确、难理解、贫血并且低概念的领域模型。显然,这样会抵消 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             
关注
打赏
1688896170
查看更多评论
0.1854s