戰術模式--領域服務

在建模時,有時會遇到一些業務邏輯的概念,它放在實體或值對象中都不太合適。這就是可能須要建立領域服務的一個信號。緩存

1 理解領域服務

從概念上說,領域服務表明領域概念,它們是存在於問題域中的行爲,它們產生於與領域專家的對話中,而且是領域模型的一部分。

模型中的領域服務表示一個無狀態的操做,他用於實現特定於某個領域的任務。
當領域中某個操做過程或轉化過程不是實體或值對象的職責時,咱們便應該將該操做放在一個單獨的元素中,即領域服務。同時務必保持該領域服務與通用語言是一致的,而且保證它是無狀態的。app

領域服務有幾個重要的特徵:框架

  • 它表明領域概念。
  • 它與通用語言保存一致,其中包括命名和內部邏輯。
  • 它無狀態。
  • 領域服務與聚合在同一包中。

1.1 什麼時候使用領域服務

若是某操做不適合放在聚合和值對象上時,最好的方式即是將其建模成領域服務。

通常狀況下,咱們使用領域服務來組織實體、值對象並封裝業務概念。領域服務適用場景以下:dom

  • 執行一個顯著的業務操做過程。
  • 對領域對象進行轉換。
  • 以多個領域對象做爲輸入,進行計算,產生一個值對象。

1.2 避免貧血領域模型

當你認同並不是全部的領域行爲都須要封裝在實體或值對象中,並明確領域服務是有用的建模手段後,就須要小心了。不要將過多的行爲放到領域服務中,這樣將致使貧血領域模型。

若是將過多的邏輯推入領域服務中,將致使不許確、難理解、貧血而且低概念的領域模型。顯然,這樣會抵消 DDD 的不少好處。ide

領域服務是排在值對象、實體模式以後的一個選項。有時,不得已爲之是個比較好的方案。函數

1.3 與應用服務的對比

應用服務,並不會處理業務邏輯,它是領域模型直接客戶,進而是領域服務的客戶方。

領域服務表明了存在於問題域內部的概念,他們的接口存在於領域模型中。相反,應用服務不表示領域概念,不包含業務規則,一般,他們不存在於領域模型中。測試

應用服務存在於服務層,處理像事務、訂閱、存儲等基礎設施問題,以執行完整的業務用例。優化

應用服務從用戶用例出發,是領域的直接用戶,與領域關係密切,會有專門章節進行詳解。

1.4 與基礎設施服務的對比

基礎設施服務,從技術角度出發,爲解決通用問題而進行的抽象。

比較典型的如,郵件發送服務、短信發送服務、定時服務等。ui

2. 實現領域服務

2.1 封裝業務概念

領域服務的執行通常會涉及實體或值對象,在其基礎之上將行爲封裝成業務概念。

比較常見的就是銀行轉帳,首先銀行轉帳具備明顯的領域概念,其次,因爲同時涉及兩個帳號,該行爲放在帳號聚合中不太合適。所以,能夠將其建模成領域服務。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 明肯定義了一個存在於通用語言的一個領域概念。領域服務存在於領域模型中,包含重要的業務規則。

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 上下文監聽事件,並將用戶上下文內的概念轉爲爲名片上下文中的概念。

@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

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 爲例。

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

2.7.2 依賴注入
若是在使用 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 對象的依賴綁定,而後在進行業務處理。

2.7.3 服務定位器
有時在實體中添加字段以維持領域服務引用,會使的實體變得臃腫。此時,咱們能夠經過服務定位器進行領域服務的查找。

通常狀況下,服務定位器會提供一組靜態方法,以方便的獲取其餘服務。

@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 對象直接使用靜態方法獲取領域服務。

以上模式重點解決若是將領域服務注入到實體中,而 領域事件 模式從相反方向努力,解決如何阻止注入的發生。
2.7.4 領域事件解耦
一種徹底避免將領域服務注入到實體中的模式是領域事件。

當重要的操做發生時,實體能夠發佈一個領域事件,註冊了該事件的訂閱器將處理該事件。此時,領域服務駐留在消息的訂閱方內,而不是駐留在實體中。

比較常見的實例是用戶通知,例如,在用戶激活後,爲用戶發送一個短信通知。

@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 後,將對緩存的事件進行發佈。

3. 領域服務建模模式

3.1 獨立接口是否有必要

不少狀況下,獨立接口時沒有必要的。咱們只需建立一個實現類便可,其命名與領域服務相同(名稱來自通用語言)。

但在下面狀況下,獨立接口時有必要的(獨立接口對解耦是有好處的):

  • 存在多個實現。
  • 領域服務的實現依賴基礎框架的支持。
  • 測試環節須要 mock 對象。

3.2 避免靜態方法

對於行爲建模,不少人第一反應是使用靜態方法。但,領域服務比靜態方法存在更多的好處。

領域服務比靜態方法要好的多:

  1. 經過多態,適配多個實現,同時可使用模板方法模式,對結構進行優化;
  2. 經過依賴注入,獲取其餘資源;
  3. 類名每每比方法名更能表達領域概念。
從表現力角度出發,類的表現力大於方法,方法的表現力大於代碼。

3.3 優先使用領域事件進行解耦

領域事件是最優雅的解耦方案,基本上沒有之一。咱們將在領域事件中進行詳解。

3.4 策略模式

當領域服務存在多個實現時,自然造成了策略模式。

當領域服務存在多個實現時,能夠根據上下文信息,動態選擇具體的實現,以增長系統的靈活性。

詳見 PreferentialStrategy 實例。

4. 小結

  • 有時,行爲不屬於實體或值對象,但它是一個重要的領域概念,這就暗示咱們須要使用領域服務模式。
  • 領域服務表明領域概念,它是對通用語言的一種建模。
  • 領域服務主要使用實體或值對象組成無狀態的操做。
  • 領域服務位於領域模型中,對於依賴基礎設施的領域服務,其接口定義位於領域模型中。
  • 過多的領域服務會致使貧血模型,使之與問題域沒法很好的配合。
  • 過少的領域服務會致使將不正確的行爲添加到實體或值對象上,形成概念的混淆。
  • 當實體依賴領域服務時,可使用手工注入、依賴注入和領域事件等多種方式進行處理。
相關文章
相關標籤/搜索