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

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

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

通过本 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<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 为输入,在订单总价基础上计算折扣价格,返回打折之后的价格。

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 <= 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 的实现。

2.5 模型概念转化

在限界上下文集成时,经常需要对上游限界上下文中的概念进行转换,以避免概念的混淆。

例如,在用户成功激活后,自动为其创建名片。

在用户激活后,会从 User 限界上下文中发出 UserActivatedEvent 事件,Card 上下文监听事件,并将用户上下文内的概念转为为名片上下文中的概念。

@Valuepublic 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 是用户上下文,在用户激活后向外发布的领域事件。

@Servicepublic class UserEventHandlers {    @EventListener    public void handle(UserActivatedEvent event){        Card card = new Card();        card.setUserId(event.getUserId());        card.setName(event.getName());    }}

UserEventHandlers 在收到 UserActivatedEvent 事件后,将来自用户上下文中的概念转化为自己上下文中的概念 Card

2.6 在服务层中使用领域服务

领域服务可以在应用服务中使用,已完成特定的业务规则。

最常用的场景为,应用服务从存储库中获取相关实体并将它们传递到领域服务中。

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 中的 updatePasswordcheckPassword 在流程中都需要使用领域服务 PasswordEncoder,我们可以通过参数将 UserApplication 所保存的 PasswordEncoder 传入到业务方法中。

2.7 在领域层中使用领域服务

由于实体和领域服务拥有不同的生命周期,在实体依赖领域服务时,会变的非常棘手。

有时,一个实体需要领域服务来执行操作,以避免在应用服务中的拼接。此时,我们需要解决的核心问题是,在实体中如何获取服务的引用。通常情况下,有以下几种方式。

2.7.1 手工链接

如果一个实体依赖领域服务,同时我们自己在管理对象的构建,那么最简单的方式便是将相关服务通过构造函数传递进去。

还是以 PasswordEncoder 为例。

@Datapublic 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 方法,来完成服务的注入。

@Datapublic 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。

2.7.2 依赖注入

如果在使用 Spring 等 IOC 框架,我们可以在从 ORM 框架中获取实体后,使用依赖注入完成领域服务的注入。

@Datapublic 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 对象的依赖绑定,然后在进行业务处理。

2.7.3 服务定位器

有时在实体中添加字段以维持领域服务引用,会使的实体变得臃肿。此时,我们可以通过服务定位器进行领域服务的查找。

一般情况下,服务定位器会提供一组静态方法,以方便的获取其他服务。

@Componentpublic 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 获取领域服务。

@Datapublic 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 对象直接使用静态方法获取领域服务。

以上模式重点解决如果将领域服务注入到实体中,而 领域事件 模式从相反方向努力,解决如何阻止注入的发生。

2.7.4 领域事件解耦

一种完全避免将领域服务注入到实体中的模式是领域事件。

当重要的操作发生时,实体可以发布一个领域事件,注册了该事件的订阅器将处理该事件。此时,领域服务驻留在消息的订阅方内,而不是驻留在实体中。

比较常见的实例是用户通知,例如,在用户激活后,为用户发送一个短信通知。

@Datapublic 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 后,将对缓存的事件进行发布。

3. 领域服务建模模式

3.1 独立接口是否有必要

很多情况下,独立接口时没有必要的。我们只需创建一个实现类即可,其命名与领域服务相同(名称来自通用语言)。

但在下面情况下,独立接口时有必要的(独立接口对解耦是有好处的):

  • 存在多个实现。
  • 领域服务的实现依赖基础框架的支持。
  • 测试环节需要 mock 对象。

3.2 避免静态方法

对于行为建模,很多人第一反应是使用静态方法。但,领域服务比静态方法存在更多的好处。

领域服务比静态方法要好的多:

  1. 通过多态,适配多个实现,同时可以使用模板方法模式,对结构进行优化;
  2. 通过依赖注入,获取其他资源;
  3. 类名往往比方法名更能表达领域概念。

从表现力角度出发,类的表现力大于方法,方法的表现力大于代码。

3.3 优先使用领域事件进行解耦

领域事件是最优雅的解耦方案,基本上没有之一。我们将在领域事件中进行详解。

3.4 策略模式

当领域服务存在多个实现时,天然形成了策略模式。

当领域服务存在多个实现时,可以根据上下文信息,动态选择具体的实现,以增加系统的灵活性。

详见 PreferentialStrategy 实例。

4. 小结

  • 有时,行为不属于实体或值对象,但它是一个重要的领域概念,这就暗示我们需要使用领域服务模式。
  • 领域服务代表领域概念,它是对通用语言的一种建模。
  • 领域服务主要使用实体或值对象组成无状态的操作。
  • 领域服务位于领域模型中,对于依赖基础设施的领域服务,其接口定义位于领域模型中。
  • 过多的领域服务会导致贫血模型,使之与问题域无法很好的配合。
  • 过少的领域服务会导致将不正确的行为添加到实体或值对象上,造成概念的混淆。
  • 当实体依赖领域服务时,可以使用手工注入、依赖注入和领域事件等多种方式进行处理。

