再有人問你DDD,把這篇文章丟給他
DDD(Domain-Driven Design,中文名領(lǐng)域模型設(shè)計)是一種軟件開發(fā)方法論,它強調(diào)將業(yè)務(wù)領(lǐng)域中的知識融入到軟件設(shè)計中。DDD 強調(diào)將軟件開發(fā)過程分為兩個主要階段:領(lǐng)域分析和領(lǐng)域建模。領(lǐng)域分析是指深入了解業(yè)務(wù)領(lǐng)域中的問題和需求,領(lǐng)域建模是將分析出的領(lǐng)域知識轉(zhuǎn)化為軟件模型。
在本文中,我不再過多說明DDD的來龍去脈,我將用多個例子來詳細(xì)說明使用 DDD 和不使用 DDD 的區(qū)別、優(yōu)勢和劣勢。
需求:假設(shè)我們正在開發(fā)一個銀行應(yīng)用程序,需要實現(xiàn)以下三個基本功能:
- 存款
- 取款
- 轉(zhuǎn)賬
我們可以使用面向?qū)ο缶幊陶Z言(如 Java)來實現(xiàn)這些功能。
public class Account { private int balance; public Account(int balance) { this.balance = balance; } public void deposit(int amount) { balance = amount; } public void withdraw(int amount) { if (balance < amount) { throw new InsufficientFundsException(); } balance -= amount; } public void transfer(Account destination, int amount) { withdraw(amount); destination.deposit(amount); } public int getBalance() { return balance; }}public class InsufficientFundsException extends RuntimeException {}
在這個示例中,我們定義了一個 Account 類表示銀行賬戶。它包含一個 balance 屬性表示余額,以及 deposit、withdraw、transfer 方法用于存款、取款和轉(zhuǎn)賬操作。
deposit方法用于存款,它會將傳入的金額加到賬戶余額中。withdraw方法用于取款,它會從賬戶余額中減去傳入的金額,如果余額不足,則會拋出 InsufficientFundsException 異常。transfer方法用于轉(zhuǎn)賬,它會調(diào)用 withdraw 和 deposit 方法實現(xiàn)轉(zhuǎn)賬操作。
存在的問題:
這個示例中沒有明確的領(lǐng)域模型,也沒有領(lǐng)域服務(wù)。所有的功能都由 Account 類實現(xiàn),這使得代碼變得簡單和易于理解。但是,這種設(shè)計方式存在一些問題。
- 這種設(shè)計方式缺乏領(lǐng)域模型。銀行業(yè)務(wù)領(lǐng)域非常復(fù)雜,它包含許多概念和關(guān)系。一個 Account類無法涵蓋所有的銀行業(yè)務(wù),也無法提供足夠的靈活性和可擴(kuò)展性。
- 這種設(shè)計方式缺乏領(lǐng)域服務(wù)。銀行業(yè)務(wù)中還包含許多與賬戶無關(guān)的操作,如查詢交易記錄、生成報告等。這些操作無法由 Account類實現(xiàn),需要另外定義領(lǐng)域服務(wù)。
- 這種設(shè)計方式缺乏可測試性。由于所有的功能都由一個類實現(xiàn),測試變得困難。在測試轉(zhuǎn)賬功能時,我們需要創(chuàng)建兩個賬戶對象并將它們連接起來,這使得測試變得復(fù)雜和冗長。
改進(jìn)
接下來,我們將使用 DDD 的方式重新設(shè)計上面的示例。首先,我們需要進(jìn)行領(lǐng)域分析,深入了解銀行業(yè)務(wù)中的概念和關(guān)系。例如,我們可以定義以下概念:
賬戶(Account):表示銀行賬戶。
交易(transaction):表示銀行交易,包括存款、取款和轉(zhuǎn)賬等。
銀行(Bank):表示銀行機構(gòu)。
我們可以定義這些概念的領(lǐng)域模型。例如,Account 類可以表示銀行賬戶,Transaction 類可以表示銀行交易,Bank 類可以表示銀行機構(gòu)。這些類都應(yīng)該是領(lǐng)域模型,它們應(yīng)該包含業(yè)務(wù)領(lǐng)域中的知識和規(guī)則。
public class Account { private int balance; public Account(int balance) { this.balance = balance; } public void deposit(int amount) { balance = amount; } public void withdraw(int amount) { if (balance < amount) { throw new InsufficientFundsException(); } balance -= amount; } public int getBalance() { return balance; }}public class Transaction { private Account source; private Account destination; private int amount; private LocalDateTime timestamp; public Transaction(Account source, Account destination, int amount) { this.source = source; this.destination = destination; this.amount = amount; this.timestamp = LocalDateTime.now(); } public void execute() { source.withdraw(amount); destination.deposit(amount); } public LocalDateTime getTimestamp() { return timestamp; }}public class Bank { private List<Account> accounts; public Bank() { this.accounts = new ArrayList<>(); } public void addAccount(Account account) { accounts.add(account); } public List<Account> getAccounts() { return accounts; } public void transfer(Account source, Account destination, int amount) { Transaction transaction = new Transaction(source, destination, amount); transaction.execute(); }}public class InsufficientFundsException extends RuntimeException {}
在這個示例中,我們定義了三個領(lǐng)域模型:Account、Transaction 和 Bank。**Account** 類和之前的示例相同,但它現(xiàn)在是一個領(lǐng)域模型。**Transaction** 類表示銀行交易,它包含 source、destination、amount 和 timestamp 屬性,分別表示交易的來源賬戶、目標(biāo)賬戶、金額和時間戳。execute 方法用于執(zhí)行交易。**Bank** 類表示銀行機構(gòu),它包含一個 accounts 屬性表示賬戶列表。addAccount 方法用于添加賬戶,getAccounts 方法用于獲取賬戶列表。transfer 方法用于執(zhí)行轉(zhuǎn)賬操作,它創(chuàng)建一個 Transaction 對象并調(diào)用其 execute 方法實現(xiàn)轉(zhuǎn)賬操作。
這個示例中使用了領(lǐng)域模型和領(lǐng)域事件來支持業(yè)務(wù)邏輯,這使得我們能夠更好地組織和管理業(yè)務(wù)邏輯,同時也使得代碼更加清晰和易于維護(hù)。
使用 DDD 和不使用 DDD 的比較
代碼結(jié)構(gòu)
使用 DDD 的示例中,代碼結(jié)構(gòu)更加清晰和易于理解。每個領(lǐng)域模型都有自己的職責(zé)和行為,使得代碼更加模塊化和可組合。而不使用 DDD 的示例中,所有的業(yè)務(wù)邏輯都被放在一個類中,導(dǎo)致代碼結(jié)構(gòu)混亂且難以維護(hù)。
代碼可讀性
使用 DDD 的示例中,代碼更加易于閱讀和理解。每個領(lǐng)域模型都有自己的概念和行為,使得代碼更加直觀和易于理解。而不使用 DDD 的示例中,所有的業(yè)務(wù)邏輯都被放在一個類中,導(dǎo)致代碼可讀性差。
測試
使用 DDD 的示例中,測試更加易于編寫和管理。每個領(lǐng)域模型都有自己的行為,可以單獨測試,使得測試更加模塊化和易于管理。而不使用 DDD 的示例中,所有的業(yè)務(wù)邏輯都被放在一個類中,導(dǎo)致測試難以編寫和管理。
擴(kuò)展性
使用 DDD 的示例中,代碼更加易于擴(kuò)展和修改。每個領(lǐng)域模型都有自己的職責(zé)和行為,使得修改和擴(kuò)展更加局部化和安全。而不使用 DDD 的示例中,所有的業(yè)務(wù)邏輯都被放在一個類中,導(dǎo)致擴(kuò)展和修改難度大。
再舉一個例子。
假設(shè)我們正在開發(fā)一個電商平臺,我們需要實現(xiàn)一個購物車模塊。購物車模塊需要完成以下功能:
- 將商品添加到購物車
- 從購物車中刪除商品
- 更新購物車中商品的數(shù)量
- 計算購物車中商品的總價
首先看一下不使用 DDD 的示例代碼:
public class ShoppingCart { private List<CartItem> cartItems = new ArrayList<>(); public void addItem(CartItem item) { cartItems.add(item); } public void removeItem(CartItem item) { cartItems.remove(item); } public void updateItemQuantity(CartItem item, int quantity) { for (CartItem cartItem : cartItems) { if (cartItem.equals(item)) { cartItem.setQuantity(quantity); break; } } } public BigDecimal calculateTotalPrice() { BigDecimal totalPrice = BigDecimal.ZERO; for (CartItem cartItem : cartItems) { totalPrice = totalPrice.add(cartItem.getPrice().multiply(BigDecimal.valueOf(cartItem.getQuantity()))); } return totalPrice; }}
在這個示例代碼中,購物車的所有邏輯都被放在一個類中,導(dǎo)致這個類的職責(zé)非常復(fù)雜。如果在將來需要修改購物車的某個功能,就需要修改這個類的某個方法,這可能會影響到購物車的其他功能,增加代碼的復(fù)雜度。
使用 DDD 改進(jìn):
首先定義一個購物車領(lǐng)域模型:
public class ShoppingCart { private List<CartItem> cartItems = new ArrayList<>(); public void addItem(CartItem item) { cartItems.add(item); } public void removeItem(CartItem item) { cartItems.remove(item); } public void updateItemQuantity(CartItem item, int quantity) { for (CartItem cartItem : cartItems) { if (cartItem.equals(item)) { cartItem.setQuantity(quantity); break; } } } public BigDecimal calculateTotalPrice() { BigDecimal totalPrice = BigDecimal.ZERO; for (CartItem cartItem : cartItems) { totalPrice = totalPrice.add(cartItem.getPrice().multiply(BigDecimal.valueOf(cartItem.getQuantity()))); } return totalPrice; }}
購物車領(lǐng)域模型只負(fù)責(zé)購物車的業(yè)務(wù)邏輯,包括將商品添加到購物車、從購物車中刪除商品、更新購物車中商品的數(shù)量和計算購物車中商品的總價。
然后定義一個購物車服務(wù),它負(fù)責(zé)將購物車領(lǐng)域模型和其他服務(wù)進(jìn)行組合和協(xié)調(diào):
public class ShoppingCartService { private ShoppingCartRepository shoppingCartRepository; private ProductService productService; public void addToCart(Long productId, int quantity, ShoppingCart shoppingCart) { Product product = productService.getProductById(productId); CartItem cartItem = new CartItem(product, quantity); shoppingCart.addItem(cartItem); shoppingCartRepository.save(shoppingCart); } public void removeFromCart(Long productId, ShoppingCart shoppingCart) { Product product = productService.getProductById(productId); CartItem cartItem = shoppingCart.findItemByProduct(product); if (cartItem != null) { shoppingCart.removeItem(cartItem); shoppingCartRepository.save(shoppingCart); } } public void updateCartItemQuantity(Long productId, int quantity, ShoppingCart shoppingCart) { Product product = productService.getProductById(productId); CartItem cartItem = shoppingCart.findItemByProduct(product); if (cartItem != null) { cartItem.setQuantity(quantity); shoppingCart.updateItemQuantity(cartItem); shoppingCartRepository.save(shoppingCart); } } public BigDecimal calculateCartTotalPrice(ShoppingCart shoppingCart) { return shoppingCart.calculateTotalPrice(); }}
購物車服務(wù)將購物車領(lǐng)域模型和產(chǎn)品服務(wù)進(jìn)行組合,負(fù)責(zé)將商品添加到購物車、從購物車中刪除商品、更新購物車中商品的數(shù)量和計算購物車中商品的總價。
通過將購物車領(lǐng)域模型和購物車服務(wù)進(jìn)行分離,我們可以使代碼更加可維護(hù)和可擴(kuò)展。例如,如果我們需要添加一個新的功能,例如促銷或折扣,我們可以簡單地修改購物車服務(wù)而不必改變購物車領(lǐng)域模型。同樣,如果我們需要更改購物車領(lǐng)域模型,我們也可以更改它而不必改變購物車服務(wù)。
再舉一個簡單的例子,例如一個簡單的博客系統(tǒng)。在不使用 DDD 的情況下,可能會編寫以下代碼:
public class BlogService { private BlogRepository blogRepository; public BlogService(BlogRepository blogRepository) { this.blogRepository = blogRepository; } public void createBlog(String title, String content) { Blog blog = new Blog(title, content); blogRepository.save(blog); } public void updateBlog(Long id, String title, String content) { Blog blog = blogRepository.findById(id); blog.setTitle(title); blog.setContent(content); blogRepository.save(blog); } public void deleteBlog(Long id) { Blog blog = blogRepository.findById(id); blogRepository.delete(blog); }}
在上面的代碼中,我們可以看到 BlogService 類,該類負(fù)責(zé)創(chuàng)建、更新和刪除博客。但是,這個類并沒有定義博客的業(yè)務(wù)邏輯,例如如何驗證博客的標(biāo)題和內(nèi)容是否有效,如何處理博客的標(biāo)簽或評論等等。這可能會導(dǎo)致代碼復(fù)雜度的增加,并且難以擴(kuò)展。
使用 DDD 的話,我們可以將博客作為領(lǐng)域模型進(jìn)行定義,例如:
public class Blog { private Long id; private String title; private String content; private List<Tag> tags; private List<Comment> comments; public Blog(String title, String content) { this.title = title; this.content = content; this.tags = new ArrayList<>(); this.comments = new ArrayList<>(); } public void setTitle(String title) { // 驗證標(biāo)題是否有效 this.title = title; } public void setContent(String content) { // 驗證內(nèi)容是否有效 this.content = content; } public void addTag(Tag tag) { // 處理標(biāo)簽 this.tags.add(tag); } public void addComment(Comment comment) { // 處理評論 this.comments.add(comment); } // getter 和 setter 略}
在上面的代碼中,我們可以看到,Blog 類定義了博客的業(yè)務(wù)邏輯,例如如何驗證博客的標(biāo)題和內(nèi)容是否有效,如何處理博客的標(biāo)簽和評論等等。現(xiàn)在,我們可以使用 BlogService 類來創(chuàng)建、更新和刪除博客,同時使用 Blog 類來處理博客的業(yè)務(wù)邏輯,例如添加標(biāo)簽和評論等等。這樣,我們可以將代碼分離到不同的領(lǐng)域模型中,使代碼更加清晰和易于維護(hù)。
總結(jié)
使用 DDD 的示例比不使用 DDD 的示例更加優(yōu)秀。DDD 提供了一種更好的方式來組織和管理業(yè)務(wù)邏輯,使得代碼更加模塊化、可組合、易于維護(hù)和擴(kuò)展。雖然使用 DDD 可能會增加代碼量和開發(fā)時間,但是它可以帶來更好的代碼質(zhì)量和更好的開發(fā)效率。