在建模時,有時會遇到一些業務邏輯的概念,它放在實體或值對象中都不太合適。這就是可能須要建立領域服務的一個信號。緩存
從概念上說,領域服務表明領域概念,它們是存在於問題域中的行爲,它們產生於與領域專家的對話中,而且是領域模型的一部分。
模型中的領域服務表示一個無狀態的操做,他用於實現特定於某個領域的任務。
當領域中某個操做過程或轉化過程不是實體或值對象的職責時,咱們便應該將該操做放在一個單獨的元素中,即領域服務。同時務必保持該領域服務與通用語言是一致的,而且保證它是無狀態的。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 實例。