本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

阅读全文: http://gitbook.cn/gitchat/activity/5d551ee2fd2738650e9dd675

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App , GitChat 专享技术内容哦。

领域驱动设计战术模式:领域服务相关推荐

  1. 领域驱动设计战术模式--领域事件

    使用领域事件来捕获发生在领域中的一些事情. 领域驱动实践者发现他们可以通过了解更多发生在问题域中的事件,来更好的理解问题域.这些事件,就是领域事件,主要是与领域专家一起进行知识提炼环节中获得. 领域事 ...

  2. 领域驱动设计战术模式:领域事件

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第五篇,重心讲解领域事件模式. 在建模时,有时会遇 ...

  3. 领域驱动设计战术模式:实体

    领域驱动设计战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说更接地气,是提升个人格局比较好的切入点. 该文章为战术模式的第三篇,重心讲解实体模式. 实体是具有唯一标识的概 ...

  4. 领域驱动设计战术模式--值对象

    值对象虽然经常被掩盖在实体的阴影之下,但它却是非常重要的 DDD 概念. 值对象不具有身份,它纯粹用于描述实体的特性.处理不具有身份的值对象是很容易的,尤其是不变性与可组合性是支持易用性的两个特征. ...

  5. 我的领域驱动设计运用实例 - 领域啊领域

    一.前言 断断续续的也有在闲余时间接触领域驱动设计的相关知识,因为目前在工作中更多的还只是一名 crud boy,因此目前也只是对其中的某些知识点有知晓,实际使用的比较少,仅此而已.因此,趁着这个春节 ...

  6. 领域驱动设计-什么是领域驱动设计和怎么使用它

    这篇文章讨论领域驱动设计(DDD),DDD是建立在面向对象分析设计上开发软件的一种方法. 通过这篇文章我们解释什么是领域驱动设计,在现代开发周期中如何实现,使用DDD的优点和缺点. 什么是领域 定义D ...

  7. 基于DDD(领域驱动设计)的微服务设计实例

    目录 一.战略设计: 1.产品愿景 2.场景分析 3.领域建模 1)提取领域对象 2)构建聚合 3)划分界限上下文 4.微服务拆分 二.战术设计 1.分析微服务领域对象 1)服务识别和设计 2)聚合内 ...

  8. 如何运用领域驱动设计 - 领域服务

    概述 本文将介绍领域驱动设计(DDD)战术模式中另一个非常重要的概念 - 领域服务.在前面两篇博文中,我们已经学习到了什么是值对象和实体,并且能够比较清晰的定位它们自身的行为.但是在某些时候,你会发现 ...

  9. 如何运用领域驱动设计 - 聚合

    概述 DDD实战与进阶 - 值对象 如何运用DDD - 实体 如何运用领域驱动设计 - 领域服务 在前几篇的博文中,我们已经学习到了如何运用实体和值对象.随着我们所在领域的不断深入,领域模型变得逐渐清 ...

最新文章

  1. 机器学习模型 知乎_机器学习:模型评估之评估方法
  2. mysql基本操作 [http://www.cnblogs.com/ggjucheng/archive/2012/11/03/2752082.html]
  3. hdu 4722(记忆化搜索)
  4. MySQL为关联表添加数据
  5. 代码评审的不可能三角
  6. C语言中无符号数和有符号数相加问题
  7. [Codeforces1132G]Greedy Subsequences——线段树+单调栈
  8. ppt转html5 带动画_天府味道 小吃龙门阵____糖饼糖画 难以忘怀的童年趣味
  9. Java 性能测试的四项原则
  10. 【OI好题推荐 #1】洛谷-P1183 多边形的面积
  11. 2021湖北技能高考成绩时间查询,2020湖北技能高考成绩查询时间
  12. 中文搜索引擎2010Q2市场份额
  13. 或且非 java_Java且或非的符号
  14. Cannot get a STRING value from a NUMERIC cell
  15. Oracle—tablespace使用
  16. (C语言)signed和unsigned类型转化
  17. Java基于ssm的大学生社团管理系统 计算机毕业设计
  18. python能解决什么数据问题_浅谈Python数据分析
  19. 通过RSA实现数字签名
  20. 数据结构考研笔记(十五)——图的存储结构邻接矩阵、邻接表、十字链表、临界多重表的概念

热门文章

  1. Python栈的应用之二进制与十进制转换
  2. 【面经】兴业证券大数据工程师面经
  3. python实训day1
  4. 2020年年度关于前端学习与工作的心得体会
  5. tipask 问答系统 如何设置第三方一键登录
  6. 什么叫手机或设备的ID
  7. 在用户计算机上的万维网客户程序是( ),在用户计算机上的万维网客户程序是()...
  8. 微信小程序--订单查询页面
  9. JavaScript 实现BASE58加密 中文英文数字都可以加密
  10. java第八章习题,第八章 Servlet技术习题